diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 95c2601a..45c84df3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,6 +15,7 @@ Please include your initializer, sidekiq.yml, and any error message with the ful If you are using an old version, have you checked the changelogs to see if your issue has been fixed in a later version? +https://github.com/gemhome/sidekiq-delay_extensions/blob/main/Changes.md https://github.com/mperham/sidekiq/blob/main/Changes.md https://github.com/mperham/sidekiq/blob/main/Pro-Changes.md https://github.com/mperham/sidekiq/blob/main/Ent-Changes.md diff --git a/.gitignore b/.gitignore index 6f286de7..d2af119d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ vendor/ tmp/ pkg/*.gem .byebug_history +Gemfile.lock diff --git a/.standard.yml b/.standard.yml index 22990b4f..3abc0644 100644 --- a/.standard.yml +++ b/.standard.yml @@ -5,9 +5,7 @@ ignore: - test/**/* - examples/**/* - myapp/**/* - - 'lib/sidekiq.rb': - - Lint/InheritException - - 'lib/sidekiq/extensions/**/*': + - 'lib/sidekiq/delay_extensions/**/*': - Style/MissingRespondToMissing - 'lib/**/*': - Lint/RescueException diff --git a/3.0-Upgrade.md b/3.0-Upgrade.md deleted file mode 100644 index 8be66fa3..00000000 --- a/3.0-Upgrade.md +++ /dev/null @@ -1,70 +0,0 @@ -# Upgrading to Sidekiq 3.0 - -Sidekiq 3.0 brings several new features but also removes old APIs and -changes a few data elements in Redis. To upgrade cleanly: - -* Upgrade to the latest Sidekiq 2.x and run it for a few weeks. - `gem 'sidekiq', '< 3'` - This is only needed if you have retries pending. -* 3rd party gems which use **client-side middleware** will need to update - due to an API change. The Redis connection for a particular job is - passed thru the middleware to handle sharding where jobs can - be pushed to different redis server instances. - - `def call(worker_class, msg, queue, redis_pool)` - - Client-side middleware should use `redis_pool.with { |conn| ... }` to - perform Redis operations and **not** `Sidekiq.redis`. -* If you used the capistrano integration, you'll need to pull in the - new [capistrano-sidekiq](https://github.com/seuros/capistrano-sidekiq) - gem and use it in your deploy.rb. -* API changes: - - `Sidekiq::Client.registered_workers` replaced by `Sidekiq::Workers.new` - - `Sidekiq::Client.registered_queues` replaced by `Sidekiq::Queue.all` - - `Sidekiq::Worker#retries_exhausted` replaced by `Sidekiq::Worker.sidekiq_retries_exhausted` - - `Sidekiq::Workers#each` has changed significantly with a reworking - of Sidekiq's internal process/thread data model. -* `sidekiq/api` is no longer automatically required. If your code uses - the API, you will need to require it. -* Redis-to-Go is no longer transparently activated on Heroku so as to not play - favorites with any particular Redis service. You need to set a config option - for your app: - `heroku config:set REDIS_PROVIDER=REDISTOGO_URL`. You may also use - the generic `REDIS_URL`. See - [Advanced Options: Setting the Location of your Redis server][1] - for details. -* Anyone using Airbrake, Honeybadger, Exceptional or ExceptionNotifier - will need to update their error gem version to the latest to pull in - Sidekiq support. Sidekiq will not provide explicit support for these - services so as to not play favorites with any particular error service. -* MRI 1.9 is no longer officially supported. Sidekiq's official - support policy is to support the current and previous major releases - of MRI and Rails. As of February 2014, that's MRI 2.1, MRI 2.0, JRuby 1.7, Rails 4.0 - and Rails 3.2. I will consider PRs to fix issues found by users for - other platforms/versions. - -## Error Service Providers - -If you previously provided a middleware to capture job errors, you -should instead provide a global error handler with Sidekiq 3.0. This -ensures **any** error within Sidekiq will be logged appropriately, not -just during job execution. - -```ruby -if Sidekiq::VERSION < '3' - # old behavior - Sidekiq.configure_server do |config| - config.server_middleware do |chain| - chain.add MyErrorService::Middleware - end - end -else - Sidekiq.configure_server do |config| - config.error_handlers << proc {|ex,context| MyErrorService.notify(ex, context) } - end -end -``` - -Your error handler must respond to `call(exception, context_hash)`. - -[1]: https://github.com/mperham/sidekiq/wiki/Advanced-Options#via-env-variable diff --git a/4.0-Upgrade.md b/4.0-Upgrade.md deleted file mode 100644 index b5c2e1bd..00000000 --- a/4.0-Upgrade.md +++ /dev/null @@ -1,53 +0,0 @@ -# Welcome to Sidekiq 4.0! - -Sidekiq 4.0 contains a redesigned, more efficient core with less overhead per job. -See my blog for [an overview of Sidekiq 4's higher performance](http://www.mikeperham.com/2015/10/14/optimizing-sidekiq/). - -## What's New - -* Sidekiq no longer uses Celluloid. If your application code uses Celluloid, - you will need to pull it in yourself. - -* `redis-namespace` has been removed from Sidekiq's gem dependencies. If - you want to use namespacing ([and I strongly urge you not to](http://www.mikeperham.com/2015/09/24/storing-data-with-redis/)), you'll need to add the gem to your Gemfile: -```ruby -gem 'redis-namespace' -``` - -* **Redis 2.8.0 or greater is required.** Redis 2.8 was released two years - ago and contains **many** useful features which Sidekiq couldn't - leverage until now. **Redis 3.0.3 or greater is recommended** for large - scale use [#2431](https://github.com/mperham/sidekiq/issues/2431). - -* Jobs are now fetched from Redis in parallel, making Sidekiq more - resilient to high network latency. This means that Sidekiq requires - more Redis connections per process. You must have a minimum of - `concurrency + 2` connections in your pool or Sidekiq will exit. - When in doubt, let Sidekiq size the connection pool for you. - -* Worker data is no longer updated in real-time but rather upon every - heartbeat. Don't expect the `Sidekiq::Workers` API to be millisecond-precise. - -* There's a new testing API based off the `Sidekiq::Queues` namespace. All - assertions made against the Worker class still work as expected. -```ruby -assert_equal 0, Sidekiq::Queues["default"].size -HardWorker.perform_async("log") -assert_equal 1, Sidekiq::Queues["default"].size -assert_equal "log", Sidekiq::Queues["default"].first['args'][0] -Sidekiq::Queues.clear_all -``` - -## Upgrade - -First, make sure you are using Redis 2.8 or greater. Next: - -* Upgrade to the latest Sidekiq 3.x. -```ruby -gem 'sidekiq', '< 4' -``` -* Fix any deprecation warnings you see. -* Upgrade to 4.x. -```ruby -gem 'sidekiq', '< 5' -``` diff --git a/5.0-Upgrade.md b/5.0-Upgrade.md deleted file mode 100644 index de782caa..00000000 --- a/5.0-Upgrade.md +++ /dev/null @@ -1,56 +0,0 @@ -# Welcome to Sidekiq 5.0! - -Sidekiq 5.0 contains a reworked job dispatch and execution core to integrate -better with the new Rails 5.0 Executor. It also drops support for older -versions of Ruby and Rails and adds support for RTL languages in the Web UI. - -## What's New - -* Integrate job logging and retry logic directly in with the job - execution logic in Sidekiq::Processor. Previously this logic was - defined as middleware. In Rails 5.0, ActiveSupport::Executor handles ActiveRecord - connection management, job callbacks, development mode class loading, - etc. Because of its extensive responsibilities, the Executor can't be - integrated as Sidekiq middleware; the logging/retry logic had to be pulled out - too. Sidekiq 4.2 had a hack to make it work but this redesign provides - a cleaner integration. [#3235] -* The Delayed Extensions `delay`, `delay_for` and `delay_until` APIs are - no longer available by default. The extensions allow you to marshal - job arguments as YAML, leading to cases where job payloads could be many - 100s of KB or larger if not careful, leading to Redis networking - timeouts or other problems. As noted in the Best Practices wiki page, - Sidekiq is designed for jobs with small, simple arguments. - - Add this line to your initializer to re-enable them and get the old behavior: - ```ruby - Sidekiq::Extensions.enable_delay! - ``` - The old `Sidekiq.remove_delay!` API has been removed as it is now the default. [#3299] -* Sidekiq's quiet signal is now `TSTP` (think of it as **T**hread - **ST**o**P**) instead of USR1 as USR1 is not available on JRuby. - USR1 will continue to be supported in Sidekiq 5.x for backwards - compatibility and will be removed in Sidekiq 6.x. [#3302] -* The Web UI is now bi-directional - it can render either LTR - (left-to-right) or RTL languages. With this change, **Farsi, Arabic, - Hebrew and Urdu** are officially supported. [#3381] -* Jobs which can't be parsed due to invalid JSON are now pushed - immediately to the Dead set since they require manual intervention and - will never execute successfully as is. The Web UI has been updated to - more gracefully display these jobs. [#3296] -* **Rails 3.2** is no longer supported. -* **Ruby 2.0 and Ruby 2.1** are no longer supported. Ruby 2.2.2+ is required. - -## Upgrade - -As always, please upgrade Sidekiq **one major version at a time**. -If you are already running Sidekiq 4.x, then: - -* Upgrade to the latest Sidekiq 4.x. -```ruby -gem 'sidekiq', '< 5' -``` -* Fix any deprecation warnings you see. -* Upgrade to 5.x. -```ruby -gem 'sidekiq', '< 6' -``` diff --git a/6.0-Upgrade.md b/6.0-Upgrade.md deleted file mode 100644 index e65d9e78..00000000 --- a/6.0-Upgrade.md +++ /dev/null @@ -1,72 +0,0 @@ -# Welcome to Sidekiq 6.0! - -Sidekiq 6.0 contains some breaking changes which streamline proper operation -of Sidekiq. It also drops support for EOL versions of Ruby and Rails. - -## What's New - -This release has major breaking changes. Read and test carefully in production. - -- ActiveJobs can now use `sidekiq_options` directly to configure Sidekiq - features/internals like the retry subsystem. Prefer the native - Sidekiq::Worker APIs as some Sidekiq features (e.g. unique jobs) do not work well with AJ. - (requires Rails 6.0.2) -```ruby -class MyJob < ActiveJob::Base - queue_as :myqueue - sidekiq_options retry: 10, backtrace: 20 - def perform(...) - end -end -``` -- Logging has been redesigned to allow pluggable formatters and several - formats ship with Sidekiq: - * default - your typical output on macOS - * heroku - enabled specifically when running in Heroku - * json - a JSON format for search indexing, one hash per line - -Sidekiq will enable the best formatter for the detected environment but -you can override it by configuring the log formatter explicitly. See -'sidekiq/logger' for implementation details. - -```ruby -Sidekiq.configure_server do |config| - config.log_formatter = AcmeCorp::PlainLogFormatter.new - # config.log_formatter = Sidekiq::Logger::Formatters::JSON.new -end -``` -Please see the [Logging](https://github.com/mperham/sidekiq/wiki/Logging) wiki page for the latest documentation and notes. -- **Remove the daemonization, logfile and pidfile command line arguments and `sidekiqctl` binary**. -I've [noted for years](https://www.mikeperham.com/2014/09/22/dont-daemonize-your-daemons/) -how modern services should be managed with a proper init system. -Managing services manually is more error-prone, let your operating system do it for you. -systemd, upstart, and foreman are three options. See the Deployment wiki page for the latest details. -- **Validate proper usage of the `REDIS_PROVIDER` variable.** -This variable is meant to hold the name of the environment -variable which contains your Redis URL, so that you can switch Redis -providers quickly and easily with a single variable change. It is not -meant to hold the actual Redis URL itself. If you want to manually set -the Redis URL then you may set `REDIS_URL` directly. [#3969] -- **Increase default shutdown timeout from 8 seconds to 25 seconds.** -Both Heroku and ECS now use 30 second shutdown timeout -by default and we want Sidekiq to take advantage of this time. If you -have deployment scripts which depend on the old default timeout, use `-t 8` to -get the old behavior. [#3968] -* **Rails <5** is no longer supported. Rails 6+ only works in zeitwerk mode. -* **Ruby <2.5** is no longer supported. -* **Redis <4** is no longer supported. - -## Upgrade - -As always, please upgrade Sidekiq **one major version at a time**. -If you are already running Sidekiq 5.x, then: - -* Upgrade to the latest Sidekiq 5.x. -```ruby -gem 'sidekiq', '< 6' -``` -* Fix any deprecation warnings you see. -* Upgrade to 6.x. -```ruby -gem 'sidekiq', '< 7' -``` diff --git a/Changes.md b/Changes.md index b20cf38f..9f9b1f8b 100644 --- a/Changes.md +++ b/Changes.md @@ -1,1864 +1,8 @@ -# Sidekiq Changes +# Sidekiq Delay Extensions Changes -[Sidekiq Changes](https://github.com/mperham/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/mperham/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/mperham/sidekiq/blob/main/Ent-Changes.md) +[Sidekiq Changes](https://github.com/gemhome/sidekiq-delay_extensions/blob/main/Changes.md) -HEAD +6.4.1 --------- -- Fix sidekiq.yml YAML load errors on Ruby 3.1 [#5141] -- Sharding support for `perform_bulk` [#5129] -- Refactor job logger for SPEEEEEEED - -6.4.0 ---------- - -- **SECURITY**: Validate input to avoid possible DoS in Web UI. -- Add **strict argument checking** [#5071] - Sidekiq will now log a warning if JSON-unsafe arguments are passed to `perform_async`. - Add `Sidekiq.strict_args!(false)` to your initializer to disable this warning. - This warning will switch to an exception in Sidekiq 7.0. -- Note that Delayed Extensions will be removed in Sidekiq 7.0 [#5076] -- Add `perform_{inline,sync}` in Sidekiq::Job to run a job synchronously [#5061, hasan-ally] -```ruby -SomeJob.perform_async(args...) -SomeJob.perform_sync(args...) -SomeJob.perform_inline(args...) -``` - You can also dynamically redirect a job to run synchronously: -```ruby -SomeJob.set("sync": true).perform_async(args...) # will run via perform_inline -``` -- Replace Sidekiq::Worker `app/workers` generator with Sidekiq::Job `app/sidekiq` generator [#5055] -``` -bin/rails generate sidekiq:job ProcessOrderJob -``` -- Fix job retries losing CurrentAttributes [#5090] -- Tweak shutdown to give long-running threads time to cleanup [#5095] - -6.3.1 ---------- - -- Fix keyword arguments error with CurrentAttributes on Ruby 3.0 [#5048] - -6.3.0 ---------- - -- **BREAK**: The Web UI has been refactored to remove jQuery. Any UI extensions - which use jQuery will break. -- **FEATURE**: Sidekiq.logger has been enhanced so any `Rails.logger` - output in jobs now shows up in the Sidekiq console. Remove any logger - hacks in your initializer and see if it Just Works™ now. [#5021] -- **FEATURE**: Add `Sidekiq::Job` alias for `Sidekiq::Worker`, to better - reflect industry standard terminology. You can now do this: -```ruby -class MyJob - include Sidekiq::Job - sidekiq_options ... - def perform(args) - end -end -``` -- **FEATURE**: Support for serializing ActiveSupport::CurrentAttributes into each job. [#4982] -```ruby -# config/initializers/sidekiq.rb -require "sidekiq/middleware/current_attributes" -Sidekiq::CurrentAttributes.persist(Myapp::Current) # Your AS::CurrentAttributes singleton -``` -- **FEATURE**: Add `Sidekiq::Worker.perform_bulk` for enqueuing jobs in bulk, - similar to `Sidekiq::Client.push_bulk` [#5042] -```ruby -MyJob.perform_bulk([[1], [2], [3]]) -``` -- Implement `queue_as`, `wait` and `wait_until` for ActiveJob compatibility [#5003] -- Scheduler now uses Lua to reduce Redis load and network roundtrips [#5044] -- Retry Redis operation if we get an `UNBLOCKED` Redis error [#4985] -- Run existing signal traps, if any, before running Sidekiq's trap [#4991] -- Fix fetch bug when using weighted queues which caused Sidekiq to stop - processing queues randomly [#5031] - -6.2.2 ---------- - -- Reduce retry jitter, add jitter to `sidekiq_retry_in` values [#4957] -- Minimize scheduler load on Redis at scale [#4882] -- Improve logging of delay jobs [#4904, BuonOno] -- Minor CSS improvements for buttons and tables, design PRs always welcome! -- Tweak Web UI `Cache-Control` header [#4966] -- Rename internal API class `Sidekiq::Job` to `Sidekiq::JobRecord` [#4955] - -6.2.1 ---------- - -- Update RTT warning logic to handle transient RTT spikes [#4851] -- Fix very low priority CVE on unescaped queue name [#4852] -- Add note about sessions and Rails apps in API mode - -6.2.0 ---------- - -- Store Redis RTT and log if poor [#4824] -- Add process/thread stats to Busy page [#4806] -- Improve Web UI on mobile devices [#4840] -- **Refactor Web UI session usage** [#4804] - Numerous people have hit "Forbidden" errors and struggled with Sidekiq's - Web UI session requirement. If you have code in your initializer for - Web sessions, it's quite possible it will need to be removed. Here's - an overview: -``` -Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app, -make sure you mount Sidekiq::Web *inside* your routes in `config/routes.rb` so -Sidekiq can reuse the Rails session: - - Rails.application.routes.draw do - mount Sidekiq::Web => "/sidekiq" - .... - end - -If this is a bare Rack app, use a session middleware before Sidekiq::Web: - - # first, use IRB to create a shared secret key for sessions and commit it - require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) } - - # now, update your Rack app to include the secret with a session cookie middleware - use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400 - run Sidekiq::Web - -If this is a Rails app in API mode, you need to enable sessions. - - https://guides.rubyonrails.org/api_app.html#using-session-middlewares -``` - -6.1.3 ---------- - -- Warn if Redis is configured to evict data under memory pressure [#4752] -- Add process RSS on the Busy page [#4717] - -6.1.2 ---------- - -- Improve readability in dark mode Web UI [#4674] -- Fix Web UI crash with corrupt session [#4672] -- Allow middleware to yield arguments [#4673, @eugeneius] -- Migrate CI from CircleCI to GitHub Actions [#4677] - -6.1.1 ---------- - -- Jobs are now sorted by age in the Busy Workers table. [#4641] -- Fix "check all" JS logic in Web UI [#4619] - -6.1.0 ---------- - -- Web UI - Dark Mode fixes [#4543, natematykiewicz] -- Ensure `Rack::ContentLength` is loaded as middleware for correct Web UI responses [#4541] -- Avoid exception dumping SSL store in Redis connection logging [#4532] -- Better error messages in Sidekiq::Client [#4549] -- Remove rack-protection, reimplement CSRF protection [#4588] -- Require redis-rb 4.2 [#4591] -- Update to jquery 1.12.4 [#4593] -- Refactor internal fetch logic and API [#4602] - -6.0.7 ---------- - -- Refactor systemd integration to work better with custom binaries [#4511] -- Don't connect to Redis at process exit if not needed [#4502] -- Remove Redis connection naming [#4479] -- Fix Redis Sentinel password redaction [#4499] -- Add Vietnamese locale (vi) [#4528] - -6.0.6 ---------- - -- **Integrate with systemd's watchdog and notification features** [#4488] - Set `Type=notify` in [sidekiq.service](https://github.com/mperham/sidekiq/blob/4b8a8bd3ae42f6e48ae1fdaf95ed7d7af18ed8bb/examples/systemd/sidekiq.service#L30-L39). The integration works automatically. -- Use `setTimeout` rather than `setInterval` to avoid thundering herd [#4480] -- Fix edge case where a job can be pushed without a queue. -- Flush job stats at exit [#4498] -- Check RAILS_ENV before RACK_ENV [#4493] -- Add Lithuanian locale [#4476] - -6.0.5 ---------- - -- Fix broken Web UI response when using NewRelic and Rack 2.1.2+. [#4440] -- Update APIs to use `UNLINK`, not `DEL`. [#4449] -- Fix Ruby 2.7 warnings [#4412] -- Add support for `APP_ENV` [[95fa5d9]](https://github.com/mperham/sidekiq/commit/95fa5d90192148026e52ca2902f1b83c70858ce8) - -6.0.4 ---------- - -- Fix ActiveJob's `sidekiq_options` integration [#4404] -- Sidekiq Pro users will now see a Pause button next to each queue in - the Web UI, allowing them to pause queues manually [#4374, shayonj] -- Fix Sidekiq::Workers API unintentional change in 6.0.2 [#4387] - - -6.0.3 ---------- - -- Fix `Sidekiq::Client.push_bulk` API which was erroneously putting - invalid `at` values in the job payloads [#4321] - -6.0.2 ---------- - -- Fix Sidekiq Enterprise's rolling restart functionality, broken by refactoring in 6.0.0. [#4334] -- More internal refactoring and performance tuning [fatkodima] - -6.0.1 ---------- - -- **Performance tuning**, Sidekiq should be 10-15% faster now [#4303, 4299, - 4269, fatkodima] -- **Dark Mode support in Web UI** (further design polish welcome!) [#4227, mperham, - fatkodima, silent-e] -- **Job-specific log levels**, allowing you to turn on debugging for - problematic workers. [fatkodima, #4287] -```ruby -MyWorker.set(log_level: :debug).perform_async(...) -``` -- **Ad-hoc job tags**. You can tag your jobs with, e.g, subdomain, tenant, country, - locale, application, version, user/client, "alpha/beta/pro/ent", types of jobs, - teams/people responsible for jobs, additional metadata, etc. - Tags are shown on different pages with job listings. Sidekiq Pro users - can filter based on them [fatkodima, #4280] -```ruby -class MyWorker - include Sidekiq::Worker - sidekiq_options tags: ['bank-ops', 'alpha'] - ... -end -``` -- Fetch scheduled jobs in batches before pushing into specific queues. - This will decrease enqueueing time of scheduled jobs by a third. [fatkodima, #4273] -``` -ScheduledSet with 10,000 jobs -Before: 56.6 seconds -After: 39.2 seconds -``` -- Compress error backtraces before pushing into Redis, if you are - storing error backtraces, this will halve the size of your RetrySet - in Redis [fatkodima, #4272] -``` -RetrySet with 100,000 jobs -Before: 261 MB -After: 129 MB -``` -- Support display of ActiveJob 6.0 payloads in the Web UI [#4263] -- Add `SortedSet#scan` for pattern based scanning. For large sets this API will be **MUCH** faster - than standard iteration using each. [fatkodima, #4262] -```ruby - Sidekiq::DeadSet.new.scan("UnreliableApi") do |job| - job.retry - end -``` -- Dramatically speed up SortedSet#find\_job(jid) by using Redis's ZSCAN - support, approx 10x faster. [fatkodima, #4259] -``` -zscan 0.179366 0.047727 0.227093 ( 1.161376) -enum 8.522311 0.419826 8.942137 ( 9.785079) -``` -- Respect rails' generators `test_framework` option and gracefully handle extra `worker` suffix on generator [fatkodima, #4256] -- Add ability to sort 'Enqueued' page on Web UI by position in the queue [fatkodima, #4248] -- Support `Client.push_bulk` with different delays [fatkodima, #4243] -```ruby -Sidekiq::Client.push_bulk("class" => FooJob, "args" => [[1], [2]], "at" => [1.minute.from_now.to_f, 5.minutes.from_now.to_f]) -``` -- Easier way to test enqueuing specific ActionMailer and ActiveRecord delayed jobs. Instead of manually - parsing embedded class, you can now test by fetching jobs for specific classes. [fatkodima, #4292] -```ruby -assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs_for(FooMailer).size -``` -- Add `sidekiqmon` to gemspec executables [#4242] -- Gracefully handle `Sidekiq.logger = nil` [#4240] -- Inject Sidekiq::LogContext module if user-supplied logger does not include it [#4239] - -6.0 ---------- - -This release has major breaking changes. Read and test carefully in production. - -- With Rails 6.0.2+, ActiveJobs can now use `sidekiq_options` directly to configure Sidekiq - features/internals like the retry subsystem. [#4213, pirj] -```ruby -class MyJob < ActiveJob::Base - queue_as :myqueue - sidekiq_options retry: 10, backtrace: 20 - def perform(...) - end -end -``` -- Logging has been redesigned to allow for pluggable log formatters: -```ruby -Sidekiq.configure_server do |config| - config.log_formatter = Sidekiq::Logger::Formatters::JSON.new -end -``` -See the [Logging wiki page](https://github.com/mperham/sidekiq/wiki/Logging) for more details. -- **BREAKING CHANGE** Validate proper usage of the `REDIS_PROVIDER` - variable. This variable is meant to hold the name of the environment - variable which contains your Redis URL, so that you can switch Redis - providers quickly and easily with a single variable change. It is not - meant to hold the actual Redis URL itself. If you want to manually set - the Redis URL (not recommended as it implies you have no failover), - then you may set `REDIS_URL` directly. [#3969] -- **BREAKING CHANGE** Increase default shutdown timeout from 8 seconds - to 25 seconds. Both Heroku and ECS now use 30 second shutdown timeout - by default and we want Sidekiq to take advantage of this time. If you - have deployment scripts which depend on the old default timeout, use `-t 8` to - get the old behavior. [#3968] -- **BREAKING CHANGE** Remove the daemonization, logfile and pidfile - arguments to Sidekiq. Use a proper process supervisor (e.g. systemd or - foreman) to manage Sidekiq. See the Deployment wiki page for links to - more resources. -- Integrate the StandardRB code formatter to ensure consistent code - styling. [#4114, gearnode] - -5.2.9 ---------- - -- Release Rack lock due to a cascade of CVEs. [#4566] - Pro-tip: don't lock Rack. - -5.2.8 ---------- - -- Lock to Rack 2.0.x to prevent future incompatibilities -- Fix invalid reference in `sidekiqctl` - -5.2.7 ---------- - -- Fix stale `enqueued_at` when retrying [#4149] -- Move build to [Circle CI](https://circleci.com/gh/mperham/sidekiq) [#4120] - -5.2.6 ---------- - -- Fix edge case where a job failure during Redis outage could result in a lost job [#4141] -- Better handling of malformed job arguments in payload [#4095] -- Restore bootstap's dropdown css component [#4099, urkle] -- Display human-friendly time diff for longer queue latencies [#4111, interlinked] -- Allow `Sidekiq::Worker#set` to be chained - -5.2.5 ---------- - -- Fix default usage of `config/sidekiq.yml` [#4077, Tensho] - -5.2.4 ---------- - -- Add warnings for various deprecations and changes coming in Sidekiq 6.0. - See the 6-0 branch. [#4056] -- Various improvements to the Sidekiq test suite and coverage [#4026, #4039, Tensho] - -5.2.3 ---------- - -- Warning message on invalid REDIS\_PROVIDER [#3970] -- Add `sidekiqctl status` command [#4003, dzunk] -- Update elapsed time calculatons to use monotonic clock [#3999] -- Fix a few issues with mobile Web UI styling [#3973, navied] -- Jobs with `retry: false` now go through the global `death_handlers`, - meaning you can take action on failed ephemeral jobs. [#3980, Benjamin-Dobell] -- Fix race condition in defining Workers. [#3997, mattbooks] - -5.2.2 ---------- - -- Raise error for duplicate queue names in config to avoid unexpected fetch algorithm change [#3911] -- Fix concurrency bug on JRuby [#3958, mattbooks] -- Add "Kill All" button to the retries page [#3938] - -5.2.1 ------------ - -- Fix concurrent modification error during heartbeat [#3921] - -5.2.0 ------------ - -- **Decrease default concurrency from 25 to 10** [#3892] -- Verify connection pool sizing upon startup [#3917] -- Smoother scheduling for large Sidekiq clusters [#3889] -- Switch Sidekiq::Testing impl from alias\_method to Module#prepend, for resiliency [#3852] -- Update Sidekiq APIs to use SCAN for scalability [#3848, ffiller] -- Remove concurrent-ruby gem dependency [#3830] -- Optimize Web UI's bootstrap.css [#3914] - -5.1.3 ------------ - -- Fix version comparison so Ruby 2.2.10 works. [#3808, nateberkopec] - -5.1.2 ------------ - -- Add link to docs in Web UI footer -- Fix crash on Ctrl-C in Windows [#3775, Bernica] -- Remove `freeze` calls on String constants. This is superfluous with Ruby - 2.3+ and `frozen_string_literal: true`. [#3759] -- Fix use of AR middleware outside of Rails [#3787] -- Sidekiq::Worker `sidekiq_retry_in` block can now return nil or 0 to use - the default backoff delay [#3796, dsalahutdinov] - -5.1.1 ------------ - -- Fix Web UI incompatibility with Redis 3.x gem [#3749] - -5.1.0 ------------ - -- **NEW** Global death handlers - called when your job exhausts all - retries and dies. Now you can take action when a job fails permanently. [#3721] -- **NEW** Enable ActiveRecord query cache within jobs by default [#3718, sobrinho] - This will prevent duplicate SELECTS; cache is cleared upon any UPDATE/INSERT/DELETE. - See the issue for how to bypass the cache or disable it completely. -- Scheduler timing is now more accurate, 15 -> 5 seconds [#3734] -- Exceptions during the :startup event will now kill the process [#3717] -- Make `Sidekiq::Client.via` reentrant [#3715] -- Fix use of Sidekiq logger outside of the server process [#3714] -- Tweak `constantize` to better match Rails class lookup. [#3701, caffeinated-tech] - -5.0.5 ------------ - -- Update gemspec to allow newer versions of the Redis gem [#3617] -- Refactor Worker.set so it can be memoized [#3602] -- Fix display of Redis URL in web footer, broken in 5.0.3 [#3560] -- Update `Sidekiq::Job#display_args` to avoid mutation [#3621] - -5.0.4 ------------ - -- Fix "slow startup" performance regression from 5.0.2. [#3525] -- Allow users to disable ID generation since some redis providers disable the CLIENT command. [#3521] - -5.0.3 ------------ - -- Fix overriding `class_attribute` core extension from ActiveSupport with Sidekiq one [PikachuEXE, #3499] -- Allow job logger to be overridden [AlfonsoUceda, #3502] -- Set a default Redis client identifier for debugging [#3516] -- Fix "Uninitialized constant" errors on startup with the delayed extensions [#3509] - -5.0.2 ------------ - -- fix broken release, thanks @nateberkopec - -5.0.1 ------------ - -- Fix incorrect server identity when daemonizing [jwilm, #3496] -- Work around error running Web UI against Redis Cluster [#3492] -- Remove core extensions, Sidekiq is now monkeypatch-free! [#3474] -- Reimplement Web UI's HTTP\_ACCEPT\_LANGUAGE parsing because the spec is utterly - incomprehensible for various edge cases. [johanlunds, natematykiewicz, #3449] -- Update `class_attribute` core extension to avoid warnings -- Expose `job_hash_context` from `Sidekiq::Logging` to support log customization - -5.0.0 ------------ - -- **BREAKING CHANGE** Job dispatch was refactored for safer integration with - Rails 5. The **Logging** and **RetryJobs** server middleware were removed and - functionality integrated directly into Sidekiq::Processor. These aren't - commonly used public APIs so this shouldn't impact most users. -``` -Sidekiq::Middleware::Server::RetryJobs -> Sidekiq::JobRetry -Sidekiq::Middleware::Server::Logging -> Sidekiq::JobLogger -``` -- Quieting Sidekiq is now done via the TSTP signal, the USR1 signal is deprecated. -- The `delay` extension APIs are no longer available by default, you - must opt into them. -- The Web UI is now BiDi and can render RTL languages like Arabic, Farsi and Hebrew. -- Rails 3.2 and Ruby 2.0 and 2.1 are no longer supported. -- The `SomeWorker.set(options)` API was re-written to avoid thread-local state. [#2152] -- Sidekiq Enterprise's encrypted jobs now display "[encrypted data]" in the Web UI instead - of random hex bytes. -- Please see the [5.0 Upgrade notes](5.0-Upgrade.md) for more detail. - -4.2.10 ------------ - -- Scheduled jobs can now be moved directly to the Dead queue via API [#3390] -- Fix edge case leading to job duplication when using Sidekiq Pro's - reliability feature [#3388] -- Fix error class name display on retry page [#3348] -- More robust latency calculation [#3340] - -4.2.9 ------------ - -- Rollback [#3303] which broke Heroku Redis users [#3311] -- Add support for TSTP signal, for Sidekiq 5.0 forward compatibility. [#3302] - -4.2.8 ------------ - -- Fix rare edge case with Redis driver that can create duplicate jobs [#3303] -- Fix Rails 5 loading issue [#3275] -- Restore missing tooltips to timestamps in Web UI [#3310] -- Work on **Sidekiq 5.0** is now active! [#3301] - -4.2.7 ------------ - -- Add new integration testing to verify code loading and job execution - in development and production modes with Rails 4 and 5 [#3241] -- Fix delayed extensions in development mode [#3227, DarthSim] -- Use Worker's `retry` default if job payload does not have a retry - attribute [#3234, mlarraz] - -4.2.6 ------------ - -- Run Rails Executor when in production [#3221, eugeneius] - -4.2.5 ------------ - -- Re-enable eager loading of all code when running non-development Rails 5. [#3203] -- Better root URL handling for zany web servers [#3207] - -4.2.4 ------------ - -- Log errors coming from the Rails 5 reloader. [#3212, eugeneius] -- Clone job data so middleware changes don't appear in Busy tab - -4.2.3 ------------ - -- Disable use of Rails 5's Reloader API in non-development modes, it has proven - to be unstable under load [#3154] -- Allow disabling of Sidekiq::Web's cookie session to handle the - case where the app provides a session already [#3180, inkstak] -```ruby -Sidekiq::Web.set :sessions, false -``` -- Fix Web UI sharding support broken in 4.2.2. [#3169] -- Fix timestamps not updating during UI polling [#3193, shaneog] -- Relax rack-protection version to >= 1.5.0 -- Provide consistent interface to exception handlers, changing the structure of the context hash. [#3161] - -4.2.2 ------------ - -- Fix ever-increasing cookie size with nginx [#3146, cconstantine] -- Fix so Web UI works without trailing slash [#3158, timdorr] - -4.2.1 ------------ - -- Ensure browser does not cache JSON/AJAX responses. [#3136] -- Support old Sinatra syntax for setting config [#3139] - -4.2.0 ------------ - -- Enable development-mode code reloading. **With Rails 5.0+, you don't need - to restart Sidekiq to pick up your Sidekiq::Worker changes anymore!** [#2457] -- **Remove Sinatra dependency**. Sidekiq's Web UI now uses Rack directly. - Thank you to Sidekiq's newest committer, **badosu**, for writing the code - and doing a lot of testing to ensure compatibility with many different - 3rd party plugins. If your Web UI works with 4.1.4 but fails with - 4.2.0, please open an issue. [#3075] -- Allow tuning of concurrency with the `RAILS_MAX_THREADS` env var. [#2985] - This is the same var used by Puma so you can tune all of your systems - the same way: -```sh -web: RAILS_MAX_THREADS=5 bundle exec puma ... -worker: RAILS_MAX_THREADS=10 bundle exec sidekiq ... -``` -Using `-c` or `config/sidekiq.yml` overrides this setting. I recommend -adjusting your `config/database.yml` to use it too so connections are -auto-scaled: -```yaml - pool: <%= ENV['RAILS_MAX_THREADS'] || 5 %> -``` - -4.1.4 ------------ - -- Unlock Sinatra so a Rails 5.0 compatible version may be used [#3048] -- Fix race condition on startup with JRuby [#3043] - - -4.1.3 ------------ - -- Please note the Redis 3.3.0 gem has a [memory leak](https://github.com/redis/redis-rb/issues/612), - Redis 3.2.2 is recommended until that issue is fixed. -- Sinatra 1.4.x is now a required dependency, avoiding cryptic errors - and old bugs due to people not upgrading Sinatra for years. [#3042] -- Fixed race condition in heartbeat which could rarely lead to lingering - processes on the Busy tab. [#2982] -```ruby -# To clean up lingering processes, modify this as necessary to connect to your Redis. -# After 60 seconds, lingering processes should disappear from the Busy page. - -require 'redis' -r = Redis.new(url: "redis://localhost:6379/0") -# uncomment if you need a namespace -#require 'redis-namespace' -#r = Redis::Namespace.new("foo", r) -r.smembers("processes").each do |pro| - r.expire(pro, 60) - r.expire("#{pro}:workers", 60) -end -``` - - -4.1.2 ------------ - -- Fix Redis data leak with worker data when a busy Sidekiq process - crashes. You can find and expire leaked data in Redis with this -script: -```bash -$ redis-cli keys "*:workers" | while read LINE ; do TTL=`redis-cli expire "$LINE" 60`; echo "$LINE"; done; -``` - Please note that `keys` can be dangerous to run on a large, busy Redis. Caveat runner. -- Freeze all string literals with Ruby 2.3. [#2741] -- Client middleware can now stop bulk job push. [#2887] - -4.1.1 ------------ - -- Much better behavior when Redis disappears and comes back. [#2866] -- Update FR locale [dbachet] -- Don't fill logfile in case of Redis downtime [#2860] -- Allow definition of a global retries_exhausted handler. [#2807] -```ruby -Sidekiq.configure_server do |config| - config.default_retries_exhausted = -> (job, ex) do - Sidekiq.logger.info "#{job['class']} job is now dead" - end -end -``` - -4.1.0 ------------ - -- Tag quiet processes in the Web UI [#2757, jcarlson] -- Pass last exception to sidekiq\_retries\_exhausted block [#2787, Nowaker] -```ruby -class MyWorker - include Sidekiq::Worker - sidekiq_retries_exhausted do |job, exception| - end -end -``` -- Add native support for ActiveJob's `set(options)` method allowing -you to override worker options dynamically. This should make it -even easier to switch between ActiveJob and Sidekiq's native APIs [#2780] -```ruby -class MyWorker - include Sidekiq::Worker - sidekiq_options queue: 'default', retry: true - - def perform(*args) - # do something - end -end - -MyWorker.set(queue: 'high', retry: false).perform_async(1) -``` - -4.0.2 ------------ - -- Better Japanese translations -- Remove `json` gem dependency from gemspec. [#2743] -- There's a new testing API based off the `Sidekiq::Queues` namespace. All - assertions made against the Worker class still work as expected. - [#2676, brandonhilkert] -```ruby -assert_equal 0, Sidekiq::Queues["default"].size -HardWorker.perform_async("log") -assert_equal 1, Sidekiq::Queues["default"].size -assert_equal "log", Sidekiq::Queues["default"].first['args'][0] -Sidekiq::Queues.clear_all -``` - -4.0.1 ------------ - -- Yank new queue-based testing API [#2663] -- Fix invalid constant reference in heartbeat - -4.0.0 ------------ - -- Sidekiq's internals have been completely overhauled for performance - and to remove dependencies. This has resulted in major speedups, as - [detailed on my blog](http://www.mikeperham.com/2015/10/14/optimizing-sidekiq/). -- See the [4.0 upgrade notes](4.0-Upgrade.md) for more detail. - -3.5.4 ------------ - -- Ensure exception message is a string [#2707] -- Revert racy Process.kill usage in sidekiqctl - -3.5.3 ------------ - -- Adjust shutdown event to run in parallel with the rest of system shutdown. [#2635] - -3.5.2 ------------ - -- **Sidekiq 3 is now in maintenance mode**, only major bugs will be fixed. -- The exception triggering a retry is now passed into `sidekiq_retry_in`, - allowing you to retry more frequently for certain types of errors. - [#2619, kreynolds] -```ruby - sidekiq_retry_in do |count, ex| - case ex - when RuntimeError - 5 * count - else - 10 * count - end - end -``` - -3.5.1 ------------ - -- **FIX MEMORY LEAK** Under rare conditions, threads may leak [#2598, gazay] -- Add Ukrainian locale [#2561, elrakita] -- Disconnect and retry Redis operations if we see a READONLY error [#2550] -- Add server middleware testing harness; see [wiki](https://github.com/mperham/sidekiq/wiki/Testing#testing-server-middleware) [#2534, ryansch] - -3.5.0 ------------ - -- Polished new banner! [#2522, firedev] -- Upgrade to Celluloid 0.17. [#2420, digitalextremist] -- Activate sessions in Sinatra for CSRF protection, requires Rails - monkeypatch due to rails/rails#15843. [#2460, jc00ke] - -3.4.2 ------------ - -- Don't allow `Sidekiq::Worker` in ActiveJob::Base classes. [#2424] -- Safer display of job data in Web UI [#2405] -- Fix CSRF vulnerability in Web UI, thanks to Egor Homakov for - reporting. [#2422] If you are running the Web UI as a standalone Rack app, - ensure you have a [session middleware -configured](https://github.com/mperham/sidekiq/wiki/Monitoring#standalone): -```ruby -use Rack::Session::Cookie, :secret => "some unique secret string here" -``` - -3.4.1 ------------ - -- Lock to Celluloid 0.16 - - -3.4.0 ------------ - -- Set a `created_at` attribute when jobs are created, set `enqueued_at` only - when they go into a queue. Fixes invalid latency calculations with scheduled jobs. - [#2373, mrsimo] -- Don't log timestamp on Heroku [#2343] -- Run `shutdown` event handlers in reverse order of definition [#2374] -- Rename and rework `poll_interval` to be simpler, more predictable [#2317, cainlevy] - The new setting is `average_scheduled_poll_interval`. To configure - Sidekiq to look for scheduled jobs every 5 seconds, just set it to 5. -```ruby -Sidekiq.configure_server do |config| - config.average_scheduled_poll_interval = 5 -end -``` - -3.3.4 ------------ - -- **Improved ActiveJob integration** - Web UI now shows ActiveJobs in a - nicer format and job logging shows the actual class name, requires - Rails 4.2.2+ [#2248, #2259] -- Add Sidekiq::Process#dump\_threads API to trigger TTIN output [#2247] -- Web UI polling now uses Ajax to avoid page reload [#2266, davydovanton] -- Several Web UI styling improvements [davydovanton] -- Add Tamil, Hindi translations for Web UI [ferdinandrosario, tejasbubane] -- Fix Web UI to work with country-specific locales [#2243] -- Handle circular error causes [#2285, eugenk] - -3.3.3 ------------ - -- Fix crash on exit when Redis is down [#2235] -- Fix duplicate logging on startup -- Undeprecate delay extension for ActionMailer 4.2+ . [#2186] - -3.3.2 ------------ - -- Add Sidekiq::Stats#queues back -- Allows configuration of dead job set size and timeout [#2173, jonhyman] -- Refactor scheduler enqueuing so Sidekiq Pro can override it. [#2159] - -3.3.1 ------------ - -- Dumb down ActionMailer integration so it tries to deliver if possible [#2149] -- Stringify Sidekiq.default\_worker\_options's keys [#2126] -- Add random integer to process identity [#2113, michaeldiscala] -- Log Sidekiq Pro's Batch ID if available [#2076] -- Refactor Processor Redis usage to avoid redis/redis-rb#490 [#2094] -- Move /dashboard/stats to /stats. Add /stats/queues. [moserke, #2099] -- Add processes count to /stats [ismaelga, #2141] -- Greatly improve speed of Sidekiq::Stats [ismaelga, #2142] -- Add better usage text for `sidekiqctl`. -- `Sidekiq::Logging.with_context` is now a stack so you can set your - own job context for logging purposes [grosser, #2110] -- Remove usage of Google Fonts in Web UI so it loads in China [#2144] - -3.3.0 ------------ - -- Upgrade to Celluloid 0.16 [#2056] -- Fix typo for generator test file name [dlackty, #2016] -- Add Sidekiq::Middleware::Chain#prepend [seuros, #2029] - -3.2.6 ------------ - -- Deprecate delay extension for ActionMailer 4.2+ . [seuros, #1933] -- Poll interval tuning now accounts for dead processes [epchris, #1984] -- Add non-production environment to Web UI page titles [JacobEvelyn, #2004] - -3.2.5 ------------ - -- Lock Celluloid to 0.15.2 due to bugs in 0.16.0. This prevents the - "hang on shutdown" problem with Celluloid 0.16.0. - -3.2.4 ------------ - -- Fix issue preventing ActionMailer sends working in some cases with - Rails 4. [pbhogan, #1923] - -3.2.3 ------------ - -- Clean invalid bytes from error message before converting to JSON (requires Ruby 2.1+) [#1705] -- Add queues list for each process to the Busy page. [davetoxa, #1897] -- Fix for crash caused by empty config file. [jordan0day, #1901] -- Add Rails Worker generator, `rails g sidekiq:worker User` will create `app/workers/user_worker.rb`. [seuros, #1909] -- Fix Web UI rendering with huge job arguments [jhass, #1918] -- Minor refactoring of Sidekiq::Client internals, for Sidekiq Pro. [#1919] - -3.2.2 ------------ - -- **This version of Sidekiq will no longer start on Ruby 1.9.** Sidekiq - 3 does not support MRI 1.9 but we've allowed it to run before now. -- Fix issue which could cause Sidekiq workers to disappear from the Busy - tab while still being active [#1884] -- Add "Back to App" button in Web UI. You can set the button link via - `Sidekiq::Web.app_url = 'http://www.mysite.com'` [#1875, seuros] -- Add process tag (`-g tag`) to the Busy page so you can differentiate processes at a glance. [seuros, #1878] -- Add "Kill" button to move retries directly to the DJQ so they don't retry. [seuros, #1867] - -3.2.1 ------------ - -- Revert eager loading change for Rails 3.x apps, as it broke a few edge - cases. - -3.2.0 ------------ - -- **Fix issue which caused duplicate job execution in Rails 3.x** - This issue is caused by [improper exception handling in ActiveRecord](https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L281) which changes Sidekiq's Shutdown exception into a database - error, making Sidekiq think the job needs to be retried. **The fix requires Ruby 2.1**. [#1805] -- Update how Sidekiq eager loads Rails application code [#1791, jonleighton] -- Change logging timestamp to show milliseconds. -- Reverse sorting of Dead tab so newer jobs are listed first [#1802] - -3.1.4 ------------ - -- Happy π release! -- Self-tuning Scheduler polling, we use heartbeat info to better tune poll\_interval [#1630] -- Remove all table column width rules, hopefully get better column formatting [#1747] -- Handle edge case where YAML can't be decoded in dev mode [#1761] -- Fix lingering jobs in Busy page on Heroku [#1764] - -3.1.3 ------------ - -- Use ENV['DYNO'] on Heroku for hostname display, rather than an ugly UUID. [#1742] -- Show per-process labels on the Busy page, for feature tagging [#1673] - - -3.1.2 ------------ - -- Suitably chastised, @mperham reverts the Bundler change. - - -3.1.1 ------------ - -- Sidekiq::CLI now runs `Bundler.require(:default, environment)` to boot all gems - before loading any app code. -- Sort queues by name in Web UI [#1734] - - -3.1.0 ------------ - -- New **remote control** feature: you can remotely trigger Sidekiq to quiet - or terminate via API, without signals. This is most useful on JRuby - or Heroku which does not support the USR1 'quiet' signal. Now you can - run a rake task like this at the start of your deploy to quiet your - set of Sidekiq processes. [#1703] -```ruby -namespace :sidekiq do - task :quiet => :environment do - Sidekiq::ProcessSet.new.each(&:quiet!) - end -end -``` -- The Web UI can use the API to quiet or stop all processes via the Busy page. -- The Web UI understands and hides the `Sidekiq::Extensions::Delay*` - classes, instead showing `Class.method` as the Job. [#1718] -- Polish the Dashboard graphs a bit, update Rickshaw [brandonhilkert, #1725] -- The poll interval is now configurable in the Web UI [madebydna, #1713] -- Delay extensions can be removed so they don't conflict with - DelayedJob: put `Sidekiq.remove_delay!` in your initializer. [devaroop, #1674] - - -3.0.2 ------------ - -- Revert gemfile requirement of Ruby 2.0. JRuby 1.7 calls itself Ruby - 1.9.3 and broke with this requirement. - -3.0.1 ------------ - -- Revert pidfile behavior from 2.17.5: Sidekiq will no longer remove its own pidfile - as this is a race condition when restarting. [#1470, #1677] -- Show warning on the Queues page if a queue is paused [#1672] -- Only activate the ActiveRecord middleware if ActiveRecord::Base is defined on boot. [#1666] -- Add ability to disable jobs going to the DJQ with the `dead` option. -```ruby -sidekiq_options :dead => false, :retry => 5 -``` -- Minor fixes - - -3.0.0 ------------ - -Please see [3.0-Upgrade.md](3.0-Upgrade.md) for more comprehensive upgrade notes. - -- **Dead Job Queue** - jobs which run out of retries are now moved to a dead - job queue. These jobs must be retried manually or they will expire - after 6 months or 10,000 jobs. The Web UI contains a "Dead" tab - exposing these jobs. Use `sidekiq_options :retry => false` if you -don't wish jobs to be retried or put in the DJQ. Use -`sidekiq_options :retry => 0` if you don't want jobs to retry but go -straight to the DJQ. -- **Process Lifecycle Events** - you can now register blocks to run at - certain points during the Sidekiq process lifecycle: startup, quiet and - shutdown. -```ruby -Sidekiq.configure_server do |config| - config.on(:startup) do - # do something - end -end -``` -- **Global Error Handlers** - blocks of code which handle errors that - occur anywhere within Sidekiq, not just within middleware. -```ruby -Sidekiq.configure_server do |config| - config.error_handlers << proc {|ex,ctx| ... } -end -``` -- **Process Heartbeat** - each Sidekiq process will ping Redis every 5 - seconds to give a summary of the Sidekiq population at work. -- The Workers tab is now renamed to Busy and contains a list of live - Sidekiq processes and jobs in progress based on the heartbeat. -- **Shardable Client** - Sidekiq::Client instances can use a custom - Redis connection pool, allowing very large Sidekiq installations to scale by - sharding: sending different jobs to different Redis instances. -```ruby -client = Sidekiq::Client.new(ConnectionPool.new { Redis.new }) -client.push(...) -``` -```ruby -Sidekiq::Client.via(ConnectionPool.new { Redis.new }) do - FooWorker.perform_async - BarWorker.perform_async -end -``` - **Sharding support does require a breaking change to client-side -middleware, see 3.0-Upgrade.md.** -- New Chinese, Greek, Swedish and Czech translations for the Web UI. -- Updated most languages translations for the new UI features. -- **Remove official Capistrano integration** - this integration has been - moved into the [capistrano-sidekiq](https://github.com/seuros/capistrano-sidekiq) gem. -- **Remove official support for MRI 1.9** - Things still might work but - I no longer actively test on it. -- **Remove built-in support for Redis-to-Go**. - Heroku users: `heroku config:set REDIS_PROVIDER=REDISTOGO_URL` -- **Remove built-in error integration for Airbrake, Honeybadger, ExceptionNotifier and Exceptional**. - Each error gem should provide its own Sidekiq integration. Update your error gem to the latest - version to pick up Sidekiq support. -- Upgrade to connection\_pool 2.0 which now creates connections lazily. -- Remove deprecated Sidekiq::Client.registered\_\* APIs -- Remove deprecated support for the old Sidekiq::Worker#retries\_exhausted method. -- Removed 'sidekiq/yaml\_patch', this was never documented or recommended. -- Removed --profile option, #1592 -- Remove usage of the term 'Worker' in the UI for clarity. Users would call both threads and - processes 'workers'. Instead, use "Thread", "Process" or "Job". - -2.17.7 ------------ - -- Auto-prune jobs older than one hour from the Workers page [#1508] -- Add Sidekiq::Workers#prune which can perform the auto-pruning. -- Fix issue where a job could be lost when an exception occurs updating - Redis stats before the job executes [#1511] - -2.17.6 ------------ - -- Fix capistrano integration due to missing pidfile. [#1490] - -2.17.5 ------------ - -- Automatically use the config file found at `config/sidekiq.yml`, if not passed `-C`. [#1481] -- Store 'retried\_at' and 'failed\_at' timestamps as Floats, not Strings. [#1473] -- A `USR2` signal will now reopen _all_ logs, using IO#reopen. Thus, instead of creating a new Logger object, - Sidekiq will now just update the existing Logger's file descriptor [#1163]. -- Remove pidfile when shutting down if started with `-P` [#1470] - -2.17.4 ------------ - -- Fix JID support in inline testing, #1454 -- Polish worker arguments display in UI, #1453 -- Marshal arguments fully to avoid worker mutation, #1452 -- Support reverse paging sorted sets, #1098 - - -2.17.3 ------------ - -- Synchronously terminates the poller and fetcher to fix a race condition in bulk requeue during shutdown [#1406] - -2.17.2 ------------ - -- Fix bug where strictly prioritized queues might be processed out of - order [#1408]. A side effect of this change is that it breaks a queue - declaration syntax that worked, although only because of a bug—it was - never intended to work and never supported. If you were declaring your - queues as a comma-separated list, e.g. `sidekiq -q critical,default,low`, - you must now use the `-q` flag before each queue, e.g. - `sidekiq -q critical -q default -q low`. - -2.17.1 ------------ - -- Expose `delay` extension as `sidekiq_delay` also. This allows you to - run Delayed::Job and Sidekiq in the same process, selectively porting - `delay` calls to `sidekiq_delay`. You just need to ensure that - Sidekiq is required **before** Delayed::Job in your Gemfile. [#1393] -- Bump redis client required version to 3.0.6 -- Minor CSS fixes for Web UI - -2.17.0 ------------ - -- Change `Sidekiq::Client#push_bulk` to return an array of pushed `jid`s. [#1315, barelyknown] -- Web UI refactoring to use more API internally (yummy dogfood!) -- Much faster Sidekiq::Job#delete performance for larger queue sizes -- Further capistrano 3 fixes -- Many misc minor fixes - -2.16.1 ------------ - -- Revert usage of `resolv-replace`. MRI's native DNS lookup releases the GIL. -- Fix several Capistrano 3 issues -- Escaping dynamic data like job args and error messages in Sidekiq Web UI. [#1299, lian] - -2.16.0 ------------ - -- Deprecate `Sidekiq::Client.registered_workers` and `Sidekiq::Client.registered_queues` -- Refactor Sidekiq::Client to be instance-based [#1279] -- Pass all Redis options to the Redis driver so Unix sockets - can be fully configured. [#1270, salimane] -- Allow sidekiq-web extensions to add locale paths so extensions - can be localized. [#1261, ondrejbartas] -- Capistrano 3 support [#1254, phallstrom] -- Use Ruby's `resolv-replace` to enable pure Ruby DNS lookups. - This ensures that any DNS resolution that takes place in worker - threads won't lock up the entire VM on MRI. [#1258] - -2.15.2 ------------ - -- Iterating over Sidekiq::Queue and Sidekiq::SortedSet will now work as - intended when jobs are deleted [#866, aackerman] -- A few more minor Web UI fixes [#1247] - -2.15.1 ------------ - -- Fix several Web UI issues with the Bootstrap 3 upgrade. - -2.15.0 ------------ - -- The Core Sidekiq actors are now monitored. If any crash, the - Sidekiq process logs the error and exits immediately. This is to - help prevent "stuck" Sidekiq processes which are running but don't - appear to be doing any work. [#1194] -- Sidekiq's testing behavior is now dynamic. You can choose between - `inline` and `fake` behavior in your tests. See -[Testing](https://github.com/mperham/sidekiq/wiki/Testing) for detail. [#1193] -- The Retries table has a new column for the error message. -- The Web UI topbar now contains the status and live poll button. -- Orphaned worker records are now auto-vacuumed when you visit the - Workers page in the Web UI. -- Sidekiq.default\_worker\_options allows you to configure default - options for all Sidekiq worker types. - -```ruby -Sidekiq.default_worker_options = { 'queue' => 'default', 'backtrace' => true } -``` -- Added two Sidekiq::Client class methods for compatibility with resque-scheduler: - `enqueue_to_in` and `enqueue_in` [#1212] -- Upgrade Web UI to Bootstrap 3.0. [#1211, jeffboek] - -2.14.1 ------------ - -- Fix misc Web UI issues due to ERB conversion. -- Bump redis-namespace version due to security issue. - -2.14.0 ------------ - -- Removed slim gem dependency, Web UI now uses ERB [Locke23rus, #1120] -- Fix more race conditions in Web UI actions -- Don't reset Job enqueued\_at when retrying -- Timestamp tooltips in the Web UI should use UTC -- Fix invalid usage of handle\_exception causing issues in Airbrake - [#1134] - - -2.13.1 ------------ - -- Make Sidekiq::Middleware::Chain Enumerable -- Make summary bar and graphs responsive [manishval, #1025] -- Adds a job status page for scheduled jobs [jonhyman] -- Handle race condition in retrying and deleting jobs in the Web UI -- The Web UI relative times are now i18n. [MadRabbit, #1088] -- Allow for default number of retry attempts to be set for - `Sidekiq::Middleware::Server::RetryJobs` middleware. [czarneckid] [#1091] - -```ruby -Sidekiq.configure_server do |config| - config.server_middleware do |chain| - chain.add Sidekiq::Middleware::Server::RetryJobs, :max_retries => 10 - end -end -``` - - -2.13.0 ------------ - -- Adding button to move scheduled job to main queue [guiceolin, #1020] -- fix i18n support resetting saved locale when job is retried [#1011] -- log rotation via USR2 now closes the old logger [#1008] -- Add ability to customize retry schedule, like so [jmazzi, #1027] - -```ruby -class MyWorker - include Sidekiq::Worker - sidekiq_retry_in { |count| count * 2 } -end -``` -- Redesign Worker#retries\_exhausted callback to use same form as above [jmazzi, #1030] - -```ruby -class MyWorker - include Sidekiq::Worker - sidekiq_retries_exhausted do |msg| - Rails.logger.error "Failed to process #{msg['class']} with args: #{msg['args']}" - end -end -``` - -2.12.4 ------------ - -- Fix error in previous release which crashed the Manager when a - Processor died. - -2.12.3 ------------ - -- Revert back to Celluloid's TaskFiber for job processing which has proven to be more - stable than TaskThread. [#985] -- Avoid possible lockup during hard shutdown [#997] - -At this point, if you are experiencing stability issues with Sidekiq in -Ruby 1.9, please try Ruby 2.0. It seems to be more stable. - -2.12.2 ------------ - -- Relax slim version requirement to >= 1.1.0 -- Refactor historical stats to use TTL, not explicit cleanup. [grosser, #971] - -2.12.1 ------------ - -- Force Celluloid 0.14.1 as 0.14.0 has a serious bug. [#954] -- Scheduled and Retry jobs now use Sidekiq::Client to push - jobs onto the queue, so they use client middleware. [dimko, #948] -- Record the timestamp when jobs are enqueued. Add - Sidekiq::Job#enqueued\_at to query the time. [mariovisic, #944] -- Add Sidekiq::Queue#latency - calculates diff between now and - enqueued\_at for the oldest job in the queue. -- Add testing method `perform_one` that dequeues and performs a single job. - This is mainly to aid testing jobs that spawn other jobs. [fumin, #963] - -2.12.0 ------------ - -- Upgrade to Celluloid 0.14, remove the use of Celluloid's thread - pool. This should halve the number of threads in each Sidekiq - process, thus requiring less resources. [#919] -- Abstract Celluloid usage to Sidekiq::Actor for testing purposes. -- Better handling for Redis downtime when fetching jobs and shutting - down, don't print exceptions every second and print success message - when Redis is back. -- Fix unclean shutdown leading to duplicate jobs [#897] -- Add Korean locale [#890] -- Upgrade test suite to Minitest 5 -- Remove usage of `multi_json` as `json` is now robust on all platforms. - -2.11.2 ------------ - -- Fix Web UI when used without Rails [#886] -- Add Sidekiq::Stats#reset [#349] -- Add Norwegian locale. -- Updates for the JA locale. - -2.11.1 ------------ - -- Fix timeout warning. -- Add Dutch web UI locale. - -2.11.0 ------------ - -- Upgrade to Celluloid 0.13. [#834] -- Remove **timeout** support from `sidekiq_options`. Ruby's timeout - is inherently unsafe in a multi-threaded application and was causing - stability problems for many. See http://bit.ly/OtYpK -- Add Japanese locale for Web UI [#868] -- Fix a few issues with Web UI i18n. - -2.10.1 ------------ - -- Remove need for the i18n gem. (brandonhilkert) -- Improve redis connection info logging on startup for debugging -purposes [#858] -- Revert sinatra/slim as runtime dependencies -- Add `find_job` method to sidekiq/api - - -2.10.0 ------------ - -- Refactor algorithm for putting scheduled jobs onto the queue [#843] -- Fix scheduler thread dying due to incorrect error handling [#839] -- Fix issue which left stale workers if Sidekiq wasn't shutdown while -quiet. [#840] -- I18n for web UI. Please submit translations of `web/locales/en.yml` for -your own language. [#811] -- 'sinatra', 'slim' and 'i18n' are now gem dependencies for Sidekiq. - - -2.9.0 ------------ - -- Update 'sidekiq/testing' to work with any Sidekiq::Client call. It - also serializes the arguments as using Redis would. [#713] -- Raise a Sidekiq::Shutdown error within workers which don't finish within the hard - timeout. This is to prevent unwanted database transaction commits. [#377] -- Lazy load Redis connection pool, you no longer need to specify - anything in Passenger or Unicorn's after_fork callback [#794] -- Add optional Worker#retries_exhausted hook after max retries failed. [jkassemi, #780] -- Fix bug in pagination link to last page [pitr, #774] -- Upstart scripts for multiple Sidekiq instances [dariocravero, #763] -- Use select via pipes instead of poll to catch signals [mrnugget, #761] - -2.8.0 ------------ - -- I18n support! Sidekiq can optionally save and restore the Rails locale - so it will be properly set when your jobs execute. Just include - `require 'sidekiq/middleware/i18n'` in your sidekiq initializer. [#750] -- Fix bug which could lose messages when using namespaces and the message -needs to be requeued in Redis. [#744] -- Refactor Redis namespace support [#747]. The redis namespace can no longer be - passed via the config file, the only supported way is via Ruby in your - initializer: - -```ruby -sidekiq_redis = { :url => 'redis://localhost:3679', :namespace => 'foo' } -Sidekiq.configure_server { |config| config.redis = sidekiq_redis } -Sidekiq.configure_client { |config| config.redis = sidekiq_redis } -``` - -A warning is printed out to the log if a namespace is found in your sidekiq.yml. - - -2.7.5 ------------ - -- Capistrano no longer uses daemonization in order to work with JRuby [#719] -- Refactor signal handling to work on Ruby 2.0 [#728, #730] -- Fix dashboard refresh URL [#732] - -2.7.4 ------------ - -- Fixed daemonization, was broken by some internal refactoring in 2.7.3 [#727] - -2.7.3 ------------ - -- Real-time dashboard is now the default web page -- Make config file optional for capistrano -- Fix Retry All button in the Web UI - -2.7.2 ------------ - -- Remove gem signing infrastructure. It was causing Sidekiq to break -when used via git in Bundler. This is why we can't have nice things. [#688] - - -2.7.1 ------------ - -- Fix issue with hard shutdown [#680] - - -2.7.0 ------------ - -- Add -d daemonize flag, capistrano recipe has been updated to use it [#662] -- Support profiling via `ruby-prof` with -p. When Sidekiq is stopped - via Ctrl-C, it will output `profile.html`. You must add `gem 'ruby-prof'` to your Gemfile for it to work. -- Dynamically update Redis stats on dashboard [brandonhilkert] -- Add Sidekiq::Workers API giving programmatic access to the current - set of active workers. - -``` -workers = Sidekiq::Workers.new -workers.size => 2 -workers.each do |name, work| - # name is a unique identifier per Processor instance - # work is a Hash which looks like: - # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg } -end -``` - -- Allow environment-specific sections within the config file which -override the global values [dtaniwaki, #630] - -``` ---- -:concurrency: 50 -:verbose: false -staging: - :verbose: true - :concurrency: 5 -``` - - -2.6.5 ------------ - -- Several reliability fixes for job requeueing upon termination [apinstein, #622, #624] -- Fix typo in capistrano recipe -- Add `retry_queue` option so retries can be given lower priority [ryanlower, #620] - -```ruby -sidekiq_options queue: 'high', retry_queue: 'low' -``` - -2.6.4 ------------ - -- Fix crash upon empty queue [#612] - -2.6.3 ------------ - -- sidekiqctl exits with non-zero exit code upon error [jmazzi] -- better argument validation in Sidekiq::Client [karlfreeman] - -2.6.2 ------------ - -- Add Dashboard beacon indicating when stats are updated. [brandonhilkert, #606] -- Revert issue with capistrano restart. [#598] - -2.6.1 ------------ - -- Dashboard now live updates summary stats also. [brandonhilkert, #605] -- Add middleware chain APIs `insert_before` and `insert_after` for fine - tuning the order of middleware. [jackrg, #595] - -2.6.0 ------------ - -- Web UI much more mobile friendly now [brandonhilkert, #573] -- Enable live polling for every section in Web UI [brandonhilkert, #567] -- Add Stats API [brandonhilkert, #565] -- Add Stats::History API [brandonhilkert, #570] -- Add Dashboard to Web UI with live and historical stat graphs [brandonhilkert, #580] -- Add option to log output to a file, reopen log file on USR2 signal [mrnugget, #581] - -2.5.4 ------------ - -- `Sidekiq::Client.push` now accepts the worker class as a string so the - Sidekiq client does not have to load your worker classes at all. [#524] -- `Sidekiq::Client.push_bulk` now works with inline testing. -- **Really** fix status icon in Web UI this time. -- Add "Delete All" and "Retry All" buttons to Retries in Web UI - - -2.5.3 ------------ - -- Small Web UI fixes -- Add `delay_until` so you can delay jobs until a specific timestamp: - -```ruby -Auction.delay_until(@auction.ends_at).close(@auction.id) -``` - -This is identical to the existing Sidekiq::Worker method, `perform_at`. - -2.5.2 ------------ - -- Remove asset pipeline from Web UI for much faster, simpler runtime. [#499, #490, #481] -- Add -g option so the procline better identifies a Sidekiq process, defaults to File.basename(Rails.root). [#486] - - sidekiq 2.5.1 myapp [0 of 25 busy] - -- Add splay to retry time so groups of failed jobs don't fire all at once. [#483] - -2.5.1 ------------ - -- Fix issues with core\_ext - -2.5.0 ------------ - -- REDESIGNED WEB UI! [unity, cavneb] -- Support Honeybadger for error delivery -- Inline testing runs the client middleware before executing jobs [#465] -- Web UI can now remove jobs from queue. [#466, dleung] -- Web UI can now show the full message, not just 100 chars [#464, dleung] -- Add APIs for manipulating the retry and job queues. See sidekiq/api. [#457] - - -2.4.0 ------------ - -- ActionMailer.delay.method now only tries to deliver if method returns a valid message. -- Logging now uses "MSG-#{Job ID}", not a random msg ID -- Allow generic Redis provider as environment variable. [#443] -- Add ability to customize sidekiq\_options with delay calls [#450] - -```ruby -Foo.delay(:retry => false).bar -Foo.delay(:retry => 10).bar -Foo.delay(:timeout => 10.seconds).bar -Foo.delay_for(5.minutes, :timeout => 10.seconds).bar -``` - -2.3.3 ------------ - -- Remove option to disable Rails hooks. [#401] -- Allow delay of any module class method - -2.3.2 ------------ - -- Fix retry. 2.3.1 accidentally disabled it. - -2.3.1 ------------ - -- Add Sidekiq::Client.push\_bulk for bulk adding of jobs to Redis. - My own simple test case shows pushing 10,000 jobs goes from 5 sec to 1.5 sec. -- Add support for multiple processes per host to Capistrano recipe -- Re-enable Celluloid::Actor#defer to fix stack overflow issues [#398] - -2.3.0 ------------ - -- Upgrade Celluloid to 0.12 -- Upgrade Twitter Bootstrap to 2.1.0 -- Rescue more Exceptions -- Change Job ID to be Hex, rather than Base64, for HTTP safety -- Use `Airbrake#notify_or_ignore` - -2.2.1 ------------ - -- Add support for custom tabs to Sidekiq::Web [#346] -- Change capistrano recipe to run 'quiet' before deploy:update\_code so - it is run upon both 'deploy' and 'deploy:migrations'. [#352] -- Rescue Exception rather than StandardError to catch and log any sort - of Processor death. - -2.2.0 ------------ - -- Roll back Celluloid optimizations in 2.1.0 which caused instability. -- Add extension to delay any arbitrary class method to Sidekiq. - Previously this was limited to ActiveRecord classes. - -```ruby -SomeClass.delay.class_method(1, 'mike', Date.today) -``` - -- Sidekiq::Client now generates and returns a random, 128-bit Job ID 'jid' which - can be used to track the processing of a Job, e.g. for calling back to a webhook - when a job is finished. - -2.1.1 ------------ - -- Handle networking errors causing the scheduler thread to die [#309] -- Rework exception handling to log all Processor and actor death (#325, subelsky) -- Clone arguments when calling worker so modifications are discarded. (#265, hakanensari) - -2.1.0 ------------ - -- Tune Celluloid to no longer run message processing within a Fiber. - This gives us a full Thread stack and also lowers Sidekiq's memory - usage. -- Add pagination within the Web UI [#253] -- Specify which Redis driver to use: *hiredis* or *ruby* (default) -- Remove FailureJobs and UniqueJobs, which were optional middleware - that I don't want to support in core. [#302] - -2.0.3 ------------ -- Fix sidekiq-web's navbar on mobile devices and windows under 980px (ezkl) -- Fix Capistrano task for first deploys [#259] -- Worker subclasses now properly inherit sidekiq\_options set in - their superclass [#221] -- Add random jitter to scheduler to spread polls across POLL\_INTERVAL - window. [#247] -- Sidekiq has a new mailing list: sidekiq@librelist.org See README. - -2.0.2 ------------ - -- Fix "Retry Now" button on individual retry page. (ezkl) - -2.0.1 ------------ - -- Add "Clear Workers" button to UI. If you kill -9 Sidekiq, the workers - set can fill up with stale entries. -- Update sidekiq/testing to support new scheduled jobs API: - - ```ruby - require 'sidekiq/testing' - DirectWorker.perform_in(10.seconds, 1, 2) - assert_equal 1, DirectWorker.jobs.size - assert_in_delta 10.seconds.from_now.to_f, DirectWorker.jobs.last['at'], 0.01 - ``` - -2.0.0 ------------ - -- **SCHEDULED JOBS**! - -You can now use `perform_at` and `perform_in` to schedule jobs -to run at arbitrary points in the future, like so: - -```ruby - SomeWorker.perform_in(5.days, 'bob', 13) - SomeWorker.perform_at(5.days.from_now, 'bob', 13) -``` - -It also works with the delay extensions: - -```ruby - UserMailer.delay_for(5.days).send_welcome_email(user.id) -``` - -The time is approximately when the job will be placed on the queue; -it is not guaranteed to run at precisely at that moment in time. - -This functionality is meant for one-off, arbitrary jobs. I still -recommend `whenever` or `clockwork` if you want cron-like, -recurring jobs. See `examples/scheduling.rb` - -I want to specially thank @yabawock for his work on sidekiq-scheduler. -His extension for Sidekiq 1.x filled an obvious functional gap that I now think is -useful enough to implement in Sidekiq proper. - -- Fixed issues due to Redis 3.x API changes. Sidekiq now requires - the Redis 3.x client. -- Inline testing now round trips arguments through JSON to catch - serialization issues (betelgeuse) - -1.2.1 ------------ - -- Sidekiq::Worker now has access to Sidekiq's standard logger -- Fix issue with non-StandardErrors leading to Processor exhaustion -- Fix issue with Fetcher slowing Sidekiq shutdown -- Print backtraces for all threads upon TTIN signal [#183] -- Overhaul retries Web UI with new index page and bulk operations [#184] - -1.2.0 ------------ - -- Full or partial error backtraces can optionally be stored as part of the retry - for display in the web UI if you aren't using an error service. [#155] - -```ruby -class Worker - include Sidekiq::Worker - sidekiq_options :backtrace => [true || 10] -end -``` -- Add timeout option to kill a worker after N seconds (blackgold9) - -```ruby -class HangingWorker - include Sidekiq::Worker - sidekiq_options :timeout => 600 - def perform - # will be killed if it takes longer than 10 minutes - end -end -``` - -- Fix delayed extensions not available in workers [#152] -- In test environments add the `#drain` class method to workers. This method - executes all previously queued jobs. (panthomakos) -- Sidekiq workers can be run inline during tests, just `require 'sidekiq/testing/inline'` (panthomakos) -- Queues can now be deleted from the Sidekiq web UI [#154] -- Fix unnecessary shutdown delay due to Retry Poller [#174] - -1.1.4 ------------ - -- Add 24 hr expiry for basic keys set in Redis, to avoid any possible leaking. -- Only register workers in Redis while working, to avoid lingering - workers [#156] -- Speed up shutdown significantly. - -1.1.3 ------------ - -- Better network error handling when fetching jobs from Redis. - Sidekiq will retry once per second until it can re-establish - a connection. (ryanlecompte) -- capistrano recipe now uses `bundle_cmd` if set [#147] -- handle multi\_json API changes (sferik) - -1.1.2 ------------ - -- Fix double restart with cap deploy [#137] - -1.1.1 ------------ - -- Set procline for easy monitoring of Sidekiq status via "ps aux" -- Fix race condition on shutdown [#134] -- Fix hang with cap sidekiq:start [#131] - -1.1.0 ------------ - -- The Sidekiq license has switched from GPLv3 to LGPLv3! -- Sidekiq::Client.push now returns whether the actual Redis - operation succeeded or not. [#123] -- Remove UniqueJobs from the default middleware chain. Its - functionality, while useful, is unexpected for new Sidekiq - users. You can re-enable it with the following config. - Read #119 for more discussion. - -```ruby -Sidekiq.configure_client do |config| - require 'sidekiq/middleware/client/unique_jobs' - config.client_middleware do |chain| - chain.add Sidekiq::Middleware::Client::UniqueJobs - end -end -Sidekiq.configure_server do |config| - require 'sidekiq/middleware/server/unique_jobs' - config.server_middleware do |chain| - chain.add Sidekiq::Middleware::Server::UniqueJobs - end -end -``` - -1.0.0 ------------ - -Thanks to all Sidekiq users and contributors for helping me -get to this big milestone! - -- Default concurrency on client-side to 5, not 25 so we don't - create as many unused Redis connections, same as ActiveRecord's - default pool size. -- Ensure redis= is given a Hash or ConnectionPool. - -0.11.2 ------------ - -- Implement "safe shutdown". The messages for any workers that - are still busy when we hit the TERM timeout will be requeued in - Redis so the messages are not lost when the Sidekiq process exits. - [#110] -- Work around Celluloid's small 4kb stack limit [#115] -- Add support for a custom Capistrano role to limit Sidekiq to - a set of machines. [#113] - -0.11.1 ------------ - -- Fix fetch breaking retry when used with Redis namespaces. [#109] -- Redis connection now just a plain ConnectionPool, not CP::Wrapper. -- Capistrano initial deploy fix [#106] -- Re-implemented weighted queues support (ryanlecompte) - -0.11.0 ------------ - -- Client-side API changes, added sidekiq\_options for Sidekiq::Worker. - As a side effect of this change, the client API works on Ruby 1.8. - It's not officially supported but should work [#103] -- NO POLL! Sidekiq no longer polls Redis, leading to lower network - utilization and lower latency for message processing. -- Add --version CLI option - -0.10.1 ------------ - -- Add details page for jobs in retry queue (jcoene) -- Display relative timestamps in web interface (jcoene) -- Capistrano fixes (hinrik, bensie) - -0.10.0 ------------ - -- Reworked capistrano recipe to make it more fault-tolerant [#94]. -- Automatic failure retry! Sidekiq will now save failed messages - and retry them, with an exponential backoff, over about 20 days. - Did a message fail to process? Just deploy a bug fix in the next - few days and Sidekiq will retry the message eventually. - -0.9.1 ------------ - -- Fix missed deprecations, poor method name in web UI - -0.9.0 ------------ - -- Add -t option to configure the TERM shutdown timeout -- TERM shutdown timeout is now configurable, defaults to 5 seconds. -- USR1 signal now stops Sidekiq from accepting new work, - capistrano sends USR1 at start of deploy and TERM at end of deploy - giving workers the maximum amount of time to finish. -- New Sidekiq::Web rack application available -- Updated Sidekiq.redis API - -0.8.0 ------------ - -- Remove :namespace and :server CLI options (mperham) -- Add ExceptionNotifier support (masterkain) -- Add capistrano support (mperham) -- Workers now log upon start and finish (mperham) -- Messages for terminated workers are now automatically requeued (mperham) -- Add support for Exceptional error reporting (bensie) - -0.7.0 ------------ - -- Example chef recipe and monitrc script (jc00ke) -- Refactor global configuration into Sidekiq.configure\_server and - Sidekiq.configure\_client blocks. (mperham) -- Add optional middleware FailureJobs which saves failed jobs to a - 'failed' queue (fbjork) -- Upon shutdown, workers are now terminated after 5 seconds. This is to - meet Heroku's hard limit of 10 seconds for a process to shutdown. (mperham) -- Refactor middleware API for simplicity, see sidekiq/middleware/chain. (mperham) -- Add `delay` extensions for ActionMailer and ActiveRecord. (mperham) -- Added config file support. See test/config.yml for an example file. (jc00ke) -- Added pidfile for tools like monit (jc00ke) - -0.6.0 ------------ - -- Resque-compatible processing stats in redis (mperham) -- Simple client testing support in sidekiq/testing (mperham) -- Plain old Ruby support via the -r cli flag (mperham) -- Refactored middleware support, introducing ability to add client-side middleware (ryanlecompte) -- Added middleware for ignoring duplicate jobs (ryanlecompte) -- Added middleware for displaying jobs in resque-web dashboard (maxjustus) -- Added redis namespacing support (maxjustus) - -0.5.1 ------------ - -- Initial release! +- Extracted from https://github.com/mperham/sidekiq/tree/v6.4.1 diff --git a/Ent-2.0-Upgrade.md b/Ent-2.0-Upgrade.md deleted file mode 100644 index 79b44a8c..00000000 --- a/Ent-2.0-Upgrade.md +++ /dev/null @@ -1,37 +0,0 @@ -# Welcome to Sidekiq Enterprise 2.0! - -Sidekiq Enterprise 2.0 adds a few new features and adds the requirement that license -credentials be available at runtime. Note that Sidekiq 6.0 does have major breaking changes. - -## What's New - -* Sidekiq Enterprise now requires license credentials at runtime. If you - configured Bundler as described in the access email you need do -nothing, everything should just work. If you are vendoring Sidekiq -Enterprise you will need to configure Bundler also or set -`SIDEKIQ_ENT_USERNAME=abcdef12 bundle exec sidekiq...` when starting the -process. [#4232] -* Dead jobs now release any unique locks they were holding when they died [#4162] -* Backoff can now be customized per rate limiter by passing in a Proc [#4219] -```ruby -limiter = Sidekiq::Limiter.bucket(:stripe, 10, :second, backoff: ->(limiter, job) { - return job['overrated'] || 5 # wait for N seconds, where N is the number of - # times we've failed the rate limit -}) -``` -* Removed deprecated APIs and warnings. -* Various changes for Sidekiq 6.0 -* Requires Ruby 2.5+ and Redis 4.0+ -* Requires Sidekiq 6.0+ and Sidekiq Pro 5.0+ - -## Upgrade - -* Upgrade to the latest Sidekiq Enterprise 1.x. -```ruby -gem 'sidekiq-ent', '< 2' -``` -* Fix any deprecation warnings you see. -* Upgrade to 2.x. -```ruby -gem 'sidekiq-ent', '< 3' -``` diff --git a/Ent-Changes.md b/Ent-Changes.md deleted file mode 100644 index 9c9b2faf..00000000 --- a/Ent-Changes.md +++ /dev/null @@ -1,333 +0,0 @@ -# Sidekiq Enterprise Changelog - -[Sidekiq Changes](https://github.com/mperham/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/mperham/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/mperham/sidekiq/blob/main/Ent-Changes.md) - -Please see [sidekiq.org](https://sidekiq.org) for more details and how to buy. - -HEAD -------------- - -- Fix periodic jobs missing the "fallback" hour during DST changeover [#5049] - -2.3.0 -------------- - -- Remove jQuery usage in UI tabs -- Pass exception to rate limiter backoff proc [#5024] - -2.2.3 -------------- - -- Fixes for leaky and unlimited limiters [#4809, #4869] -- Invalid leaders now immediately step down [#4950] -- Web UI now displays "next run time" in the specified timezone [#4833] -- Fix swarm memory monitoring on BSDs - -2.2.2 -------------- - -- Periodic job timezone fix [#4796] - -2.2.1 -------------- - -- Support configurable timezones for periodic jobs [#4749] -- Handle edge case leading to negative expiry in uniqueness [#4763] - -2.2.0 -------------- - -- Add new **leaky bucket** rate limiter [#4414] - This allows clients to burst up to X calls before throttling - back to X calls per Y seconds. To limit the user to 60 calls - per minute: -```ruby -leaker = Sidekiq::Limiter.leaky("shopify", 60, :minute) -leaker.within_limit do - ... -end -``` - See the Rate Limiting wiki page for more detail. -- Rate limiters may now customize their reschedule count [#4725] - To disable rate limit reschedules, use `reschedule: 0`. -```ruby -Sidekiq::Limiter.concurrent("somename", 5, reschedule: 0) -``` -- Allow filtering by name in the Rate Limiter UI [#4695] -- Add IT locale - -2.1.2 -------------- - -- The Sidekiq Pro and Enterprise gem servers now `bundle install` much faster with **Bundler 2.2+** [#4158] -- Now that ActiveJobs support `sidekiq_options`, add support for uniqueness in AJs [#4667] - -2.1.1 -------------- - -- Add optional **app preload** in swarm, saves even more memory [#4646] -- Fix incorrect queue tags in historical metrics [#4377] - -2.1.0 -------------- - -- Move historical metrics to use tags rather than interpolating name [#4377] -``` -sidekiq.enqueued.#{name} -> sidekiq.queue.size with tag queue:#{name} -sidekiq.latency.#{name} -> sidekiq.queue.latency with tag queue:#{name} -``` -- Remove `concurrent-ruby` gem dependency [#4586] -- Add systemd `Type=notify` support for swarm [#4511] -- Length swarm's boot timeout to 60 sec [#4544] -- Add NL locale - -2.0.1 -------------- - -- Periodic job registration API adjusted to avoid loading classes in initializer [#4271] -- Remove support for deprecated ENV variables (COUNT, MAXMEM\_MB, INDEX) in swarm code - -2.0.0 -------------- - -- Except for the [newly required credentials](https://github.com/mperham/sidekiq/issues/4232), Sidekiq Enterprise 2.0 does - not have any significant migration steps. -- Sidekiq Enterprise must now be started with valid license credentials. [#4232] -- Call `GC.compact` if possible in sidekiqswarm before forking [#4181] -- Changes for forward-compatibility with Sidekiq 6.0. -- Add death handler to remove any lingering unique locks [#4162] -- Backoff can now be customized per rate limiter [#4219] -- Code formatting changes for StandardRB - -1.8.1 -------------- - -- Fix excessive lock reclaims with concurrent limiter [#4105] -- Add ES translations, see issues [#3949](https://github.com/mperham/sidekiq/issues/3949) and [#3951](https://github.com/mperham/sidekiq/issues/3951) to add your own language. - -1.8.0 -------------- - -- Require Sidekiq Pro 4.0 and Sidekiq 5.2. -- Refactor historical metrics API to use revamped Statsd support in Sidekiq Pro -- Add a gauge to historical metrics for `default` queue latency [#4079] - -1.7.2 -------------- - -- Add PT and JA translations -- Fix elapsed time calculations to use monotonic clock [#4000, sj26] -- Fix edge case where flapping leadership would cause old periodic - jobs to be fired once [#3974] -- Add support for sidekiqswarm memory monitoring on FreeBSD [#3884] - -1.7.1 -------------- - -- Fix Lua error in concurrent rate limiter under heavy contention -- Remove superfluous `freeze` calls on Strings [#3759] - -1.7.0 -------------- - -- **NEW FEATURE** [Rolling restarts](https://github.com/mperham/sidekiq/wiki/Ent-Rolling-Restarts) - great for long running jobs! -- Adjust middleware so unique jobs that don't push aren't registered in a Batch [#3662] -- Add new unlimited rate limiter, useful for testing [#3743] -```ruby -limiter = Sidekiq::Limiter.unlimited(...any args...) -``` - -1.6.1 -------------- - -- Fix crash in rate limiter middleware when used with custom exceptions [#3604] - -1.6.0 -------------- - -- Show process "leader" tag on Busy page, requires Sidekiq 5.0.2 [#2867] -- Capture custom metrics with the `save_history` API. [#2815] -- Implement new `unique_until: 'start'` policy option. [#3471] - -1.5.4 -------------- - -- Fix broken Cron page in Web UI [#3458] - -1.5.3 -------------- - -- Remove dependency on the algorithms gem [#3446] -- Allow user to specify max memory in megabytes with SIDEKIQ\_MAXMEM\_MB [#3451] -- Implement logic to detect app startup failure, sidekiqswarm will exit - rather than try to restart the app forever [#3450] -- Another fix for doubly-encrypted arguments [#3368] - -1.5.2 -------------- - -- Fix encrypted arguments double-encrypted by retry or rate limiting [#3368] -- Fix leak in concurrent rate limiter, run this in Rails console to clean up existing data [#3323] -```ruby -expiry = 1.month.to_i; Sidekiq::Limiter.redis { |c| c.scan_each(match: "lmtr-cfree-*") { |key| c.expire(key, expiry) } } -``` - -1.5.1 -------------- - -- Fix issue with census startup when not using Bundler configuration for - source credentials. - -1.5.0 -------------- - -- Add new web authorization API [#3251] -- Update all sidekiqswarm env vars to use SIDEKIQ\_ prefix [#3218] -- Add census reporting, the leader will ping contribsys nightly with aggregate usage metrics - -1.4.0 -------------- - -- No functional changes, require latest Sidekiq and Sidekiq Pro versions - -1.3.2 -------------- - -- Upgrade encryption to use OpenSSL's more secure GCM mode. [#3060] - -1.3.1 -------------- - -- Fix multi-process memory monitoring on CentOS 6.x [#3063] -- Polish the new encryption feature a bit. - -1.3.0 -------------- - -- **BETA** [New encryption feature](https://github.com/mperham/sidekiq/wiki/Ent-Encryption) - which automatically encrypts the last argument of a Worker, aka the secret bag. - -1.2.4 -------------- - -- Fix issue causing some minutely jobs to execute every other minute. -- Log a warning if slow periodic processing causes us to miss a clock tick. - -1.2.3 -------------- - -- Periodic jobs could stop executing until process restart if Redis goes down [#3047] - -1.2.2 -------------- - -- Add API to check if a unique lock is present. See [#2932] for details. -- Tune concurrent limiters to minimize thread thrashing under heavy contention. [#2944] -- Add option for tuning which Bundler groups get preloaded with `sidekiqswarm` [#3025] -``` -SIDEKIQ_PRELOAD=default,production bin/sidekiqswarm ... -# Use an empty value for maximum application compatibility -SIDEKIQ_PRELOAD= bin/sidekiqswarm ... -``` - -1.2.1 -------------- - -- Multi-Process mode can now monitor the RSS memory of children and - restart any that grow too large. To limit children to 1GB each: -``` -MAXMEM_KB=1048576 COUNT=2 bundle exec sidekiqswarm ... -``` - -1.2.0 -------------- - -- **NEW FEATURE** Multi-process mode! Sidekiq Enterprise can now fork multiple worker - processes, enabling significant memory savings. See the [wiki -documentation](https://github.com/mperham/sidekiq/wiki/Ent-Multi-Process) for details. - - -0.7.10 -------------- - -- More precise gemspec dependency versioning - -1.1.0 -------------- - -- **NEW FEATURE** Historical queue metrics, [documented in the wiki](https://github.com/mperham/sidekiq/wiki/Ent-Historical-Metrics) [#2719] - -0.7.9, 1.0.2 -------------- - -- Window limiters can now accept arbitrary window sizes [#2686] -- Fix race condition in window limiters leading to non-stop OverLimit [#2704] -- Fix invalid overage counts when nesting concurrent limiters - -1.0.1 ----------- - -- Fix crash in periodic subsystem when a follower shuts down, thanks - to @justinko for reporting. - -1.0.0 ----------- - -- Enterprise 1.x targets Sidekiq 4.x. -- Rewrite several features to remove Celluloid dependency. No - functional changes. - -0.7.8 ----------- - -- Fix `unique_for: false` [#2658] - - -0.7.7 ----------- - -- Enterprise 0.x targets Sidekiq 3.x. -- Fix racy shutdown event which could lead to disappearing periodic - jobs, requires Sidekiq >= 3.5.3. -- Add new :leader event which is fired when a process gains leadership. - -0.7.6 ----------- - -- Redesign how overrated jobs are rescheduled to avoid creating new - jobs. [#2619] - -0.7.5 ----------- - -- Fix dynamic creation of concurrent limiters [#2617] - -0.7.4 ----------- -- Add additional check to prevent duplicate periodic job creation -- Allow user-specified TTLs for rate limiters [#2607] -- Paginate rate limiter index page [#2606] - -0.7.3 ----------- - -- Rework `Sidekiq::Limiter` redis handling to match global redis handling. -- Allow user to customize rate limit backoff logic and handle custom - rate limit errors. -- Fix scalability issue with Limiter index page. - -0.7.2 ----------- - -- Fix typo which prevented limiters with '0' in their names. - -0.7.1 ----------- - -- Fix issue where unique scheduled jobs can't be enqueued upon schedule - due to the existing unique lock. [#2499] - -0.7.0 ----------- - -Initial release. diff --git a/Gemfile b/Gemfile index cf298b27..e1fdc703 100644 --- a/Gemfile +++ b/Gemfile @@ -2,6 +2,7 @@ source "https://rubygems.org" gemspec +gem "sidekiq", ">= 6.4.1" gem "rake" gem "redis-namespace" gem "rails", ">= 6.0.2" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index b0c17da0..00000000 --- a/Gemfile.lock +++ /dev/null @@ -1,217 +0,0 @@ -PATH - remote: . - specs: - sidekiq (6.4.1) - connection_pool (>= 2.2.2) - rack (~> 2.0) - redis (>= 4.2.0) - -GEM - remote: https://rubygems.org/ - specs: - actioncable (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) - nio4r (~> 2.0) - websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) - mail (>= 2.7.1) - actionmailer (6.1.4.4) - actionpack (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activesupport (= 6.1.4.4) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (6.1.4.4) - actionview (= 6.1.4.4) - activesupport (= 6.1.4.4) - rack (~> 2.0, >= 2.0.9) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.4) - actionpack (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) - nokogiri (>= 1.8.5) - actionview (6.1.4.4) - activesupport (= 6.1.4.4) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.4.4) - activesupport (= 6.1.4.4) - globalid (>= 0.3.6) - activemodel (6.1.4.4) - activesupport (= 6.1.4.4) - activerecord (6.1.4.4) - activemodel (= 6.1.4.4) - activesupport (= 6.1.4.4) - activestorage (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activesupport (= 6.1.4.4) - marcel (~> 1.0.0) - mini_mime (>= 1.1.0) - activesupport (6.1.4.4) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - ast (2.4.2) - builder (3.2.4) - codecov (0.2.8) - json - simplecov - concurrent-ruby (1.1.9) - connection_pool (2.2.5) - crass (1.0.6) - digest (3.1.0) - docile (1.3.2) - erubi (1.10.0) - globalid (1.0.0) - activesupport (>= 5.0) - hiredis (0.6.3) - hiredis (0.6.3-java) - i18n (1.8.11) - concurrent-ruby (~> 1.0) - io-wait (0.2.1) - json (2.3.1) - json (2.3.1-java) - loofah (2.13.0) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.1) - mini_mime (>= 0.1.1) - marcel (1.0.2) - method_source (1.0.0) - mini_mime (1.1.2) - mini_portile2 (2.6.1) - minitest (5.15.0) - net-protocol (0.1.2) - io-wait - timeout - net-smtp (0.3.1) - digest - net-protocol - timeout - nio4r (2.5.8) - nio4r (2.5.8-java) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) - racc (~> 1.4) - nokogiri (1.12.5-java) - racc (~> 1.4) - parallel (1.21.0) - parser (3.1.0.0) - ast (~> 2.4.1) - racc (1.6.0) - racc (1.6.0-java) - rack (2.2.3) - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.4.4) - actioncable (= 6.1.4.4) - actionmailbox (= 6.1.4.4) - actionmailer (= 6.1.4.4) - actionpack (= 6.1.4.4) - actiontext (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activemodel (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) - bundler (>= 1.15.0) - railties (= 6.1.4.4) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) - loofah (~> 2.3) - railties (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) - method_source - rake (>= 0.13) - thor (~> 1.0) - rainbow (3.0.0) - rake (13.0.6) - redis (4.5.1) - redis-namespace (1.8.1) - redis (>= 3.0.4) - regexp_parser (2.2.0) - rexml (3.2.5) - rubocop (1.24.1) - parallel (~> 1.10) - parser (>= 3.0.0.0) - rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.15.1, < 2.0) - ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.15.1) - parser (>= 3.0.1.1) - rubocop-performance (1.13.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - ruby-progressbar (1.11.0) - simplecov (0.19.0) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov-html (0.12.3) - sprockets (4.0.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) - sqlite3 (1.4.2) - standard (1.6.0) - rubocop (= 1.24.1) - rubocop-performance (= 1.13.1) - thor (1.2.1) - timeout (0.2.0) - toxiproxy (1.0.3) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - unicode-display_width (2.1.0) - websocket-driver (0.7.5) - websocket-extensions (>= 0.1.0) - websocket-driver (0.7.5-java) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) - zeitwerk (2.5.3) - -PLATFORMS - java - ruby - universal-java-1.8 - -DEPENDENCIES - activerecord-jdbcsqlite3-adapter - codecov - hiredis - minitest - net-smtp - rails (>= 6.0.2) - rake - redis-namespace - sidekiq! - simplecov - sqlite3 - standard - toxiproxy diff --git a/Pro-2.0-Upgrade.md b/Pro-2.0-Upgrade.md deleted file mode 100644 index e6fe75fc..00000000 --- a/Pro-2.0-Upgrade.md +++ /dev/null @@ -1,138 +0,0 @@ -# Upgrading to Sidekiq Pro 2.0 - -Sidekiq Pro 2.0 allows nested batches for more complex job workflows -and provides a new reliable scheduler which uses Lua to guarantee -atomicity and much higher performance. - -It also removes deprecated APIs, changes the batch data format and -how features are activated. Read carefully to ensure your upgrade goes -smoothly. - -Sidekiq Pro 2.0 requires Sidekiq 3.3.2 or greater. Redis 2.8 is -recommended; Redis 2.4 or 2.6 will work but some functionality will not be -available. - -**Note that you CANNOT go back to Pro 1.x once you've created batches -with 2.x. The new batches will not process correctly with 1.x.** - -**If you are on a version of Sidekiq Pro <1.5, you should upgrade to the -latest 1.x version and run it for a week before upgrading to 2.0.** - -## Nested Batches - -Batches can now be nested within the `jobs` method. -This feature enables Sidekiq Pro to handle workflow processing of any size -and complexity! - -```ruby -a = Sidekiq::Batch.new -a.on(:success, SomeCallback) -a.jobs do - SomeWork.perform_async - - b = Sidekiq::Batch.new - b.on(:success, MyCallback) - b.jobs do - OtherWork.perform_async - end -end -``` - -Parent batch callbacks are not processed until all child batch callbacks have -run successfully. In the example above, `MyCallback` will always fire -before `SomeCallback` because `b` is considered a child of `a`. - -Of course you can dynamically add child batches while a batch job is executing. - -```ruby -def perform(*args) - do_something(args) - - if more_work? - # Sidekiq::Worker#batch returns the Batch this job is part of. - batch.jobs do - b = Sidekiq::Batch.new - b.on(:success, MyCallback) - b.jobs do - OtherWork.perform_async - end - end - end -end -``` - -More context: [#1485] - -## Batch Data - -The batch data model was overhauled. Batch data should take -significantly less space in Redis now. A simple benchmark shows 25% -savings but real world savings should be even greater. - -* Batch 2.x BIDs are 14 character URL-safe Base64-encoded strings, e.g. - "vTF1-9QvLPnREQ". Batch 1.x BIDs were 16 character hex-encoded - strings, e.g. "4a3fc67d30370edf". -* In 1.x, batch data was not removed until it naturally expired in Redis. - In 2.x, all data for a batch is removed from Redis once the batch has - run any success callbacks. -* Because of the former point, batch expiry is no longer a concern. - Batch expiry is hardcoded to 30 days and is no longer user-tunable. -* Failed batch jobs no longer automatically store any associated - backtrace in Redis. - -**There's no data migration required. Sidekiq Pro 2.0 transparently handles -both old and new format.** - -More context: [#2130] - -## Reliability - -2.0 brings a new reliable scheduler which uses Lua inside Redis so enqueuing -scheduled jobs is atomic. Benchmarks show it 50x faster when enqueuing -lots of jobs. - -**Two caveats**: -- Client-side middleware is not executed - for each job when enqueued with the reliable scheduler. No Sidekiq or - Sidekiq Pro functionality is affected by this change but some 3rd party - plugins might be. -- The Lua script used inside the reliable scheduler is not safe for use - with Redis Cluster or other multi-master Redis solutions. - It is safe to use with Redis Sentinel or a typical master/slave replication setup. - -**You no longer require anything to use the Reliability features.** - -* Activate reliable fetch and/or the new reliable scheduler: -```ruby -Sidekiq.configure_server do |config| - config.reliable_fetch! - config.reliable_scheduler! -end -``` -* Activate reliable push: -```ruby -Sidekiq::Client.reliable_push! -``` - -More context: [#2130] - -## Other Changes - -* You must require `sidekiq/pro/notifications` if you want to use the - existing notification schemes. I don't recommend using them as the - newer-style `Sidekiq::Batch#on` method is simpler and more flexible. -* Several classes have been renamed. Generally these classes are ones - you should not need to require/use in your own code, e.g. the Batch - middleware. -* You can add `attr_accessor :jid` to a Batch callback class and Sidekiq - Pro will set it to the jid of the callback job. [#2178] -* There's now an official API to iterate all known Batches [#2191] -```ruby -Sidekiq::BatchSet.new.each {|status| p status.bid } -``` -* The Web UI now shows the Sidekiq Pro version in the footer. [#1991] - -## Thanks - -Adam Prescott, Luke van der Hoeven and Jon Hyman all provided valuable -feedback during the release process. Thank you guys! diff --git a/Pro-3.0-Upgrade.md b/Pro-3.0-Upgrade.md deleted file mode 100644 index 5e5559ff..00000000 --- a/Pro-3.0-Upgrade.md +++ /dev/null @@ -1,44 +0,0 @@ -# Welcome to Sidekiq Pro 3.0! - -Sidekiq Pro 3.0 is designed to work with Sidekiq 4.0. - -## What's New - -* **Redis 2.8.0 or greater is required.** Redis 2.8 was released two years - ago and contains **many** useful features which Sidekiq couldn't - leverage until now. **Redis 3.0.3 or greater is recommended** for large - scale use. - -* Sidekiq Pro no longer uses Celluloid. If your application code uses Celluloid, - you will need to pull it in yourself. - -* Pausing and unpausing queues is now instantaneous, no more polling! - -* Reliable fetch has been re-implemented due to the fetch changes in - Sidekiq 4.0. - -* Support for platforms without persistent hostnames. Since the reliable\_fetch - algorithm requires a persistent hostname, an alternative reliability -algorithm is now available for platforms like Heroku and Docker: -```ruby -Sidekiq.configure_server do |config| - config.timed_fetch! -end -``` - The wiki contains [much more detail about each reliability option](https://github.com/mperham/sidekiq/wiki/Pro-Reliability-Server). - -* The old 'sidekiq/notifications' features have been removed. - -## Upgrade - -First, make sure you are using Redis 2.8 or greater. Next: - -* Upgrade to the latest Sidekiq Pro 2.x. -```ruby -gem 'sidekiq-pro', '< 3' -``` -* Fix any deprecation warnings you see. -* Upgrade to 3.x. -```ruby -gem 'sidekiq-pro', '< 4' -``` diff --git a/Pro-4.0-Upgrade.md b/Pro-4.0-Upgrade.md deleted file mode 100644 index 82a4994e..00000000 --- a/Pro-4.0-Upgrade.md +++ /dev/null @@ -1,35 +0,0 @@ -# Welcome to Sidekiq Pro 4.0! - -Sidekiq Pro 4.0 is designed to work with Sidekiq 5.0. - -## What's New - -* Batches now "die" if any of their jobs die. You can enumerate the set - of dead batches and their associated dead jobs. The success callback - for a dead batch will never fire unless these jobs are fixed. -```ruby -Sidekiq::Batch::DeadSet.new.each do |status| - status.dead? # => true - status.dead_jobs # => [...] -end -``` -This API allows you to enumerate the batches which need help. -If you fix the issue and the dead jobs succeed, the batch will succeed. -* The older `reliable_fetch` and `timed_fetch` algorithms have been - removed. Only super\_fetch is available in 4.0. -* The statsd middleware has been tweaked to remove support for legacy, - pre-3.6.0 configuration and add relevant tags. -* Requires Sidekiq 5.0.5+. - -## Upgrade - -* Upgrade to the latest Sidekiq Pro 3.x. -```ruby -gem 'sidekiq-pro', '< 4' -``` -* Fix any deprecation warnings you see. -* Upgrade to 4.x. -```ruby -gem 'sidekiq-pro', '< 5' -``` - diff --git a/Pro-5.0-Upgrade.md b/Pro-5.0-Upgrade.md deleted file mode 100644 index bab7c7a3..00000000 --- a/Pro-5.0-Upgrade.md +++ /dev/null @@ -1,25 +0,0 @@ -# Welcome to Sidekiq Pro 5.0! - -Sidekiq Pro 5.0 is mainly a cleanup release for Sidekiq 6.0. The -migration should be as close to trivial as a major version bump can be. -Note that Sidekiq 6.0 does have major breaking changes. - -## What's New - -* New localizations for the Sidekiq Pro Web UI: ES, ZH, PT, JA, RU -* Removed deprecated APIs and warnings. -* Various changes for Sidekiq 6.0 -* Requires Ruby 2.5+ and Redis 4.0+ -* Requires Sidekiq 6.0+. - -## Upgrade - -* Upgrade to the latest Sidekiq Pro 4.x. -```ruby -gem 'sidekiq-pro', '< 5' -``` -* Fix any deprecation warnings you see. -* Upgrade to 5.x. -```ruby -gem 'sidekiq-pro', '< 6' -``` diff --git a/Pro-Changes.md b/Pro-Changes.md deleted file mode 100644 index 803f36f2..00000000 --- a/Pro-Changes.md +++ /dev/null @@ -1,851 +0,0 @@ -# Sidekiq Pro Changelog - -[Sidekiq Changes](https://github.com/mperham/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/mperham/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/mperham/sidekiq/blob/main/Ent-Changes.md) - -Please see [sidekiq.org](https://sidekiq.org/) for more details and how to buy. - -HEAD ---------- - -- Fix namespace issue with dogstatsd-ruby in Ruby 3+ [#5094] - -5.3.0 ---------- - -- Fix thread-safety issue with Sidekiq::Pro::Config -- Allow job-specific options in Statsd metrics [#5037] -```ruby -# add to your initializer -Sidekiq::Middleware::Server::Statsd.options = ->(klass, job, q) do - {tags: ["worker:#{klass}", "queue:#{q}"]}.tap do |h| - h[:tags] << "tenant:#{job['tenant_id']}" if job["tenant_id"] - end -end -``` - -5.2.4 ---------- - -- Initialize paused queue set before allowing jobs to be fetched [#4975] - -5.2.3 ---------- - -- Reduce superfluous logging of Redis errors [#4969] -- Display dead JIDs on Batch details page [#4926] - -5.2.2 ---------- - -- Include poison pill info in super_fetch's orphan handler [#4859] -- Use Sidekiq::Batch::Immutable error so race conditions can easily be caught [#4845] -- Fix sharded UI not using middleware in Sidekiq 6.2 [#4843] -- Compatibility with dogstatsd-ruby 4.x and 5.x [#4863] - -5.2.1 ---------- - -- Propagate death callbacks to parent batches [#4774] -- Allow customization of Batch linger to quickly reclaim memory in Redis [#4772] -- Fix disappearing processes in Busy due to super_fetch initialization when used in - tandem with `SIDEKIQ_PRELOAD_APP=1` in `sidekiqswarm`. [#4733] - -5.2.0 ---------- - -- The Sidekiq Pro and Enterprise gem servers now `bundle install` much faster with **Bundler 2.2+** [#4158] -- Fix issue with reliable push and multiple shards [#4669] -- Fix Pro memory leak due to fetch refactoring in Sidekiq 6.1 [#4652] -- Gracefully handle poison pill jobs [#4633] -- Remove support for multi-shard batches [#4642] -- Rename `Sidekiq::Rack::BatchStatus` to `Sidekiq::Pro::BatchStatus` [#4655] - -5.1.1 ---------- - -- Fix broken basic fetcher [#4616] - -5.1.0 ---------- - -- Remove old Statsd metrics with `WorkerName` in the name [#4377] -``` -job.WorkerName.count -> job.count with tag worker:WorkerName -job.WorkerName.perform -> job.perform with tag worker:WorkerName -job.WorkerName.failure -> job.failure with tag worker:WorkerName -``` -- Remove `concurrent-ruby` gem dependency [#4586] -- Update `constantize` for batch callbacks. [#4469] -- Add queue tag to `jobs.recovered.fetch` metric [#4594] -- Refactor Pro's fetch infrastructure [#4602] - -5.0.1 ---------- - -- Rejigger batch failures UI to add direct links to retries and scheduled jobs [#4209] -- Delete batch data with `UNLINK` [#4155] -- Fix bug where a scheduled job can lose its scheduled time when using reliable push [#4267] -- Sidekiq::JobSet#scan and #find_job APIs have been promoted to Sidekiq OSS. [#4259] - -5.0.0 ---------- - -- There is no significant migration from Sidekiq Pro 4.0 to 5.0 - but make sure you read the [update notes for Sidekiq -6.0](https://github.com/mperham/sidekiq/blob/master/6.0-Upgrade.md). -- Removed various deprecated APIs and associated warnings. -- **BREAKING CHANGE** Remove the `Sidekiq::Batch::Status#dead_jobs` API in favor of - `Sidekiq::Batch::Status#dead_jids`. [#4217] -- Update Sidekiq Pro codebase to use StandardRB formatting -- Fix lingering "b-XXX-died" elements in Redis which could cause - excessive memory usage. [#4217] -- Add ES translations, see issues [#3949](https://github.com/mperham/sidekiq/issues/3949) and [#3951](https://github.com/mperham/sidekiq/issues/3951) to add your own language. - -4.0.5 ---------- - -- Increase super\_fetch retriever thread count from 1 to 2 to make it - less sensitive to Redis latency. -- Better handling of invalid job JSON by reliable scheduler [#4053] -- Added ZH, PT, JA and RU translations. - -4.0.4 ---------- - -- Update Sidekiq::Client patches to work with new Module#prepend - mechanism in Sidekiq 5.2.0. [#3930] - -4.0.3 ---------- - -- Add at\_exit handler to push any saved jobs in `reliable_push` when exiting. [#3823] -- Implement batch death callback. This is fired the first time a job within a batch dies. [#3841] -```ruby -batch = Sidekiq::Batch.new -batch.on(:death, ...) -``` - -4.0.2 ---------- - -- Remove super\_fetch edge case leading to an unnecessary `sleep(1)` - call and resulting latency [#3790] -- Fix possible bad statsd metric call on super\_fetch startup -- Remove superfluous `freeze` calls on Strings [#3759] - -4.0.1 ---------- - -- Fix incompatibility with the statsd-ruby gem [#3740] -- Add tags to Statsd metrics when using Datadog [#3744] - -4.0.0 ---------- - -- See the [Sidekiq Pro 4.0](Pro-4.0-Upgrade.md) release notes. - - -3.7.1 ---------- - -- Deprecate timed\_fetch. Switch to super\_fetch: -```ruby -config.super_fetch! -``` - - -3.7.0 ---------- - -- Refactor batch job success/failure to gracefully handle several edge - cases with regard to Sidekiq::Shutdown. This should greatly reduce - the chances of seeing the long-standing "negative pending count" problem. [#3710] - - -3.6.1 ---------- - -- Add support for Datadog::Statsd, it is the recommended Statsd client. [#3699] -```ruby -Sidekiq::Pro.dogstatsd = ->{ Datadog::Statsd.new("metrics.example.com", 8125) } -``` -- Size the statsd connection pool based on Sidekiq's concurrency [#3700] - - -3.6.0 ---------- - -This release overhauls the Statsd metrics support and adds more -metrics for tracking Pro feature usage. In your initializer: -```ruby -Sidekiq::Pro.statsd = ->{ ::Statsd.new("127.0.0.1", 8125) } -``` -Sidekiq Pro will emit more metrics to Statsd: -``` -jobs.expired - when a job is expired -jobs.recovered.push - when a job is recovered by reliable_push after network outage -jobs.recovered.fetch - when a job is recovered by super_fetch after process crash -batch.created - when a batch is created -batch.complete - when a batch is completed -batch.success - when a batch is successful -``` -Sidekiq Pro's existing Statsd middleware has been rewritten to leverage the new API. -Everything should be backwards compatible with one deprecation notice. - - -3.5.4 ---------- - -- Fix case in SuperFetch where Redis downtime can lead to processor thread death [#3684] -- Fix case where TimedFetch might not recover some pending jobs -- Fix edge case in Batch::Status#poll leading to premature completion [#3640] -- Adjust scan API to check 100 elements at a time, to minimize network round trips - when scanning large sets. - -3.5.3 ---------- - -- Restore error check for super\_fetch's job ack [#3601] -- Trim error messages saved in Batch's failure hash, preventing huge - messages from bloating Redis. [#3570] - -3.5.2 ---------- - -- Fix `Status#completed?` when run against a Batch that had succeeded - and was deleted. [#3519] - -3.5.1 ---------- - -- Work with Sidekiq 5.0.2+ -- Improve performance of super\_fetch with weighted queues [#3489] - -3.5.0 ---------- - -- Add queue pause/unpause endpoints for scripting via curl [#3445] -- Change how super\_fetch names private queues to avoid hostname/queue clashes. [#3443] -- Re-implement `Sidekiq::Queue#delete_job` to avoid O(n) runtime [#3408] -- Batch page displays Pending JIDs if less than 10 [#3130] -- Batch page has a Search button to find associated Retries [#3130] -- Make Batch UI progress bar more friendly to the colorblind [#3387] - -3.4.5 ---------- - -- Fix potential job loss with reliable scheduler when lots of jobs are scheduled - at precisely the same time. Thanks to raivil for his hard work in - reproducing the bug. [#3371] - -3.4.4 ---------- - -- Optimize super\_fetch shutdown to restart jobs quicker [#3249] - -3.4.3 ---------- - -- Limit reliable scheduler to enqueue up to 100 jobs per call, minimizing Redis latency [#3332] -- Fix bug in super\_fetch logic for queues with `_` in the name [#3339] - -3.4.2 ---------- - -- Add `Batch::Status#invalidated?` API which returns true if any/all - JIDs were invalidated within the batch. [#3326] - -3.4.1 ---------- - -- Allow super\_fetch's orphan job check to happen as often as every hour [#3273] -- Officially deprecate reliable\_fetch algorithm, I now recommend you use `super_fetch` instead: -```ruby -Sidekiq.configure_server do |config| - config.super_fetch! -end -``` -Also note that Sidekiq's `-i/--index` option is no longer used/relevant with super\_fetch. -- Don't display "Delete/Retry All" buttons when filtering in Web UI [#3243] -- Reimplement Sidekiq::JobSet#find\_job with ZSCAN [#3197] - -3.4.0 ---------- - -- Introducing the newest reliable fetching algorithm: `super_fetch`! This - algorithm will replace reliable\_fetch in Pro 4.0. super\_fetch is - bullet-proof across all environments, no longer requiring stable - hostnames or an index to be set per-process. [#3077] -```ruby -Sidekiq.configure_server do |config| - config.super_fetch! -end -``` - Thank you to @jonhyman for code review and the Sidekiq Pro customers that - beta tested super\_fetch. - -3.3.3 ---------- - -- Update Web UI extension to work with Sidekiq 4.2.0's new Web UI. [#3075] - -3.3.2 ---------- - -- Minimize batch memory usage after success [#3083] -- Extract batch's 24 hr linger expiry to a LINGER constant so it can be tuned. [#3011] - -3.3.1 ---------- - -- If environment is unset, treat it as development so reliable\_fetch works as before 3.2.2. - -3.3.0 ---------- - -- Don't delete batches immediately upon success but set a 24 hr expiry, this allows - Sidekiq::Batch::Status#poll to work, even after batch success. [#3011] -- New `Sidekiq::PendingSet#destroy(jid)` API to remove poison pill jobs [#3015] - -3.2.2 ---------- - -- A default value for -i is only set in development now, staging or - other environments must set an index if you wish to use reliable\_fetch. [#2971] -- Fix nil dereference when checking for jobs over timeout in timed\_fetch - - -3.2.1 ---------- - -- timed\_fetch now works with namespaces. [ryansch] - - -3.2.0 ---------- - -- Fixed detection of missing batches, `NoSuchBatch` should be raised - properly now if `Sidekiq::Batch.new(bid)` is called on a batch no - longer in Redis. -- Remove support for Pro 1.x format batches. This version will no - longer seamlessly process batches created with Sidekiq Pro 1.x. - As always, upgrade one major version at a time to ensure a smooth - transition. -- Fix edge case where a parent batch could expire before a child batch - was finished processing, leading to missing batches [#2889] - -2.1.5 ---------- - -- Fix edge case where a parent batch could expire before a child batch - was finished processing, leading to missing batches [#2889] - -3.1.0 ---------- - -- New container-friendly fetch algorithm: `timed_fetch`. See the - [wiki documentation](https://github.com/mperham/sidekiq/wiki/Pro-Reliability-Server) - for trade offs between the two reliability options. You should - use this if you are on Heroku, Docker, Amazon ECS or EBS or - another container-based system. - - -3.0.6 ---------- - -- Fix race condition on reliable fetch shutdown - -3.0.5 ---------- - -- Statsd metrics now account for ActiveJob class names -- Allow reliable fetch internals to be overridden [jonhyman] - -3.0.4 ---------- - -- Queue pausing no longer requires reliable fetch. [#2786] - -3.0.3, 2.1.4 ------------- - -- Convert Lua-based `Sidekiq::Queue#delete_by_class` to Ruby-based, to - avoid O(N^2) performance and possible Redis failure. [#2806] - -3.0.2 ------------ - -- Make job registration with batch part of the atomic push so batch - metadata can't get out of sync with the job data. [#2714] - -3.0.1 ------------ - -- Remove a number of Redis version checks since we can assume 2.8+ now. -- Fix expiring jobs client middleware not loaded on server - -3.0.0 ------------ - -- See the [Pro 3.0 release notes](Pro-3.0-Upgrade.md). - -2.1.3 ------------ - -- Don't enable strict priority if using weighted queueing like `-q a,1 -q b,1` -- Safer JSON mangling in Lua [#2639] - -2.1.2 ------------ - -- Lock Sidekiq Pro 2.x to Sidekiq 3.x. - -2.1.1 ------------ - -- Make ShardSet lazier so Redis can first be initialized at startup. [#2603] - - -2.1.0 ------------ - -- Explicit support for sharding batches. You list your Redis shards and - Sidekiq Pro will randomly spread batches across the shards. The BID - will indicate which shard contains the batch data. Jobs within a - batch may be spread across all shards too. [#2548, jonhyman] -- Officially deprecate Sidekiq::Notifications code. Notifications have - been undocumented for months now. [#2575] - - -2.0.8 ------------ - -- Fix reliable scheduler mangling large numeric arguments. Lua's CJSON - library cannot accurately encode numbers larger than 14 digits! [#2478] - -2.0.7 ------------ - -- Optimize delete of enormous batches (100,000s of jobs) [#2458] - -2.0.6, 1.9.3 --------------- - -- CSRF protection in Sidekiq 3.4.2 broke job filtering in the Web UI [#2442] -- Sidekiq Pro 1.x is now limited to Sidekiq < 3.5.0. - -2.0.5 ------------ - -- Atomic scheduler now sets `enqueued_at` [#2414] -- Batches now account for jobs which are stopped by client middleware [#2406] -- Ignore redundant calls to `Sidekiq::Client.reliable_push!` [#2408] - -2.0.4 ------------ - -- Reliable push now supports sharding [#2409] -- Reliable push now only catches Redis exceptions [#2307] - -2.0.3 ------------ - -- Display Batch callback data on the Batch details page. [#2347] -- Fix incompatibility with Pro Web and Rack middleware. [#2344] Thank - you to Jason Clark for the tip on how to fix it. - -2.0.2 ------------ - -- Multiple Web UIs can now run in the same process. [#2267] If you have - multiple Redis shards, you can mount UIs for all in the same process: -```ruby -POOL1 = ConnectionPool.new { Redis.new(:url => "redis://localhost:6379/0") } -POOL2 = ConnectionPool.new { Redis.new(:url => "redis://localhost:6378/0") } - -mount Sidekiq::Pro::Web => '/sidekiq' # default -mount Sidekiq::Pro::Web.with(redis_pool: POOL1), at: '/sidekiq1', as: 'sidekiq1' # shard1 -mount Sidekiq::Pro::Web.with(redis_pool: POOL2), at: '/sidekiq2', as: 'sidekiq2' # shard2 -``` -- **SECURITY** Fix batch XSS in error data. Thanks to moneybird.com for - reporting the issue. - -2.0.1 ------------ - -- Add `batch.callback_queue` so batch callbacks can use a higher - priority queue than jobs. [#2200] -- Gracefully recover if someone runs `SCRIPT FLUSH` on Redis. [#2240] -- Ignore errors when attempting `bulk_requeue`, allowing clean shutdown - -2.0.0 ------------ - -- See [the Upgrade Notes](Pro-2.0-Upgrade.md) for detailed notes. - -1.9.2 ------------ - -- As of 1/1/2015, Sidekiq Pro is hosted on a new dedicated server. - Happy new year and let's hope for 100% uptime! -- Fix bug in reliable\_fetch where jobs could be duplicated if a Sidekiq - process crashed and you were using weighted queues. [#2120] - -1.9.1 ------------ - -- **SECURITY** Fix XSS in batch description, thanks to intercom.io for reporting the - issue. If you don't use batch descriptions, you don't need the fix. - -1.9.0 ------------ - -- Add new expiring jobs feature [#1982] -- Show batch expiration on Batch details page [#1981] -- Add '$' batch success token to the pubsub support. [#1953] - - -1.8.0 ------------ - -- Fix race condition where Batches can complete - before they have been fully defined or only half-defined. Requires - Sidekiq 3.2.3. [#1919] - - -1.7.6 ------------ - -- Quick release to verify #1919 - - -1.7.5 ------------ - -- Fix job filtering within the Dead tab. -- Add APIs and wiki documentation for invalidating jobs within a batch. - - -1.7.4 ------------ - -- Awesome ANSI art startup banner! - - -1.7.3 ------------ - -- Batch callbacks should use the same queue as the associated jobs. - -1.7.2 ------------ - -- **DEPRECATION** Use `Batch#on(:complete)` instead of `Batch#notify`. - The specific Campfire, HipChat, email and other notification schemes - will be removed in 2.0.0. -- Remove batch from UI when successful. [#1745] -- Convert batch callbacks to be asynchronous jobs for error handling [#1744] - -1.7.1 ------------ - -- Fix for paused queues being processed for a few seconds when starting - a new Sidekiq process. -- Add a 5 sec delay when starting reliable fetch on Heroku to minimize - any duplicate job processing with another process shutting down. - -1.7.0 ------------ - -- Add ability to pause reliable queues via API. -```ruby -q = Sidekiq::Queue.new("critical") -q.pause! -q.paused? # => true -q.unpause! -``` - -Sidekiq polls Redis every 10 seconds for paused queues so pausing will take -a few seconds to take effect. - -1.6.0 ------------ - -- Compatible with Sidekiq 3. - -1.5.1 ------------ - -- Due to a breaking API change in Sidekiq 3.0, this version is limited - to Sidekiq 2.x. - -1.5.0 ------------ - -- Fix issue on Heroku where reliable fetch could orphan jobs [#1573] - - -1.4.3 ------------ - -- Reverse sorting of Batches in Web UI [#1098] -- Refactoring for Sidekiq 3.0, Pro now requires Sidekiq 2.17.5 - -1.4.2 ------------ - -- Tolerate expired Batches in the web UI. -- Fix 100% CPU usage when using weighted queues and reliable fetch. - -1.4.1 ------------ - -- Add batch progress bar to batch detail page. [#1398] -- Fix race condition in initializing Lua scripts - - -1.4.0 ------------ - -- Default batch expiration has been extended to 3 days, from 1 day previously. -- Batches now sort in the Web UI according to expiry time, not creation time. -- Add user-configurable batch expiry. If your batches might take longer - than 72 hours to process, you can extend the expiration date. - -```ruby -b = Sidekiq::Batch.new -b.expires_in 5.days -... -``` - -1.3.2 ------------ - -- Lazy load Lua scripts so a Redis connection is not required on bootup. - -1.3.1 ------------ - -- Fix a gemspec packaging issue which broke the Batch UI. - -1.3.0 ------------ - -Thanks to @jonhyman for his contributions to this Sidekiq Pro release. - -This release includes new functionality based on the SCAN command newly -added to Redis 2.8. Pro still works with Redis 2.4 but some -functionality will be unavailable. - -- Job Filtering in the Web UI! - You can now filter retries and scheduled jobs in the Web UI so you - only see the jobs relevant to your needs. Queues cannot be filtered; - Redis does not provide the same SCAN operation on the LIST type. - **Redis 2.8** - ![Filtering](https://f.cloud.github.com/assets/2911/1619465/f47529f2-5657-11e3-8cd1-33899eb72aad.png) -- SCAN support in the Sidekiq::SortedSet API. Here's an example that - finds all jobs which contain the substring "Warehouse::OrderShip" - and deletes all matching retries. If the set is large, this API - will be **MUCH** faster than standard iteration using each. - **Redis 2.8** -```ruby - Sidekiq::RetrySet.new.scan("Warehouse::OrderShip") do |job| - job.delete - end -``` - -- Sidekiq::Batch#jobs now returns the set of JIDs added to the batch. -- Sidekiq::Batch#jids returns the complete set of JIDs associated with the batch. -- Sidekiq::Batch#remove\_jobs(jid, jid, ...) removes JIDs from the set, allowing early termination of jobs if they become irrelevant according to application logic. -- Sidekiq::Batch#include?(jid) allows jobs to check if they are still - relevant to a Batch and exit early if not. -- Sidekiq::SortedSet#find\_job(jid) now uses server-side Lua if possible **Redis 2.6** [jonhyman] -- The statsd integration now sets global job counts: -```ruby - jobs.count - jobs.success - jobs.failure -``` - -- Change shutdown logic to push leftover jobs in the private queue back - into the public queue when shutting down with Reliable Fetch. This - allows the safe decommission of a Sidekiq Pro process when autoscaling. [jonhyman] -- Add support for weighted random fetching with Reliable Fetch [jonhyman] -- Pro now requires Sidekiq 2.17.0 - -1.2.5 ------------ - -- Convert Batch UI to use Sidekiq 2.16's support for extension localization. -- Update reliable\_push to work with Sidekiq::Client refactoring in 2.16 -- Pro now requires Sidekiq 2.16.0 - -1.2.4 ------------ - -- Convert Batch UI to Bootstrap 3 -- Pro now requires Sidekiq 2.15.0 -- Add Sidekiq::Batch::Status#delete [#1205] - -1.2.3 ------------ - -- Pro now requires Sidekiq 2.14.0 -- Fix bad exception handling in batch callbacks [#1134] -- Convert Batch UI to ERB - -1.2.2 ------------ - -- Problem with reliable fetch which could lead to lost jobs when Sidekiq - is shut down normally. Thanks to MikaelAmborn for the report. [#1109] - -1.2.1 ------------ - -- Forgot to push paging code necessary for `delete_job` performance. - -1.2.0 ------------ - -- **LEAK** Fix batch key which didn't expire in Redis. Keys match - /b-[a-f0-9]{16}-pending/, e.g. "b-4f55163ddba10aa0-pending" [#1057] -- **Reliable fetch now supports multiple queues**, using the algorithm spec'd - by @jackrg [#1102] -- Fix issue with reliable\_push where it didn't return the JID for a pushed - job when sending previously cached jobs to Redis. -- Add fast Sidekiq::Queue#delete\_job(jid) API which leverages Lua so job lookup is - 100% server-side. Benchmark vs Sidekiq's Job#delete API. **Redis 2.6** - -``` -Sidekiq Pro API - 0.030000 0.020000 0.050000 ( 1.640659) -Sidekiq API - 17.250000 2.220000 19.470000 ( 22.193300) -``` - -- Add fast Sidekiq::Queue#delete\_by\_class(klass) API to remove all - jobs of a given type. Uses server-side Lua for performance. **Redis 2.6** - -1.1.0 ------------ - -- New `sidekiq/pro/reliable_push` which makes Sidekiq::Client resiliant - to Redis network failures. [#793] -- Move `sidekiq/reliable_fetch` to `sidekiq/pro/reliable_fetch` - - -1.0.0 ------------ - -- Sidekiq Pro changelog moved to mperham/sidekiq for public visibility. -- Add new Rack endpoint for easy polling of batch status via JavaScript. See `sidekiq/rack/batch_status` - -0.9.3 ------------ - -- Fix bad /batches path in Web UI -- Fix Sinatra conflict with sidekiq-failures - -0.9.2 ------------ - -- Fix issue with lifecycle notifications not firing. - -0.9.1 ------------ - -- Update due to Sidekiq API changes. - -0.9.0 ------------ - -- Rearchitect Sidekiq's Fetch code to support different fetch -strategies. Add a ReliableFetch strategy which works with Redis' -RPOPLPUSH to ensure we don't lose messages, even when the Sidekiq -process crashes unexpectedly. [mperham/sidekiq#607] - -0.8.2 ------------ - -- Reimplement existing notifications using batch on_complete events. - -0.8.1 ------------ - -- Rejigger batch callback notifications. - - -0.8.0 ------------ - -- Add new Batch 'callback' notification support, for in-process - notification. -- Symbolize option keys passed to Pony [mperham/sidekiq#603] -- Batch no longer requires the Web UI since Web UI usage is optional. - You must require is manually in your Web process: - -```ruby -require 'sidekiq/web' -require 'sidekiq/batch/web' -mount Sidekiq::Web => '/sidekiq' -``` - - -0.7.1 ------------ - -- Worker instances can access the associated jid and bid via simple - accessors. -- Batches can now be modified while being processed so, e.g. a batch - job can add additional jobs to its own batch. - -```ruby -def perform(...) - batch = Sidekiq::Batch.new(bid) # instantiate batch associated with this job - batch.jobs do - SomeWorker.perform_async # add another job - end -end -``` - -- Save error backtraces in batch's failure info for display in Web UI. -- Clean up email notification a bit. - - -0.7.0 ------------ - -- Add optional batch description -- Mutable batches. Batches can now be modified to add additional jobs - at runtime. Example would be a batch job which needs to create more - jobs based on the data it is processing. - -```ruby -batch = Sidekiq::Batch.new(bid) -batch.jobs do - # define more jobs here -end -``` -- Fix issues with symbols vs strings in option hashes - - -0.6.1 ------------ - -- Webhook notification support - - -0.6 ------------ - -- Redis pubsub -- Email polish - - -0.5 ------------ - -- Batches -- Notifications -- Statsd middleware diff --git a/README.md b/README.md index 0acf8f59..79671640 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,54 @@ -Sidekiq +Sidekiq Delay Extensions ============== -[![Gem Version](https://badge.fury.io/rb/sidekiq.svg)](https://rubygems.org/gems/sidekiq) -![Build](https://github.com/mperham/sidekiq/workflows/CI/badge.svg) +[![Gem Version](https://badge.fury.io/rb/sidekiq-delay_extensions.svg)](https://rubygems.org/gems/sidekiq-delay_extensions) +![Build](https://github.com/gehome/sidekiq-delay_extensions/workflows/CI/badge.svg) -Simple, efficient background processing for Ruby. +The [Sidekiq delay extensions are deprecated in 6.x and will be removed from 7.x](https://github.com/mperham/sidekiq/issues/5076). -Sidekiq uses threads to handle many jobs at the same time in the -same process. It does not require Rails but will integrate tightly with -Rails to make background processing dead simple. +This gem extracts the delay extensions from the latest 6.x release and will match +Sidekiq 6.x version numbers. -Performance ---------------- - -Version | Latency | Garbage created for 10k jobs | Time to process 100k jobs | Throughput | Ruby ------------------|------|---------|---------|------------------------|----- -Sidekiq 6.0.2 | 3 ms | 156 MB | 14.0 sec| **7100 jobs/sec** | MRI 2.6.3 -Sidekiq 6.0.0 | 3 ms | 156 MB | 19 sec | 5200 jobs/sec | MRI 2.6.3 -Sidekiq 4.0.0 | 10 ms | 151 MB | 22 sec | 4500 jobs/sec | -Sidekiq 3.5.1 | 22 ms | 1257 MB | 125 sec | 800 jobs/sec | -Resque 1.25.2 | - | - | 420 sec | 240 jobs/sec | -DelayedJob 4.1.1 | - | - | 465 sec | 215 jobs/sec | - -This benchmark can be found in `bin/sidekiqload` and assumes a Redis network latency of 1ms. +When Sidekiq reaches 7.0, this gem will begin being maintained on its own. Maintainers wanted. Requirements ----------------- -- Redis: 4.0+ -- Ruby: MRI 2.5+ or JRuby 9.2+. - -Sidekiq 6.0 supports Rails 5.0+ but does not require it. - +- See https://github.com/mperham/sidekiq/tree/v6.4.1 + - Redis: 4.0+ + - Ruby: MRI 2.5+ or JRuby 9.2+. + - Sidekiq 6.0 supports Rails 5.0+ but does not require it. Installation ----------------- gem install sidekiq + gem install sidekiq-delay_extensions +In your initializers, include the line: -Getting Started ------------------ - -See the [Getting Started wiki page](https://github.com/mperham/sidekiq/wiki/Getting-Started) and follow the simple setup process. -You can watch [this YouTube playlist](https://www.youtube.com/playlist?list=PLjeHh2LSCFrWGT5uVjUuFKAcrcj5kSai1) to learn all about -Sidekiq and see its features in action. Here's the Web UI: - -![Web UI](https://github.com/mperham/sidekiq/raw/main/examples/web-ui.png) - + Sidekiq::DelayExtensions.enable_delay! -Want to Upgrade? -------------------- - -I also sell Sidekiq Pro and Sidekiq Enterprise, extensions to Sidekiq which provide more -features, a commercial-friendly license and allow you to support high -quality open source development all at the same time. Please see the -[Sidekiq](https://sidekiq.org/) homepage for more detail. - -Subscribe to the **[quarterly newsletter](https://tinyletter.com/sidekiq)** to stay informed about the latest -features and changes to Sidekiq and its bigger siblings. - - -Problems? +Testing ----------------- -**Please do not directly email any Sidekiq committers with questions or problems.** A community is best served when discussions are held in public. - -If you have a problem, please review the [FAQ](https://github.com/mperham/sidekiq/wiki/FAQ) and [Troubleshooting](https://github.com/mperham/sidekiq/wiki/Problems-and-Troubleshooting) wiki pages. -Searching the [issues](https://github.com/mperham/sidekiq/issues) for your problem is also a good idea. - -Sidekiq Pro and Sidekiq Enterprise customers get private email support. You can purchase at https://sidekiq.org; email support@contribsys.com for help. - -Useful resources: - -* Product documentation is in the [wiki](https://github.com/mperham/sidekiq/wiki). -* Occasional announcements are made to the [@sidekiq](https://twitter.com/sidekiq) Twitter account. -* The [Sidekiq tag](https://stackoverflow.com/questions/tagged/sidekiq) on Stack Overflow has lots of useful Q & A. +In your test environment, include the line: -Every Friday morning is Sidekiq happy hour: I video chat and answer questions. -See the [Sidekiq support page](https://sidekiq.org/support.html) for details. + require "sidekiq/delay_extensions/testing" Contributing ----------------- -Please see [the contributing guidelines](https://github.com/mperham/sidekiq/blob/main/.github/contributing.md). +Please see [the contributing guidelines](https://github.com/gemhome/sidekiq-delay_extensions/blob/main/.github/contributing.md). License ----------------- -Please see [LICENSE](https://github.com/mperham/sidekiq/blob/main/LICENSE) for licensing details. +Please see [LICENSE](https://github.com/gemhome/sidekiq-delay_extensions/blob/main/LICENSE) for licensing details. -Author +Original Author ----------------- Mike Perham, [@getajobmike](https://twitter.com/getajobmike) / [@sidekiq](https://twitter.com/sidekiq), [https://www.mikeperham.com](https://www.mikeperham.com) / [https://www.contribsys.com](https://www.contribsys.com) diff --git a/bin/sidekiq b/bin/sidekiq deleted file mode 100755 index 57fd2284..00000000 --- a/bin/sidekiq +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env ruby - -# Quiet some warnings we see when running in warning mode: -# RUBYOPT=-w bundle exec sidekiq -$TESTING = false - -require_relative "../lib/sidekiq/cli" - -def integrate_with_systemd - return unless ENV["NOTIFY_SOCKET"] - - Sidekiq.configure_server do |config| - Sidekiq.logger.info "Enabling systemd notification integration" - require "sidekiq/sd_notify" - config.on(:startup) do - Sidekiq::SdNotify.ready - end - config.on(:shutdown) do - Sidekiq::SdNotify.stopping - end - Sidekiq.start_watchdog if Sidekiq::SdNotify.watchdog? - end -end - -begin - cli = Sidekiq::CLI.instance - cli.parse - - integrate_with_systemd - - cli.run -rescue => e - raise e if $DEBUG - if Sidekiq.error_handlers.length == 0 - warn e.message - warn e.backtrace.join("\n") - else - cli.handle_exception e - end - - exit 1 -end diff --git a/bin/sidekiqload b/bin/sidekiqload deleted file mode 100755 index 026c2903..00000000 --- a/bin/sidekiqload +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env ruby - -# Quiet some warnings we see when running in warning mode: -# RUBYOPT=-w bundle exec sidekiq -$TESTING = false - -# require "ruby-prof" -require "bundler/setup" -Bundler.require(:default, :load_test) - -require_relative "../lib/sidekiq/cli" -require_relative "../lib/sidekiq/launcher" - -Sidekiq.configure_server do |config| - config.options[:concurrency] = 10 - config.redis = {db: 13, port: 6380} - # config.redis = { db: 13, port: 6380, driver: :hiredis} - config.options[:queues] << "default" - config.logger.level = Logger::ERROR - config.average_scheduled_poll_interval = 2 - config.reliable! if defined?(Sidekiq::Pro) -end - -class LoadWorker - include Sidekiq::Worker - sidekiq_options retry: 1 - sidekiq_retry_in do |x| - 1 - end - - def perform(idx, ts = nil) - puts(Time.now.to_f - ts) if !ts.nil? - # raise idx.to_s if idx % 100 == 1 - end -end - -# brew tap shopify/shopify -# brew install toxiproxy -# gem install toxiproxy -# run `toxiproxy-server` in a separate terminal window. -require "toxiproxy" -# simulate a non-localhost network for realer-world conditions. -# adding 1ms of network latency has an ENORMOUS impact on benchmarks -Toxiproxy.populate([{ - name: "redis", - listen: "127.0.0.1:6380", - upstream: "127.0.0.1:6379" -}]) - -self_read, self_write = IO.pipe -%w[INT TERM TSTP TTIN].each do |sig| - trap sig do - self_write.puts(sig) - end -rescue ArgumentError - puts "Signal #{sig} not supported" -end - -Sidekiq.redis { |c| c.flushdb } -def handle_signal(launcher, sig) - Sidekiq.logger.debug "Got #{sig} signal" - case sig - when "INT" - # Handle Ctrl-C in JRuby like MRI - # http://jira.codehaus.org/browse/JRUBY-4637 - raise Interrupt - when "TERM" - # Heroku sends TERM and then waits 30 seconds for process to exit. - raise Interrupt - when "TSTP" - Sidekiq.logger.info "Received TSTP, no longer accepting new work" - launcher.quiet - when "TTIN" - Thread.list.each do |thread| - Sidekiq.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread["label"]}" - if thread.backtrace - Sidekiq.logger.warn thread.backtrace.join("\n") - else - Sidekiq.logger.warn "" - end - end - end -end - -def Process.rss - `ps -o rss= -p #{Process.pid}`.chomp.to_i -end - -iter = 50 -count = 10_000 - -iter.times do - arr = Array.new(count) do - [] - end - count.times do |idx| - arr[idx][0] = idx - end - Sidekiq::Client.push_bulk("class" => LoadWorker, "args" => arr) -end -Sidekiq.logger.error "Created #{count * iter} jobs" - -start = Time.now - -Monitoring = Thread.new do - while true - sleep 0.2 - qsize = Sidekiq.redis do |conn| - conn.llen "queue:default" - end - total = qsize - # Sidekiq.logger.error("RSS: #{Process.rss} Pending: #{total}") - if total == 0 - Sidekiq.logger.error("Done, #{iter * count} jobs in #{Time.now - start} sec") - Sidekiq.logger.error("Now here's the latency for three jobs") - - LoadWorker.perform_async(1, Time.now.to_f) - LoadWorker.perform_async(2, Time.now.to_f) - LoadWorker.perform_async(3, Time.now.to_f) - - sleep 0.2 - exit(0) - end - end -end - -begin - # RubyProf::exclude_threads = [ Monitoring ] - # RubyProf.start - events = Sidekiq.options[:lifecycle_events][:startup] - events.each(&:call) - events.clear - - Sidekiq.logger.error "Simulating 1ms of latency between Sidekiq and redis" - Toxiproxy[:redis].downstream(:latency, latency: 1).apply do - launcher = Sidekiq::Launcher.new(Sidekiq.options) - launcher.run - - while readable_io = IO.select([self_read]) - signal = readable_io.first[0].gets.strip - handle_signal(launcher, signal) - end - end -rescue SystemExit => e - # Sidekiq.logger.error("Profiling...") - # result = RubyProf.stop - # printer = RubyProf::GraphHtmlPrinter.new(result) - # printer.print(File.new("output.html", "w"), :min_percent => 1) - # normal -rescue => e - raise e if $DEBUG - warn e.message - warn e.backtrace.join("\n") - exit 1 -end diff --git a/bin/sidekiqmon b/bin/sidekiqmon deleted file mode 100755 index ca931948..00000000 --- a/bin/sidekiqmon +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby - -require "sidekiq/monitor" - -section = "all" -section = ARGV[0] if ARGV.size == 1 - -Sidekiq::Monitor::Status.new.display(section) diff --git a/examples/busy-ui.png b/examples/busy-ui.png deleted file mode 100644 index 9ffe03dd..00000000 Binary files a/examples/busy-ui.png and /dev/null differ diff --git a/examples/complex_batch_workflow.png b/examples/complex_batch_workflow.png deleted file mode 100644 index f2a234d5..00000000 Binary files a/examples/complex_batch_workflow.png and /dev/null differ diff --git a/examples/complex_batch_workflow.svg b/examples/complex_batch_workflow.svg deleted file mode 100644 index b62d849e..00000000 --- a/examples/complex_batch_workflow.svg +++ /dev/null @@ -1 +0,0 @@ -
A
A
B
B
C
C
D
D
E
E
F
F
G
G
H
H
I
I
J
J
K
K
L
L
M
M
diff --git a/examples/config.yml b/examples/config.yml deleted file mode 100644 index d5924ea2..00000000 --- a/examples/config.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Sample configuration file for Sidekiq. -# Options here can still be overridden by cmd line args. -# Place this file at config/sidekiq.yml and Sidekiq will -# pick it up automatically. ---- -:verbose: false -:concurrency: 10 -:timeout: 25 - -# Sidekiq will run this file through ERB when reading it so you can -# even put in dynamic logic, like a host-specific queue. -# http://www.mikeperham.com/2013/11/13/advanced-sidekiq-host-specific-queues/ -:queues: - - critical - - default - - <%= `hostname`.strip %> - - low - -# you can override concurrency based on environment -production: - :concurrency: 25 -staging: - :concurrency: 15 diff --git a/examples/ent-bucket.png b/examples/ent-bucket.png deleted file mode 100644 index 7cc6e872..00000000 Binary files a/examples/ent-bucket.png and /dev/null differ diff --git a/examples/ent-concurrent.png b/examples/ent-concurrent.png deleted file mode 100644 index 6ec80174..00000000 Binary files a/examples/ent-concurrent.png and /dev/null differ diff --git a/examples/ent-periodic.png b/examples/ent-periodic.png deleted file mode 100644 index a5bb7641..00000000 Binary files a/examples/ent-periodic.png and /dev/null differ diff --git a/examples/por.rb b/examples/por.rb deleted file mode 100644 index 724ab64a..00000000 --- a/examples/por.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'sidekiq' - -# Start up sidekiq via -# ./bin/sidekiq -r ./examples/por.rb -# and then you can open up an IRB session like so: -# irb -r ./examples/por.rb -# where you can then say -# PlainOldRuby.perform_async "like a dog", 3 -# -class PlainOldRuby - include Sidekiq::Worker - - def perform(how_hard="super hard", how_long=1) - sleep how_long - puts "Workin' #{how_hard}" - end -end diff --git a/examples/systemd/sidekiq.service b/examples/systemd/sidekiq.service deleted file mode 100644 index a0848657..00000000 --- a/examples/systemd/sidekiq.service +++ /dev/null @@ -1,89 +0,0 @@ -# -# This file tells systemd how to run Sidekiq as a 24/7 long-running daemon. -# -# Customize this file based on your bundler location, app directory, etc. -# -# If you are going to run this as a user service (or you are going to use capistrano-sidekiq) -# Customize and copy this to ~/.config/systemd/user -# Then run: -# - systemctl --user enable sidekiq -# - systemctl --user {start,stop,restart} sidekiq -# -# If you are going to run this as a system service -# Customize and copy this into /usr/lib/systemd/system (CentOS) or /lib/systemd/system (Ubuntu). -# Then run: -# - systemctl enable sidekiq -# - systemctl {start,stop,restart} sidekiq -# -# This file corresponds to a single Sidekiq process. Add multiple copies -# to run multiple processes (sidekiq-1, sidekiq-2, etc). -# -# Use `journalctl -u sidekiq -rn 100` to view the last 100 lines of log output. -# -[Unit] -Description=sidekiq -# start us only once the network and logging subsystems are available, -# consider adding redis-server.service if Redis is local and systemd-managed. -After=syslog.target network.target - -# See these pages for lots of options: -# -# https://www.freedesktop.org/software/systemd/man/systemd.service.html -# https://www.freedesktop.org/software/systemd/man/systemd.exec.html -# -# THOSE PAGES ARE CRITICAL FOR ANY LINUX DEVOPS WORK; read them multiple -# times! systemd is a critical tool for all developers to know and understand. -# -[Service] -# -# !!!! !!!! !!!! -# -# As of v6.0.6, Sidekiq automatically supports systemd's `Type=notify` and watchdog service -# monitoring. If you are using an earlier version of Sidekiq, change this to `Type=simple` -# and remove the `WatchdogSec` line. -# -# !!!! !!!! !!!! -# -Type=notify -# If your Sidekiq process locks up, systemd's watchdog will restart it within seconds. -WatchdogSec=10 - -WorkingDirectory=/opt/myapp/current -# If you use rbenv: -# ExecStart=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec sidekiq -e production' -# If you use the system's ruby: -# ExecStart=/usr/local/bin/bundle exec sidekiq -e production -# If you use rvm in production without gemset and your ruby version is 2.6.5 -# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5/wrappers/bundle exec sidekiq -e production -# If you use rvm in production with gemset and your ruby version is 2.6.5 -# ExecStart=/home/deploy/.rvm/gems/ruby-2.6.5@gemset-name/wrappers/bundle exec sidekiq -e production -# If you use rvm in production with gemset and ruby version/gemset is specified in .ruby-version, -# .ruby-gemsetor or .rvmrc file in the working directory -ExecStart=/home/deploy/.rvm/bin/rvm in /opt/myapp/current do bundle exec sidekiq -e production - -# Use `systemctl kill -s TSTP sidekiq` to quiet the Sidekiq process - -# Uncomment this if you are going to use this as a system service -# if using as a user service then leave commented out, or you will get an error trying to start the service -# !!! Change this to your deploy user account if you are using this as a system service !!! -# User=deploy -# Group=deploy -# UMask=0002 - -# Greatly reduce Ruby memory fragmentation and heap usage -# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/ -Environment=MALLOC_ARENA_MAX=2 - -# if we crash, restart -RestartSec=1 -Restart=on-failure - -# output goes to /var/log/syslog (Ubuntu) or /var/log/messages (CentOS) -StandardOutput=syslog -StandardError=syslog - -# This will default to "bundler" if we don't specify it -SyslogIdentifier=sidekiq - -[Install] -WantedBy=multi-user.target diff --git a/examples/upstart/sidekiq.conf b/examples/upstart/sidekiq.conf deleted file mode 100644 index 79f8f098..00000000 --- a/examples/upstart/sidekiq.conf +++ /dev/null @@ -1,75 +0,0 @@ -# /etc/init/sidekiq.conf - Sidekiq config - -# This example config should work with Ubuntu 12.04+. It -# allows you to manage multiple Sidekiq instances with -# Upstart, Ubuntu's native service management tool. -# -# See workers.conf for how to manage all Sidekiq instances at once. -# -# Save this config as /etc/init/sidekiq.conf then manage sidekiq with: -# sudo start sidekiq index=0 -# sudo stop sidekiq index=0 -# sudo status sidekiq index=0 -# -# Hack Upstart's reload command to 'quiet' Sidekiq: -# -# sudo reload sidekiq index=0 -# -# or use the service command: -# sudo service sidekiq {start,stop,restart,status} -# - -description "Sidekiq Background Worker" - -# This script is not meant to start on bootup, workers.conf -# will start all sidekiq instances explicitly when it starts. -#start on runlevel [2345] -#stop on runlevel [06] - -# change to match your deployment user -# setuid deploy -# setgid deploy -# env HOME=/home/deploy - -# Greatly reduce Ruby memory fragmentation and heap usage -# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/ -env MALLOC_ARENA_MAX=2 - -respawn -respawn limit 3 30 - -# TERM is used when stopping sidekiq. Without declaring these as -# normal exit codes, it just respawns. -normal exit 0 TERM - -# Older versions of Upstart might not support the reload command and need -# this commented out. -reload signal TSTP - -# Upstart waits 5 seconds by default to kill a process. Increase timeout to -# give sidekiq process enough time to exit. -kill timeout 30 - -instance $index - -script -# this script runs in /bin/sh by default -# respawn as bash so we can source in rbenv -exec /bin/bash <<'EOT' - # Pick your poison :) Or none if you're using a system wide installed Ruby. - # rbenv - # source /home/apps/.bash_profile - # OR - # source /home/apps/.profile - # OR system: - # source /etc/profile.d/rbenv.sh - # - # rvm - # source /home/apps/.rvm/scripts/rvm - - # Logs out to /var/log/upstart/sidekiq.log by default - - cd /var/www/app - exec bundle exec sidekiq -e production -EOT -end script diff --git a/examples/upstart/workers.conf b/examples/upstart/workers.conf deleted file mode 100644 index a6eb57d8..00000000 --- a/examples/upstart/workers.conf +++ /dev/null @@ -1,37 +0,0 @@ -# /etc/init/workers.conf - manage a set of Sidekiqs - -# This example config should work with Ubuntu 14.10 and below It -# allows you to manage multiple Sidekiq instances with -# Upstart, Ubuntu's native service management tool. -# -# See sidekiq.conf for how to manage a single Sidekiq instance. -# -# Use "stop workers" to stop all Sidekiq instances. -# Use "start workers" to start all instances. -# Use "restart workers" to restart all instances. -# Crazy, right? -# - -description "manages the set of sidekiq processes" - -# This starts upon bootup and stops on shutdown -start on runlevel [2345] -stop on runlevel [06] - -# Set this to the number of Sidekiq processes you want -# to run on this machine -env NUM_WORKERS=2 - -pre-start script - for i in `seq 1 ${NUM_WORKERS}` - do - start sidekiq index=$i - done -end script - -post-stop script - for i in `seq 1 ${NUM_WORKERS}` - do - stop sidekiq index=$i - done -end script diff --git a/examples/web-ui.png b/examples/web-ui.png deleted file mode 100644 index 1104754d..00000000 Binary files a/examples/web-ui.png and /dev/null differ diff --git a/lib/generators/sidekiq/job_generator.rb b/lib/generators/sidekiq/job_generator.rb deleted file mode 100644 index dcf7dd59..00000000 --- a/lib/generators/sidekiq/job_generator.rb +++ /dev/null @@ -1,57 +0,0 @@ -require "rails/generators/named_base" - -module Sidekiq - module Generators # :nodoc: - class JobGenerator < ::Rails::Generators::NamedBase # :nodoc: - desc "This generator creates a Sidekiq Job in app/sidekiq and a corresponding test" - - check_class_collision suffix: "Job" - - def self.default_generator_root - File.dirname(__FILE__) - end - - def create_job_file - template "job.rb.erb", File.join("app/sidekiq", class_path, "#{file_name}_job.rb") - end - - def create_test_file - return unless test_framework - - if test_framework == :rspec - create_job_spec - else - create_job_test - end - end - - private - - def create_job_spec - template_file = File.join( - "spec/sidekiq", - class_path, - "#{file_name}_job_spec.rb" - ) - template "job_spec.rb.erb", template_file - end - - def create_job_test - template_file = File.join( - "test/sidekiq", - class_path, - "#{file_name}_job_test.rb" - ) - template "job_test.rb.erb", template_file - end - - def file_name - @_file_name ||= super.sub(/_?job\z/i, "") - end - - def test_framework - ::Rails.application.config.generators.options[:rails][:test_framework] - end - end - end -end diff --git a/lib/generators/sidekiq/templates/job.rb.erb b/lib/generators/sidekiq/templates/job.rb.erb deleted file mode 100644 index f96a01c8..00000000 --- a/lib/generators/sidekiq/templates/job.rb.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %>Job - include Sidekiq::Job - - def perform(*args) - # Do something - end -end -<% end -%> \ No newline at end of file diff --git a/lib/generators/sidekiq/templates/job_spec.rb.erb b/lib/generators/sidekiq/templates/job_spec.rb.erb deleted file mode 100644 index 161f5d87..00000000 --- a/lib/generators/sidekiq/templates/job_spec.rb.erb +++ /dev/null @@ -1,6 +0,0 @@ -require 'rails_helper' -<% module_namespacing do -%> -RSpec.describe <%= class_name %>Job, type: :job do - pending "add some examples to (or delete) #{__FILE__}" -end -<% end -%> diff --git a/lib/generators/sidekiq/templates/job_test.rb.erb b/lib/generators/sidekiq/templates/job_test.rb.erb deleted file mode 100644 index 4cd8bc83..00000000 --- a/lib/generators/sidekiq/templates/job_test.rb.erb +++ /dev/null @@ -1,8 +0,0 @@ -require 'test_helper' -<% module_namespacing do -%> -class <%= class_name %>JobTest < Minitest::Test - def test_example - skip "add some examples to (or delete) #{__FILE__}" - end -end -<% end -%> diff --git a/lib/sidekiq.rb b/lib/sidekiq.rb deleted file mode 100644 index 76113a66..00000000 --- a/lib/sidekiq.rb +++ /dev/null @@ -1,270 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq/version" -fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.5.0." if RUBY_PLATFORM != "java" && Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5.0") - -require "sidekiq/logger" -require "sidekiq/client" -require "sidekiq/worker" -require "sidekiq/job" -require "sidekiq/redis_connection" -require "sidekiq/delay" - -require "json" - -module Sidekiq - NAME = "Sidekiq" - LICENSE = "See LICENSE and the LGPL-3.0 for licensing details." - - DEFAULTS = { - queues: [], - labels: [], - concurrency: 10, - require: ".", - strict: true, - environment: nil, - timeout: 25, - poll_interval_average: nil, - average_scheduled_poll_interval: 5, - on_complex_arguments: :warn, - error_handlers: [], - death_handlers: [], - lifecycle_events: { - startup: [], - quiet: [], - shutdown: [], - heartbeat: [] - }, - dead_max_jobs: 10_000, - dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months - reloader: proc { |&block| block.call } - } - - DEFAULT_WORKER_OPTIONS = { - "retry" => true, - "queue" => "default" - } - - FAKE_INFO = { - "redis_version" => "9.9.9", - "uptime_in_days" => "9999", - "connected_clients" => "9999", - "used_memory_human" => "9P", - "used_memory_peak_human" => "9P" - } - - def self.❨╯°□°❩╯︵┻━┻ - puts "Calm down, yo." - end - - def self.options - @options ||= DEFAULTS.dup - end - - def self.options=(opts) - @options = opts - end - - ## - # Configuration for Sidekiq server, use like: - # - # Sidekiq.configure_server do |config| - # config.redis = { :namespace => 'myapp', :size => 25, :url => 'redis://myhost:8877/0' } - # config.server_middleware do |chain| - # chain.add MyServerHook - # end - # end - def self.configure_server - yield self if server? - end - - ## - # Configuration for Sidekiq client, use like: - # - # Sidekiq.configure_client do |config| - # config.redis = { :namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/0' } - # end - def self.configure_client - yield self unless server? - end - - def self.server? - defined?(Sidekiq::CLI) - end - - def self.redis - raise ArgumentError, "requires a block" unless block_given? - redis_pool.with do |conn| - retryable = true - begin - yield conn - rescue Redis::BaseError => ex - # 2550 Failover can cause the server to become a replica, need - # to disconnect and reopen the socket to get back to the primary. - # 4495 Use the same logic if we have a "Not enough replicas" error from the primary - # 4985 Use the same logic when a blocking command is force-unblocked - # The same retry logic is also used in client.rb - if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/ - conn.disconnect! - retryable = false - retry - end - raise - end - end - end - - def self.redis_info - redis do |conn| - # admin commands can't go through redis-namespace starting - # in redis-namespace 2.0 - if conn.respond_to?(:namespace) - conn.redis.info - else - conn.info - end - rescue Redis::CommandError => ex - # 2850 return fake version when INFO command has (probably) been renamed - raise unless /unknown command/.match?(ex.message) - FAKE_INFO - end - end - - def self.redis_pool - @redis ||= Sidekiq::RedisConnection.create - end - - def self.redis=(hash) - @redis = if hash.is_a?(ConnectionPool) - hash - else - Sidekiq::RedisConnection.create(hash) - end - end - - def self.client_middleware - @client_chain ||= Middleware::Chain.new - yield @client_chain if block_given? - @client_chain - end - - def self.server_middleware - @server_chain ||= default_server_middleware - yield @server_chain if block_given? - @server_chain - end - - def self.default_server_middleware - Middleware::Chain.new - end - - def self.default_worker_options=(hash) - # stringify - @default_worker_options = default_worker_options.merge(hash.transform_keys(&:to_s)) - end - - def self.default_worker_options - defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS - end - - ## - # Death handlers are called when all retries for a job have been exhausted and - # the job dies. It's the notification to your application - # that this job will not succeed without manual intervention. - # - # Sidekiq.configure_server do |config| - # config.death_handlers << ->(job, ex) do - # end - # end - def self.death_handlers - options[:death_handlers] - end - - def self.load_json(string) - JSON.parse(string) - end - - def self.dump_json(object) - JSON.generate(object) - end - - def self.log_formatter - @log_formatter ||= if ENV["DYNO"] - Sidekiq::Logger::Formatters::WithoutTimestamp.new - else - Sidekiq::Logger::Formatters::Pretty.new - end - end - - def self.log_formatter=(log_formatter) - @log_formatter = log_formatter - logger.formatter = log_formatter - end - - def self.logger - @logger ||= Sidekiq::Logger.new($stdout, level: Logger::INFO) - end - - def self.logger=(logger) - if logger.nil? - self.logger.level = Logger::FATAL - return self.logger - end - - logger.extend(Sidekiq::LoggingUtils) - - @logger = logger - end - - def self.pro? - defined?(Sidekiq::Pro) - end - - # How frequently Redis should be checked by a random Sidekiq process for - # scheduled and retriable jobs. Each individual process will take turns by - # waiting some multiple of this value. - # - # See sidekiq/scheduled.rb for an in-depth explanation of this value - def self.average_scheduled_poll_interval=(interval) - options[:average_scheduled_poll_interval] = interval - end - - # Register a proc to handle any error which occurs within the Sidekiq process. - # - # Sidekiq.configure_server do |config| - # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) } - # end - # - # The default error handler logs errors to Sidekiq.logger. - def self.error_handlers - options[:error_handlers] - end - - # Register a block to run at a point in the Sidekiq lifecycle. - # :startup, :quiet or :shutdown are valid events. - # - # Sidekiq.configure_server do |config| - # config.on(:shutdown) do - # puts "Goodbye cruel world!" - # end - # end - def self.on(event, &block) - raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol) - raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event) - options[:lifecycle_events][event] << block - end - - def self.strict_args!(mode = :raise) - options[:on_complex_arguments] = mode - end - - # We are shutting down Sidekiq but what about workers that - # are working on some long job? This error is - # raised in workers that have not finished within the hard - # timeout limit. This is needed to rollback db transactions, - # otherwise Ruby's Thread#kill will commit. See #377. - # DO NOT RESCUE THIS ERROR IN YOUR WORKERS - class Shutdown < Interrupt; end -end - -require "sidekiq/rails" if defined?(::Rails::Engine) diff --git a/lib/sidekiq/api.rb b/lib/sidekiq/api.rb deleted file mode 100644 index 2076f899..00000000 --- a/lib/sidekiq/api.rb +++ /dev/null @@ -1,1008 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq" - -require "zlib" -require "base64" - -module Sidekiq - class Stats - def initialize - fetch_stats_fast! - end - - def processed - stat :processed - end - - def failed - stat :failed - end - - def scheduled_size - stat :scheduled_size - end - - def retry_size - stat :retry_size - end - - def dead_size - stat :dead_size - end - - def enqueued - stat :enqueued - end - - def processes_size - stat :processes_size - end - - def workers_size - stat :workers_size - end - - def default_queue_latency - stat :default_queue_latency - end - - def queues - Sidekiq::Stats::Queues.new.lengths - end - - # O(1) redis calls - def fetch_stats_fast! - pipe1_res = Sidekiq.redis { |conn| - conn.pipelined do |pipeline| - pipeline.get("stat:processed") - pipeline.get("stat:failed") - pipeline.zcard("schedule") - pipeline.zcard("retry") - pipeline.zcard("dead") - pipeline.scard("processes") - pipeline.lrange("queue:default", -1, -1) - end - } - - default_queue_latency = if (entry = pipe1_res[6].first) - job = begin - Sidekiq.load_json(entry) - rescue - {} - end - now = Time.now.to_f - thence = job["enqueued_at"] || now - now - thence - else - 0 - end - - @stats = { - processed: pipe1_res[0].to_i, - failed: pipe1_res[1].to_i, - scheduled_size: pipe1_res[2], - retry_size: pipe1_res[3], - dead_size: pipe1_res[4], - processes_size: pipe1_res[5], - - default_queue_latency: default_queue_latency - } - end - - # O(number of processes + number of queues) redis calls - def fetch_stats_slow! - processes = Sidekiq.redis { |conn| - conn.sscan_each("processes").to_a - } - - queues = Sidekiq.redis { |conn| - conn.sscan_each("queues").to_a - } - - pipe2_res = Sidekiq.redis { |conn| - conn.pipelined do |pipeline| - processes.each { |key| pipeline.hget(key, "busy") } - queues.each { |queue| pipeline.llen("queue:#{queue}") } - end - } - - s = processes.size - workers_size = pipe2_res[0...s].sum(&:to_i) - enqueued = pipe2_res[s..-1].sum(&:to_i) - - @stats[:workers_size] = workers_size - @stats[:enqueued] = enqueued - @stats - end - - def fetch_stats! - fetch_stats_fast! - fetch_stats_slow! - end - - def reset(*stats) - all = %w[failed processed] - stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s) - - mset_args = [] - stats.each do |stat| - mset_args << "stat:#{stat}" - mset_args << 0 - end - Sidekiq.redis do |conn| - conn.mset(*mset_args) - end - end - - private - - def stat(s) - fetch_stats_slow! if @stats[s].nil? - @stats[s] || raise(ArgumentError, "Unknown stat #{s}") - end - - class Queues - def lengths - Sidekiq.redis do |conn| - queues = conn.sscan_each("queues").to_a - - lengths = conn.pipelined { |pipeline| - queues.each do |queue| - pipeline.llen("queue:#{queue}") - end - } - - array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size } - array_of_arrays.to_h - end - end - end - - class History - def initialize(days_previous, start_date = nil) - # we only store five years of data in Redis - raise ArgumentError if days_previous < 1 || days_previous > (5 * 365) - @days_previous = days_previous - @start_date = start_date || Time.now.utc.to_date - end - - def processed - @processed ||= date_stat_hash("processed") - end - - def failed - @failed ||= date_stat_hash("failed") - end - - private - - def date_stat_hash(stat) - stat_hash = {} - dates = @start_date.downto(@start_date - @days_previous + 1).map { |date| - date.strftime("%Y-%m-%d") - } - - keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" } - - begin - Sidekiq.redis do |conn| - conn.mget(keys).each_with_index do |value, idx| - stat_hash[dates[idx]] = value ? value.to_i : 0 - end - end - rescue Redis::CommandError - # mget will trigger a CROSSSLOT error when run against a Cluster - # TODO Someone want to add Cluster support? - end - - stat_hash - end - end - end - - ## - # Encapsulates a queue within Sidekiq. - # Allows enumeration of all jobs within the queue - # and deletion of jobs. - # - # queue = Sidekiq::Queue.new("mailer") - # queue.each do |job| - # job.klass # => 'MyWorker' - # job.args # => [1, 2, 3] - # job.delete if job.jid == 'abcdef1234567890' - # end - # - class Queue - include Enumerable - - ## - # Return all known queues within Redis. - # - def self.all - Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) } - end - - attr_reader :name - - def initialize(name = "default") - @name = name.to_s - @rname = "queue:#{name}" - end - - def size - Sidekiq.redis { |con| con.llen(@rname) } - end - - # Sidekiq Pro overrides this - def paused? - false - end - - ## - # Calculates this queue's latency, the difference in seconds since the oldest - # job in the queue was enqueued. - # - # @return Float - def latency - entry = Sidekiq.redis { |conn| - conn.lrange(@rname, -1, -1) - }.first - return 0 unless entry - job = Sidekiq.load_json(entry) - now = Time.now.to_f - thence = job["enqueued_at"] || now - now - thence - end - - def each - initial_size = size - deleted_size = 0 - page = 0 - page_size = 50 - - loop do - range_start = page * page_size - deleted_size - range_end = range_start + page_size - 1 - entries = Sidekiq.redis { |conn| - conn.lrange @rname, range_start, range_end - } - break if entries.empty? - page += 1 - entries.each do |entry| - yield JobRecord.new(entry, @name) - end - deleted_size = initial_size - size - end - end - - ## - # Find the job with the given JID within this queue. - # - # This is a slow, inefficient operation. Do not use under - # normal conditions. - def find_job(jid) - detect { |j| j.jid == jid } - end - - def clear - Sidekiq.redis do |conn| - conn.multi do |transaction| - transaction.unlink(@rname) - transaction.srem("queues", name) - end - end - end - alias_method :💣, :clear - end - - ## - # Encapsulates a pending job within a Sidekiq queue or - # sorted set. - # - # The job should be considered immutable but may be - # removed from the queue via JobRecord#delete. - # - class JobRecord - attr_reader :item - attr_reader :value - - def initialize(item, queue_name = nil) - @args = nil - @value = item - @item = item.is_a?(Hash) ? item : parse(item) - @queue = queue_name || @item["queue"] - end - - def parse(item) - Sidekiq.load_json(item) - rescue JSON::ParserError - # If the job payload in Redis is invalid JSON, we'll load - # the item as an empty hash and store the invalid JSON as - # the job 'args' for display in the Web UI. - @invalid = true - @args = [item] - {} - end - - def klass - self["class"] - end - - def display_class - # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI - @klass ||= self["display_class"] || begin - case klass - when /\ASidekiq::Extensions::Delayed/ - safe_load(args[0], klass) do |target, method, _| - "#{target}.#{method}" - end - when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" - job_class = @item["wrapped"] || args[0] - if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob" - # MailerClass#mailer_method - args[0]["arguments"][0..1].join("#") - else - job_class - end - else - klass - end - end - end - - def display_args - # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI - @display_args ||= case klass - when /\ASidekiq::Extensions::Delayed/ - safe_load(args[0], args) do |_, _, arg, kwarg| - if !kwarg || kwarg.empty? - arg - else - [arg, kwarg] - end - end - when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper" - job_args = self["wrapped"] ? args[0]["arguments"] : [] - if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob" - # remove MailerClass, mailer_method and 'deliver_now' - job_args.drop(3) - elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob" - # remove MailerClass, mailer_method and 'deliver_now' - job_args.drop(3).first["args"] - else - job_args - end - else - if self["encrypt"] - # no point in showing 150+ bytes of random garbage - args[-1] = "[encrypted data]" - end - args - end - end - - def args - @args || @item["args"] - end - - def jid - self["jid"] - end - - def enqueued_at - self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil - end - - def created_at - Time.at(self["created_at"] || self["enqueued_at"] || 0).utc - end - - def tags - self["tags"] || [] - end - - def error_backtrace - # Cache nil values - if defined?(@error_backtrace) - @error_backtrace - else - value = self["error_backtrace"] - @error_backtrace = value && uncompress_backtrace(value) - end - end - - attr_reader :queue - - def latency - now = Time.now.to_f - now - (@item["enqueued_at"] || @item["created_at"] || now) - end - - ## - # Remove this job from the queue. - def delete - count = Sidekiq.redis { |conn| - conn.lrem("queue:#{@queue}", 1, @value) - } - count != 0 - end - - def [](name) - # nil will happen if the JSON fails to parse. - # We don't guarantee Sidekiq will work with bad job JSON but we should - # make a best effort to minimize the damage. - @item ? @item[name] : nil - end - - private - - def safe_load(content, default) - yield(*YAML.load(content)) - rescue => ex - # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into - # memory yet so the YAML can't be loaded. - Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development" - default - end - - def uncompress_backtrace(backtrace) - if backtrace.is_a?(Array) - # Handle old jobs with raw Array backtrace format - backtrace - else - decoded = Base64.decode64(backtrace) - uncompressed = Zlib::Inflate.inflate(decoded) - begin - Sidekiq.load_json(uncompressed) - rescue - # Handle old jobs with marshalled backtrace format - # TODO Remove in 7.x - Marshal.load(uncompressed) - end - end - end - end - - class SortedEntry < JobRecord - attr_reader :score - attr_reader :parent - - def initialize(parent, score, item) - super(item) - @score = score - @parent = parent - end - - def at - Time.at(score).utc - end - - def delete - if @value - @parent.delete_by_value(@parent.name, @value) - else - @parent.delete_by_jid(score, jid) - end - end - - def reschedule(at) - Sidekiq.redis do |conn| - conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item)) - end - end - - def add_to_queue - remove_job do |message| - msg = Sidekiq.load_json(message) - Sidekiq::Client.push(msg) - end - end - - def retry - remove_job do |message| - msg = Sidekiq.load_json(message) - msg["retry_count"] -= 1 if msg["retry_count"] - Sidekiq::Client.push(msg) - end - end - - ## - # Place job in the dead set - def kill - remove_job do |message| - DeadSet.new.kill(message) - end - end - - def error? - !!item["error_class"] - end - - private - - def remove_job - Sidekiq.redis do |conn| - results = conn.multi { |transaction| - transaction.zrangebyscore(parent.name, score, score) - transaction.zremrangebyscore(parent.name, score, score) - }.first - - if results.size == 1 - yield results.first - else - # multiple jobs with the same score - # find the one with the right JID and push it - matched, nonmatched = results.partition { |message| - if message.index(jid) - msg = Sidekiq.load_json(message) - msg["jid"] == jid - else - false - end - } - - msg = matched.first - yield msg if msg - - # push the rest back onto the sorted set - conn.multi do |transaction| - nonmatched.each do |message| - transaction.zadd(parent.name, score.to_f.to_s, message) - end - end - end - end - end - end - - class SortedSet - include Enumerable - - attr_reader :name - - def initialize(name) - @name = name - @_size = size - end - - def size - Sidekiq.redis { |c| c.zcard(name) } - end - - def scan(match, count = 100) - return to_enum(:scan, match, count) unless block_given? - - match = "*#{match}*" unless match.include?("*") - Sidekiq.redis do |conn| - conn.zscan_each(name, match: match, count: count) do |entry, score| - yield SortedEntry.new(self, score, entry) - end - end - end - - def clear - Sidekiq.redis do |conn| - conn.unlink(name) - end - end - alias_method :💣, :clear - end - - class JobSet < SortedSet - def schedule(timestamp, message) - Sidekiq.redis do |conn| - conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message)) - end - end - - def each - initial_size = @_size - offset_size = 0 - page = -1 - page_size = 50 - - loop do - range_start = page * page_size + offset_size - range_end = range_start + page_size - 1 - elements = Sidekiq.redis { |conn| - conn.zrange name, range_start, range_end, with_scores: true - } - break if elements.empty? - page -= 1 - elements.reverse_each do |element, score| - yield SortedEntry.new(self, score, element) - end - offset_size = initial_size - @_size - end - end - - ## - # Fetch jobs that match a given time or Range. Job ID is an - # optional second argument. - def fetch(score, jid = nil) - begin_score, end_score = - if score.is_a?(Range) - [score.first, score.last] - else - [score, score] - end - - elements = Sidekiq.redis { |conn| - conn.zrangebyscore(name, begin_score, end_score, with_scores: true) - } - - elements.each_with_object([]) do |element, result| - data, job_score = element - entry = SortedEntry.new(self, job_score, data) - result << entry if jid.nil? || entry.jid == jid - end - end - - ## - # Find the job with the given JID within this sorted set. - # This is a slower O(n) operation. Do not use for app logic. - def find_job(jid) - Sidekiq.redis do |conn| - conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score| - job = JSON.parse(entry) - matched = job["jid"] == jid - return SortedEntry.new(self, score, entry) if matched - end - end - nil - end - - def delete_by_value(name, value) - Sidekiq.redis do |conn| - ret = conn.zrem(name, value) - @_size -= 1 if ret - ret - end - end - - def delete_by_jid(score, jid) - Sidekiq.redis do |conn| - elements = conn.zrangebyscore(name, score, score) - elements.each do |element| - if element.index(jid) - message = Sidekiq.load_json(element) - if message["jid"] == jid - ret = conn.zrem(name, element) - @_size -= 1 if ret - break ret - end - end - end - end - end - - alias_method :delete, :delete_by_jid - end - - ## - # Allows enumeration of scheduled jobs within Sidekiq. - # Based on this, you can search/filter for jobs. Here's an - # example where I'm selecting all jobs of a certain type - # and deleting them from the schedule queue. - # - # r = Sidekiq::ScheduledSet.new - # r.select do |scheduled| - # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' && - # scheduled.args[0] == 'User' && - # scheduled.args[1] == 'setup_new_subscriber' - # end.map(&:delete) - class ScheduledSet < JobSet - def initialize - super "schedule" - end - end - - ## - # Allows enumeration of retries within Sidekiq. - # Based on this, you can search/filter for jobs. Here's an - # example where I'm selecting all jobs of a certain type - # and deleting them from the retry queue. - # - # r = Sidekiq::RetrySet.new - # r.select do |retri| - # retri.klass == 'Sidekiq::Extensions::DelayedClass' && - # retri.args[0] == 'User' && - # retri.args[1] == 'setup_new_subscriber' - # end.map(&:delete) - class RetrySet < JobSet - def initialize - super "retry" - end - - def retry_all - each(&:retry) while size > 0 - end - - def kill_all - each(&:kill) while size > 0 - end - end - - ## - # Allows enumeration of dead jobs within Sidekiq. - # - class DeadSet < JobSet - def initialize - super "dead" - end - - def kill(message, opts = {}) - now = Time.now.to_f - Sidekiq.redis do |conn| - conn.multi do |transaction| - transaction.zadd(name, now.to_s, message) - transaction.zremrangebyscore(name, "-inf", now - self.class.timeout) - transaction.zremrangebyrank(name, 0, - self.class.max_jobs) - end - end - - if opts[:notify_failure] != false - job = Sidekiq.load_json(message) - r = RuntimeError.new("Job killed by API") - r.set_backtrace(caller) - Sidekiq.death_handlers.each do |handle| - handle.call(job, r) - end - end - true - end - - def retry_all - each(&:retry) while size > 0 - end - - def self.max_jobs - Sidekiq.options[:dead_max_jobs] - end - - def self.timeout - Sidekiq.options[:dead_timeout_in_seconds] - end - end - - ## - # Enumerates the set of Sidekiq processes which are actively working - # right now. Each process sends a heartbeat to Redis every 5 seconds - # so this set should be relatively accurate, barring network partitions. - # - # Yields a Sidekiq::Process. - # - class ProcessSet - include Enumerable - - def initialize(clean_plz = true) - cleanup if clean_plz - end - - # Cleans up dead processes recorded in Redis. - # Returns the number of processes cleaned. - def cleanup - count = 0 - Sidekiq.redis do |conn| - procs = conn.sscan_each("processes").to_a.sort - heartbeats = conn.pipelined { |pipeline| - procs.each do |key| - pipeline.hget(key, "info") - end - } - - # the hash named key has an expiry of 60 seconds. - # if it's not found, that means the process has not reported - # in to Redis and probably died. - to_prune = procs.select.with_index { |proc, i| - heartbeats[i].nil? - } - count = conn.srem("processes", to_prune) unless to_prune.empty? - end - count - end - - def each - result = Sidekiq.redis { |conn| - procs = conn.sscan_each("processes").to_a.sort - - # We're making a tradeoff here between consuming more memory instead of - # making more roundtrips to Redis, but if you have hundreds or thousands of workers, - # you'll be happier this way - conn.pipelined do |pipeline| - procs.each do |key| - pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us") - end - end - } - - result.each do |info, busy, at_s, quiet, rss, rtt| - # If a process is stopped between when we query Redis for `procs` and - # when we query for `result`, we will have an item in `result` that is - # composed of `nil` values. - next if info.nil? - - hash = Sidekiq.load_json(info) - yield Process.new(hash.merge("busy" => busy.to_i, - "beat" => at_s.to_f, - "quiet" => quiet, - "rss" => rss.to_i, - "rtt_us" => rtt.to_i)) - end - end - - # This method is not guaranteed accurate since it does not prune the set - # based on current heartbeat. #each does that and ensures the set only - # contains Sidekiq processes which have sent a heartbeat within the last - # 60 seconds. - def size - Sidekiq.redis { |conn| conn.scard("processes") } - end - - # Total number of threads available to execute jobs. - # For Sidekiq Enterprise customers this number (in production) must be - # less than or equal to your licensed concurrency. - def total_concurrency - sum { |x| x["concurrency"].to_i } - end - - def total_rss_in_kb - sum { |x| x["rss"].to_i } - end - alias_method :total_rss, :total_rss_in_kb - - # Returns the identity of the current cluster leader or "" if no leader. - # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq - # or Sidekiq Pro. - def leader - @leader ||= begin - x = Sidekiq.redis { |c| c.get("dear-leader") } - # need a non-falsy value so we can memoize - x ||= "" - x - end - end - end - - # - # Sidekiq::Process represents an active Sidekiq process talking with Redis. - # Each process has a set of attributes which look like this: - # - # { - # 'hostname' => 'app-1.example.com', - # 'started_at' => , - # 'pid' => 12345, - # 'tag' => 'myapp' - # 'concurrency' => 25, - # 'queues' => ['default', 'low'], - # 'busy' => 10, - # 'beat' => , - # 'identity' => , - # } - class Process - def initialize(hash) - @attribs = hash - end - - def tag - self["tag"] - end - - def labels - Array(self["labels"]) - end - - def [](key) - @attribs[key] - end - - def identity - self["identity"] - end - - def queues - self["queues"] - end - - def quiet! - signal("TSTP") - end - - def stop! - signal("TERM") - end - - def dump_threads - signal("TTIN") - end - - def stopping? - self["quiet"] == "true" - end - - private - - def signal(sig) - key = "#{identity}-signals" - Sidekiq.redis do |c| - c.multi do |transaction| - transaction.lpush(key, sig) - transaction.expire(key, 60) - end - end - end - end - - ## - # The WorkSet stores the work being done by this Sidekiq cluster. - # It tracks the process and thread working on each job. - # - # WARNING WARNING WARNING - # - # This is live data that can change every millisecond. - # If you call #size => 5 and then expect #each to be - # called 5 times, you're going to have a bad time. - # - # works = Sidekiq::WorkSet.new - # works.size => 2 - # works.each do |process_id, thread_id, work| - # # process_id is a unique identifier per Sidekiq process - # # thread_id is a unique identifier per thread - # # work is a Hash which looks like: - # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash } - # # run_at is an epoch Integer. - # end - # - class WorkSet - include Enumerable - - def each(&block) - results = [] - Sidekiq.redis do |conn| - procs = conn.sscan_each("processes").to_a - procs.sort.each do |key| - valid, workers = conn.pipelined { |pipeline| - pipeline.exists?(key) - pipeline.hgetall("#{key}:workers") - } - next unless valid - workers.each_pair do |tid, json| - hsh = Sidekiq.load_json(json) - p = hsh["payload"] - # avoid breaking API, this is a side effect of the JSON optimization in #4316 - hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String) - results << [key, tid, hsh] - end - end - end - - results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block) - end - - # Note that #size is only as accurate as Sidekiq's heartbeat, - # which happens every 5 seconds. It is NOT real-time. - # - # Not very efficient if you have lots of Sidekiq - # processes but the alternative is a global counter - # which can easily get out of sync with crashy processes. - def size - Sidekiq.redis do |conn| - procs = conn.sscan_each("processes").to_a - if procs.empty? - 0 - else - conn.pipelined { |pipeline| - procs.each do |key| - pipeline.hget(key, "busy") - end - }.sum(&:to_i) - end - end - end - end - # Since "worker" is a nebulous term, we've deprecated the use of this class name. - # Is "worker" a process, a type of job, a thread? Undefined! - # WorkSet better describes the data. - Workers = WorkSet -end diff --git a/lib/sidekiq/cli.rb b/lib/sidekiq/cli.rb deleted file mode 100644 index eeccb926..00000000 --- a/lib/sidekiq/cli.rb +++ /dev/null @@ -1,427 +0,0 @@ -# frozen_string_literal: true - -$stdout.sync = true - -require "yaml" -require "singleton" -require "optparse" -require "erb" -require "fileutils" - -require "sidekiq" -require "sidekiq/launcher" -require "sidekiq/util" - -module Sidekiq - class CLI - include Util - include Singleton unless $TESTING - - attr_accessor :launcher - attr_accessor :environment - - def parse(args = ARGV) - setup_options(args) - initialize_logger - validate! - end - - def jruby? - defined?(::JRUBY_VERSION) - end - - # Code within this method is not tested because it alters - # global process state irreversibly. PRs which improve the - # test coverage of Sidekiq::CLI are welcomed. - def run(boot_app: true) - boot_application if boot_app - - if environment == "development" && $stdout.tty? && Sidekiq.log_formatter.is_a?(Sidekiq::Logger::Formatters::Pretty) - print_banner - end - logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app? - - self_read, self_write = IO.pipe - sigs = %w[INT TERM TTIN TSTP] - # USR1 and USR2 don't work on the JVM - sigs << "USR2" if Sidekiq.pro? && !jruby? - sigs.each do |sig| - old_handler = Signal.trap(sig) do - if old_handler.respond_to?(:call) - begin - old_handler.call - rescue Exception => exc - # signal handlers can't use Logger so puts only - puts ["Error in #{sig} handler", exc].inspect - end - end - self_write.puts(sig) - end - rescue ArgumentError - puts "Signal #{sig} not supported" - end - - logger.info "Running in #{RUBY_DESCRIPTION}" - logger.info Sidekiq::LICENSE - logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro) - - # touch the connection pool so it is created before we - # fire startup and start multithreading. - info = Sidekiq.redis_info - ver = info["redis_version"] - raise "You are connecting to Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4" - - maxmemory_policy = info["maxmemory_policy"] - if maxmemory_policy != "noeviction" - logger.warn <<~EOM - - - WARNING: Your Redis instance will evict Sidekiq data under heavy load. - The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}'). - See: https://github.com/mperham/sidekiq/wiki/Using-Redis#memory - - EOM - end - - # Since the user can pass us a connection pool explicitly in the initializer, we - # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed. - cursize = Sidekiq.redis_pool.size - needed = Sidekiq.options[:concurrency] + 2 - raise "Your pool of #{cursize} Redis connections is too small, please increase the size to at least #{needed}" if cursize < needed - - # cache process identity - Sidekiq.options[:identity] = identity - - # Touch middleware so it isn't lazy loaded by multiple threads, #3043 - Sidekiq.server_middleware - - # Before this point, the process is initializing with just the main thread. - # Starting here the process will now have multiple threads running. - fire_event(:startup, reverse: false, reraise: true) - - logger.debug { "Client Middleware: #{Sidekiq.client_middleware.map(&:klass).join(", ")}" } - logger.debug { "Server Middleware: #{Sidekiq.server_middleware.map(&:klass).join(", ")}" } - - launch(self_read) - end - - def launch(self_read) - if environment == "development" && $stdout.tty? - logger.info "Starting processing, hit Ctrl-C to stop" - end - - @launcher = Sidekiq::Launcher.new(options) - - begin - launcher.run - - while (readable_io = IO.select([self_read])) - signal = readable_io.first[0].gets.strip - handle_signal(signal) - end - rescue Interrupt - logger.info "Shutting down" - launcher.stop - logger.info "Bye!" - - # Explicitly exit so busy Processor threads won't block process shutdown. - # - # NB: slow at_exit handlers will prevent a timely exit if they take - # a while to run. If Sidekiq is getting here but the process isn't exiting, - # use the TTIN signal to determine where things are stuck. - exit(0) - end - end - - def self.w - "\e[37m" - end - - def self.r - "\e[31m" - end - - def self.b - "\e[30m" - end - - def self.reset - "\e[0m" - end - - def self.banner - %{ - #{w} m, - #{w} `$b - #{w} .ss, $$: .,d$ - #{w} `$$P,d$P' .,md$P"' - #{w} ,$$$$$b#{b}/#{w}md$$$P^' - #{w} .d$$$$$$#{b}/#{w}$$$P' - #{w} $$^' `"#{b}/#{w}$$$' #{r}____ _ _ _ _ - #{w} $: ,$$: #{r} / ___|(_) __| | ___| | _(_) __ _ - #{w} `b :$$ #{r} \\___ \\| |/ _` |/ _ \\ |/ / |/ _` | - #{w} $$: #{r} ___) | | (_| | __/ <| | (_| | - #{w} $$ #{r}|____/|_|\\__,_|\\___|_|\\_\\_|\\__, | - #{w} .d$$ #{r} |_| - #{reset}} - end - - SIGNAL_HANDLERS = { - # Ctrl-C in terminal - "INT" => ->(cli) { raise Interrupt }, - # TERM is the signal that Sidekiq must exit. - # Heroku sends TERM and then waits 30 seconds for process to exit. - "TERM" => ->(cli) { raise Interrupt }, - "TSTP" => ->(cli) { - Sidekiq.logger.info "Received TSTP, no longer accepting new work" - cli.launcher.quiet - }, - "TTIN" => ->(cli) { - Thread.list.each do |thread| - Sidekiq.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}" - if thread.backtrace - Sidekiq.logger.warn thread.backtrace.join("\n") - else - Sidekiq.logger.warn "" - end - end - } - } - UNHANDLED_SIGNAL_HANDLER = ->(cli) { Sidekiq.logger.info "No signal handler registered, ignoring" } - SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER - - def handle_signal(sig) - Sidekiq.logger.debug "Got #{sig} signal" - SIGNAL_HANDLERS[sig].call(self) - end - - private - - def print_banner - puts "\e[31m" - puts Sidekiq::CLI.banner - puts "\e[0m" - end - - def set_environment(cli_env) - # See #984 for discussion. - # APP_ENV is now the preferred ENV term since it is not tech-specific. - # Both Sinatra 2.0+ and Sidekiq support this term. - # RAILS_ENV and RACK_ENV are there for legacy support. - @environment = cli_env || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" - end - - def symbolize_keys_deep!(hash) - hash.keys.each do |k| - symkey = k.respond_to?(:to_sym) ? k.to_sym : k - hash[symkey] = hash.delete k - symbolize_keys_deep! hash[symkey] if hash[symkey].is_a? Hash - end - end - - alias_method :die, :exit - alias_method :☠, :exit - - def setup_options(args) - # parse CLI options - opts = parse_options(args) - - set_environment opts[:environment] - - # check config file presence - if opts[:config_file] - unless File.exist?(opts[:config_file]) - raise ArgumentError, "No such file #{opts[:config_file]}" - end - else - config_dir = if File.directory?(opts[:require].to_s) - File.join(opts[:require], "config") - else - File.join(options[:require], "config") - end - - %w[sidekiq.yml sidekiq.yml.erb].each do |config_file| - path = File.join(config_dir, config_file) - opts[:config_file] ||= path if File.exist?(path) - end - end - - # parse config file options - opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file] - - # set defaults - opts[:queues] = ["default"] if opts[:queues].nil? - opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"] - - # merge with defaults - options.merge!(opts) - end - - def options - Sidekiq.options - end - - def boot_application - ENV["RACK_ENV"] = ENV["RAILS_ENV"] = environment - - if File.directory?(options[:require]) - require "rails" - if ::Rails::VERSION::MAJOR < 5 - raise "Sidekiq no longer supports this version of Rails" - else - require "sidekiq/rails" - require File.expand_path("#{options[:require]}/config/environment.rb") - end - options[:tag] ||= default_tag - else - require options[:require] - end - end - - def default_tag - dir = ::Rails.root - name = File.basename(dir) - prevdir = File.dirname(dir) # Capistrano release directory? - if name.to_i != 0 && prevdir - if File.basename(prevdir) == "releases" - return File.basename(File.dirname(prevdir)) - end - end - name - end - - def validate! - if !File.exist?(options[:require]) || - (File.directory?(options[:require]) && !File.exist?("#{options[:require]}/config/application.rb")) - logger.info "==================================================================" - logger.info " Please point Sidekiq to a Rails application or a Ruby file " - logger.info " to load your worker classes with -r [DIR|FILE]." - logger.info "==================================================================" - logger.info @parser - die(1) - end - - [:concurrency, :timeout].each do |opt| - raise ArgumentError, "#{opt}: #{options[opt]} is not a valid value" if options.key?(opt) && options[opt].to_i <= 0 - end - end - - def parse_options(argv) - opts = {} - @parser = option_parser(opts) - @parser.parse!(argv) - opts - end - - def option_parser(opts) - parser = OptionParser.new { |o| - o.on "-c", "--concurrency INT", "processor threads to use" do |arg| - opts[:concurrency] = Integer(arg) - end - - o.on "-d", "--daemon", "Daemonize process" do |arg| - puts "ERROR: Daemonization mode was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services" - end - - o.on "-e", "--environment ENV", "Application environment" do |arg| - opts[:environment] = arg - end - - o.on "-g", "--tag TAG", "Process tag for procline" do |arg| - opts[:tag] = arg - end - - o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg| - queue, weight = arg.split(",") - parse_queue opts, queue, weight - end - - o.on "-r", "--require [PATH|DIR]", "Location of Rails application with workers or file to require" do |arg| - opts[:require] = arg - end - - o.on "-t", "--timeout NUM", "Shutdown timeout" do |arg| - opts[:timeout] = Integer(arg) - end - - o.on "-v", "--verbose", "Print more verbose output" do |arg| - opts[:verbose] = arg - end - - o.on "-C", "--config PATH", "path to YAML config file" do |arg| - opts[:config_file] = arg - end - - o.on "-L", "--logfile PATH", "path to writable logfile" do |arg| - puts "ERROR: Logfile redirection was removed in Sidekiq 6.0, Sidekiq will only log to STDOUT" - end - - o.on "-P", "--pidfile PATH", "path to pidfile" do |arg| - puts "ERROR: PID file creation was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services" - end - - o.on "-V", "--version", "Print version and exit" do |arg| - puts "Sidekiq #{Sidekiq::VERSION}" - die(0) - end - } - - parser.banner = "sidekiq [options]" - parser.on_tail "-h", "--help", "Show help" do - logger.info parser - die 1 - end - - parser - end - - def initialize_logger - Sidekiq.logger.level = ::Logger::DEBUG if options[:verbose] - end - - def parse_config(path) - erb = ERB.new(File.read(path)) - erb.filename = File.expand_path(path) - opts = load_yaml(erb.result) || {} - - if opts.respond_to? :deep_symbolize_keys! - opts.deep_symbolize_keys! - else - symbolize_keys_deep!(opts) - end - - opts = opts.merge(opts.delete(environment.to_sym) || {}) - opts.delete(:strict) - - parse_queues(opts, opts.delete(:queues) || []) - - opts - end - - def load_yaml(src) - if Psych::VERSION > "4.0" - YAML.safe_load(src, permitted_classes: [Symbol], aliases: true) - else - YAML.load(src) - end - end - - def parse_queues(opts, queues_and_weights) - queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) } - end - - def parse_queue(opts, queue, weight = nil) - opts[:queues] ||= [] - opts[:strict] = true if opts[:strict].nil? - raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue) - [weight.to_i, 1].max.times { opts[:queues] << queue.to_s } - opts[:strict] = false if weight.to_i > 0 - end - - def rails_app? - defined?(::Rails) && ::Rails.respond_to?(:application) - end - end -end - -require "sidekiq/systemd" diff --git a/lib/sidekiq/client.rb b/lib/sidekiq/client.rb deleted file mode 100644 index 74feb56b..00000000 --- a/lib/sidekiq/client.rb +++ /dev/null @@ -1,240 +0,0 @@ -# frozen_string_literal: true - -require "securerandom" -require "sidekiq/middleware/chain" -require "sidekiq/job_util" - -module Sidekiq - class Client - include Sidekiq::JobUtil - - ## - # Define client-side middleware: - # - # client = Sidekiq::Client.new - # client.middleware do |chain| - # chain.use MyClientMiddleware - # end - # client.push('class' => 'SomeWorker', 'args' => [1,2,3]) - # - # All client instances default to the globally-defined - # Sidekiq.client_middleware but you can change as necessary. - # - def middleware(&block) - @chain ||= Sidekiq.client_middleware - if block - @chain = @chain.dup - yield @chain - end - @chain - end - - attr_accessor :redis_pool - - # Sidekiq::Client normally uses the default Redis pool but you may - # pass a custom ConnectionPool if you want to shard your - # Sidekiq jobs across several Redis instances (for scalability - # reasons, e.g.) - # - # Sidekiq::Client.new(ConnectionPool.new { Redis.new }) - # - # Generally this is only needed for very large Sidekiq installs processing - # thousands of jobs per second. I don't recommend sharding unless you - # cannot scale any other way (e.g. splitting your app into smaller apps). - def initialize(redis_pool = nil) - @redis_pool = redis_pool || Thread.current[:sidekiq_via_pool] || Sidekiq.redis_pool - end - - ## - # The main method used to push a job to Redis. Accepts a number of options: - # - # queue - the named queue to use, default 'default' - # class - the worker class to call, required - # args - an array of simple arguments to the perform method, must be JSON-serializable - # at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f) - # retry - whether to retry this job if it fails, default true or an integer number of retries - # backtrace - whether to save any error backtrace, default false - # - # If class is set to the class name, the jobs' options will be based on Sidekiq's default - # worker options. Otherwise, they will be based on the job class's options. - # - # Any options valid for a worker class's sidekiq_options are also available here. - # - # All options must be strings, not symbols. NB: because we are serializing to JSON, all - # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of - # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful. - # - # Returns a unique Job ID. If middleware stops the job, nil will be returned instead. - # - # Example: - # push('queue' => 'my_queue', 'class' => MyWorker, 'args' => ['foo', 1, :bat => 'bar']) - # - def push(item) - normed = normalize_item(item) - payload = process_single(item["class"], normed) - - if payload - raw_push([payload]) - payload["jid"] - end - end - - ## - # Push a large number of jobs to Redis. This method cuts out the redis - # network round trip latency. I wouldn't recommend pushing more than - # 1000 per call but YMMV based on network quality, size of job args, etc. - # A large number of jobs can cause a bit of Redis command processing latency. - # - # Takes the same arguments as #push except that args is expected to be - # an Array of Arrays. All other keys are duplicated for each job. Each job - # is run through the client middleware pipeline and each job gets its own Job ID - # as normal. - # - # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less - # than the number given if the middleware stopped processing for one or more jobs. - def push_bulk(items) - args = items["args"] - raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless args.is_a?(Array) && args.all?(Array) - return [] if args.empty? # no jobs to push - - at = items.delete("at") - raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) }) - raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size - - normed = normalize_item(items) - payloads = args.map.with_index { |job_args, index| - copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12), "enqueued_at" => Time.now.to_f) - copy["at"] = (at.is_a?(Array) ? at[index] : at) if at - - result = process_single(items["class"], copy) - result || nil - }.compact - - raw_push(payloads) unless payloads.empty? - payloads.collect { |payload| payload["jid"] } - end - - # Allows sharding of jobs across any number of Redis instances. All jobs - # defined within the block will use the given Redis connection pool. - # - # pool = ConnectionPool.new { Redis.new } - # Sidekiq::Client.via(pool) do - # SomeWorker.perform_async(1,2,3) - # SomeOtherWorker.perform_async(1,2,3) - # end - # - # Generally this is only needed for very large Sidekiq installs processing - # thousands of jobs per second. I do not recommend sharding unless - # you cannot scale any other way (e.g. splitting your app into smaller apps). - def self.via(pool) - raise ArgumentError, "No pool given" if pool.nil? - current_sidekiq_pool = Thread.current[:sidekiq_via_pool] - Thread.current[:sidekiq_via_pool] = pool - yield - ensure - Thread.current[:sidekiq_via_pool] = current_sidekiq_pool - end - - class << self - def push(item) - new.push(item) - end - - def push_bulk(items) - new.push_bulk(items) - end - - # Resque compatibility helpers. Note all helpers - # should go through Worker#client_push. - # - # Example usage: - # Sidekiq::Client.enqueue(MyWorker, 'foo', 1, :bat => 'bar') - # - # Messages are enqueued to the 'default' queue. - # - def enqueue(klass, *args) - klass.client_push("class" => klass, "args" => args) - end - - # Example usage: - # Sidekiq::Client.enqueue_to(:queue_name, MyWorker, 'foo', 1, :bat => 'bar') - # - def enqueue_to(queue, klass, *args) - klass.client_push("queue" => queue, "class" => klass, "args" => args) - end - - # Example usage: - # Sidekiq::Client.enqueue_to_in(:queue_name, 3.minutes, MyWorker, 'foo', 1, :bat => 'bar') - # - def enqueue_to_in(queue, interval, klass, *args) - int = interval.to_f - now = Time.now.to_f - ts = (int < 1_000_000_000 ? now + int : int) - - item = {"class" => klass, "args" => args, "at" => ts, "queue" => queue} - item.delete("at") if ts <= now - - klass.client_push(item) - end - - # Example usage: - # Sidekiq::Client.enqueue_in(3.minutes, MyWorker, 'foo', 1, :bat => 'bar') - # - def enqueue_in(interval, klass, *args) - klass.perform_in(interval, *args) - end - end - - private - - def raw_push(payloads) - @redis_pool.with do |conn| - retryable = true - begin - conn.pipelined do |pipeline| - atomic_push(pipeline, payloads) - end - rescue Redis::BaseError => ex - # 2550 Failover can cause the server to become a replica, need - # to disconnect and reopen the socket to get back to the primary. - # 4495 Use the same logic if we have a "Not enough replicas" error from the primary - # 4985 Use the same logic when a blocking command is force-unblocked - # The retry logic is copied from sidekiq.rb - if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/ - conn.disconnect! - retryable = false - retry - end - raise - end - end - true - end - - def atomic_push(conn, payloads) - if payloads.first.key?("at") - conn.zadd("schedule", payloads.map { |hash| - at = hash.delete("at").to_s - [at, Sidekiq.dump_json(hash)] - }) - else - queue = payloads.first["queue"] - now = Time.now.to_f - to_push = payloads.map { |entry| - entry["enqueued_at"] = now - Sidekiq.dump_json(entry) - } - conn.sadd("queues", queue) - conn.lpush("queue:#{queue}", to_push) - end - end - - def process_single(worker_class, item) - queue = item["queue"] - - middleware.invoke(worker_class, item, queue, @redis_pool) do - item - end - end - end -end diff --git a/lib/sidekiq/delay.rb b/lib/sidekiq/delay_extensions.rb similarity index 58% rename from lib/sidekiq/delay.rb rename to lib/sidekiq/delay_extensions.rb index 026307bf..07010b24 100644 --- a/lib/sidekiq/delay.rb +++ b/lib/sidekiq/delay_extensions.rb @@ -1,28 +1,31 @@ # frozen_string_literal: true +require "sidekiq" + module Sidekiq - module Extensions + module DelayExtensions def self.enable_delay! - Sidekiq.logger.error "Sidekiq's Delayed Extensions will be removed in Sidekiq 7.0. #{caller(1..1).first}" - if defined?(::ActiveSupport) - require "sidekiq/extensions/active_record" - require "sidekiq/extensions/action_mailer" + require "sidekiq/delay_extensions/active_record" + require "sidekiq/delay_extensions/action_mailer" # Need to patch Psych so it can autoload classes whose names are serialized # in the delayed YAML. - Psych::Visitors::ToRuby.prepend(Sidekiq::Extensions::PsychAutoload) + Psych::Visitors::ToRuby.prepend(Sidekiq::DelayExtensions::PsychAutoload) ActiveSupport.on_load(:active_record) do - include Sidekiq::Extensions::ActiveRecord + include Sidekiq::DelayExtensions::ActiveRecord end ActiveSupport.on_load(:action_mailer) do - extend Sidekiq::Extensions::ActionMailer + extend Sidekiq::DelayExtensions::ActionMailer end end - require "sidekiq/extensions/class_methods" - Module.__send__(:include, Sidekiq::Extensions::Klass) + require "sidekiq/delay_extensions/class_methods" + Module.__send__(:include, Sidekiq::DelayExtensions::Klass) + + require "sidekiq/delay_extensions/api" + Sidekiq::JobRecord.prepend(Sidekiq::DelayExtensions::JobRecord) end module PsychAutoload diff --git a/lib/sidekiq/extensions/action_mailer.rb b/lib/sidekiq/delay_extensions/action_mailer.rb similarity index 95% rename from lib/sidekiq/extensions/action_mailer.rb rename to lib/sidekiq/delay_extensions/action_mailer.rb index 444b82aa..30761d0e 100644 --- a/lib/sidekiq/extensions/action_mailer.rb +++ b/lib/sidekiq/delay_extensions/action_mailer.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require "sidekiq/extensions/generic_proxy" +require "sidekiq/delay_extensions/generic_proxy" module Sidekiq - module Extensions + module DelayExtensions ## # Adds +delay+, +delay_for+ and +delay_until+ methods to ActionMailer to offload arbitrary email # delivery to Sidekiq. diff --git a/lib/sidekiq/extensions/active_record.rb b/lib/sidekiq/delay_extensions/active_record.rb similarity index 94% rename from lib/sidekiq/extensions/active_record.rb rename to lib/sidekiq/delay_extensions/active_record.rb index 5342ed85..94bf03de 100644 --- a/lib/sidekiq/extensions/active_record.rb +++ b/lib/sidekiq/delay_extensions/active_record.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require "sidekiq/extensions/generic_proxy" +require "sidekiq/delay_extensions/generic_proxy" module Sidekiq - module Extensions + module DelayExtensions ## # Adds +delay+, +delay_for+ and +delay_until+ methods to ActiveRecord to offload instance method # execution to Sidekiq. diff --git a/lib/sidekiq/delay_extensions/api.rb b/lib/sidekiq/delay_extensions/api.rb new file mode 100644 index 00000000..0237df8d --- /dev/null +++ b/lib/sidekiq/delay_extensions/api.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "sidekiq/api" + +module Sidekiq + module DelayExtensions + module JobRecord + def display_class + # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI + @klass ||= self["display_class"] || begin + case klass + when /\ASidekiq::DelayExtensions::Delayed/ + safe_load(args[0], klass) do |target, method, _| + "#{target}.#{method}" + end + else + super + end + end + end + + def display_args + # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI + @display_args ||= case klass + when /\ASidekiq::DelayExtensions::Delayed/ + safe_load(args[0], args) do |_, _, arg, kwarg| + if !kwarg || kwarg.empty? + arg + else + [arg, kwarg] + end + end + else + super + end + end + end + end +end diff --git a/lib/sidekiq/extensions/class_methods.rb b/lib/sidekiq/delay_extensions/class_methods.rb similarity index 87% rename from lib/sidekiq/extensions/class_methods.rb rename to lib/sidekiq/delay_extensions/class_methods.rb index 1723b6f8..f40b7f57 100644 --- a/lib/sidekiq/extensions/class_methods.rb +++ b/lib/sidekiq/delay_extensions/class_methods.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require "sidekiq/extensions/generic_proxy" +require "sidekiq/delay_extensions/generic_proxy" module Sidekiq - module Extensions + module DelayExtensions ## # Adds `delay`, `delay_for` and `delay_until` methods to all Classes to offload class method # execution to Sidekiq. @@ -40,4 +40,4 @@ def sidekiq_delay_until(timestamp, options = {}) end end -Module.__send__(:include, Sidekiq::Extensions::Klass) unless defined?(::Rails) +Module.__send__(:include, Sidekiq::DelayExtensions::Klass) unless defined?(::Rails) diff --git a/lib/sidekiq/extensions/generic_proxy.rb b/lib/sidekiq/delay_extensions/generic_proxy.rb similarity index 97% rename from lib/sidekiq/extensions/generic_proxy.rb rename to lib/sidekiq/delay_extensions/generic_proxy.rb index 313dd48c..47d6c4ae 100644 --- a/lib/sidekiq/extensions/generic_proxy.rb +++ b/lib/sidekiq/delay_extensions/generic_proxy.rb @@ -3,7 +3,7 @@ require "yaml" module Sidekiq - module Extensions + module DelayExtensions SIZE_LIMIT = 8_192 class Proxy < BasicObject diff --git a/lib/sidekiq/delay_extensions/testing.rb b/lib/sidekiq/delay_extensions/testing.rb new file mode 100644 index 00000000..088a2c6f --- /dev/null +++ b/lib/sidekiq/delay_extensions/testing.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "sidekiq/testing" + +module Sidekiq + Sidekiq::DelayExtensions::DelayedMailer.extend(TestingExtensions) if defined?(Sidekiq::DelayExtensions::DelayedMailer) + Sidekiq::DelayExtensions::DelayedModel.extend(TestingExtensions) if defined?(Sidekiq::DelayExtensions::DelayedModel) +end diff --git a/lib/sidekiq/delay_extensions/version.rb b/lib/sidekiq/delay_extensions/version.rb new file mode 100644 index 00000000..2a136464 --- /dev/null +++ b/lib/sidekiq/delay_extensions/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Sidekiq + module DelayExtensions + VERSION = "6.4.1" + end +end diff --git a/lib/sidekiq/exception_handler.rb b/lib/sidekiq/exception_handler.rb deleted file mode 100644 index 77382177..00000000 --- a/lib/sidekiq/exception_handler.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq" - -module Sidekiq - module ExceptionHandler - class Logger - def call(ex, ctx) - Sidekiq.logger.warn(Sidekiq.dump_json(ctx)) unless ctx.empty? - Sidekiq.logger.warn("#{ex.class.name}: #{ex.message}") - Sidekiq.logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil? - end - - Sidekiq.error_handlers << Sidekiq::ExceptionHandler::Logger.new - end - - def handle_exception(ex, ctx = {}) - Sidekiq.error_handlers.each do |handler| - handler.call(ex, ctx) - rescue => ex - Sidekiq.logger.error "!!! ERROR HANDLER THREW AN ERROR !!!" - Sidekiq.logger.error ex - Sidekiq.logger.error ex.backtrace.join("\n") unless ex.backtrace.nil? - end - end - end -end diff --git a/lib/sidekiq/fetch.rb b/lib/sidekiq/fetch.rb deleted file mode 100644 index f3d1fbb8..00000000 --- a/lib/sidekiq/fetch.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq" - -module Sidekiq - class BasicFetch - # We want the fetch operation to timeout every few seconds so the thread - # can check if the process is shutting down. - TIMEOUT = 2 - - UnitOfWork = Struct.new(:queue, :job) { - def acknowledge - # nothing to do - end - - def queue_name - queue.delete_prefix("queue:") - end - - def requeue - Sidekiq.redis do |conn| - conn.rpush(queue, job) - end - end - } - - def initialize(options) - raise ArgumentError, "missing queue list" unless options[:queues] - @options = options - @strictly_ordered_queues = !!@options[:strict] - @queues = @options[:queues].map { |q| "queue:#{q}" } - if @strictly_ordered_queues - @queues.uniq! - @queues << TIMEOUT - end - end - - def retrieve_work - qs = queues_cmd - # 4825 Sidekiq Pro with all queues paused will return an - # empty set of queues with a trailing TIMEOUT value. - if qs.size <= 1 - sleep(TIMEOUT) - return nil - end - - work = Sidekiq.redis { |conn| conn.brpop(*qs) } - UnitOfWork.new(*work) if work - end - - def bulk_requeue(inprogress, options) - return if inprogress.empty? - - Sidekiq.logger.debug { "Re-queueing terminated jobs" } - jobs_to_requeue = {} - inprogress.each do |unit_of_work| - jobs_to_requeue[unit_of_work.queue] ||= [] - jobs_to_requeue[unit_of_work.queue] << unit_of_work.job - end - - Sidekiq.redis do |conn| - conn.pipelined do |pipeline| - jobs_to_requeue.each do |queue, jobs| - pipeline.rpush(queue, jobs) - end - end - end - Sidekiq.logger.info("Pushed #{inprogress.size} jobs back to Redis") - rescue => ex - Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}") - end - - # Creating the Redis#brpop command takes into account any - # configured queue weights. By default Redis#brpop returns - # data from the first queue that has pending elements. We - # recreate the queue command each time we invoke Redis#brpop - # to honor weights and avoid queue starvation. - def queues_cmd - if @strictly_ordered_queues - @queues - else - permute = @queues.shuffle - permute.uniq! - permute << TIMEOUT - permute - end - end - end -end diff --git a/lib/sidekiq/job.rb b/lib/sidekiq/job.rb deleted file mode 100644 index c8ccc794..00000000 --- a/lib/sidekiq/job.rb +++ /dev/null @@ -1,13 +0,0 @@ -require "sidekiq/worker" - -module Sidekiq - # Sidekiq::Job is a new alias for Sidekiq::Worker as of Sidekiq 6.3.0. - # Use `include Sidekiq::Job` rather than `include Sidekiq::Worker`. - # - # The term "worker" is too generic and overly confusing, used in several - # different contexts meaning different things. Many people call a Sidekiq - # process a "worker". Some people call the thread that executes jobs a - # "worker". This change brings Sidekiq closer to ActiveJob where your job - # classes extend ApplicationJob. - Job = Worker -end diff --git a/lib/sidekiq/job_logger.rb b/lib/sidekiq/job_logger.rb deleted file mode 100644 index 91be17b1..00000000 --- a/lib/sidekiq/job_logger.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module Sidekiq - class JobLogger - def initialize(logger = Sidekiq.logger) - @logger = logger - end - - def call(item, queue) - start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @logger.info("start") - - yield - - Sidekiq::Context.add(:elapsed, elapsed(start)) - @logger.info("done") - rescue Exception - Sidekiq::Context.add(:elapsed, elapsed(start)) - @logger.info("fail") - - raise - end - - def prepare(job_hash, &block) - # If we're using a wrapper class, like ActiveJob, use the "wrapped" - # attribute to expose the underlying thing. - h = { - class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"], - jid: job_hash["jid"] - } - h[:bid] = job_hash["bid"] if job_hash.has_key?("bid") - h[:tags] = job_hash["tags"] if job_hash.has_key?("tags") - - Thread.current[:sidekiq_context] = h - level = job_hash["log_level"] - if level - @logger.log_at(level, &block) - else - yield - end - ensure - Thread.current[:sidekiq_context] = nil - end - - private - - def elapsed(start) - (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start).round(3) - end - end -end diff --git a/lib/sidekiq/job_retry.rb b/lib/sidekiq/job_retry.rb deleted file mode 100644 index 1d96f6d0..00000000 --- a/lib/sidekiq/job_retry.rb +++ /dev/null @@ -1,261 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq/scheduled" -require "sidekiq/api" - -require "zlib" -require "base64" - -module Sidekiq - ## - # Automatically retry jobs that fail in Sidekiq. - # Sidekiq's retry support assumes a typical development lifecycle: - # - # 0. Push some code changes with a bug in it. - # 1. Bug causes job processing to fail, Sidekiq's middleware captures - # the job and pushes it onto a retry queue. - # 2. Sidekiq retries jobs in the retry queue multiple times with - # an exponential delay, the job continues to fail. - # 3. After a few days, a developer deploys a fix. The job is - # reprocessed successfully. - # 4. Once retries are exhausted, Sidekiq will give up and move the - # job to the Dead Job Queue (aka morgue) where it must be dealt with - # manually in the Web UI. - # 5. After 6 months on the DJQ, Sidekiq will discard the job. - # - # A job looks like: - # - # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true } - # - # The 'retry' option also accepts a number (in place of 'true'): - # - # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 } - # - # The job will be retried this number of times before giving up. (If simply - # 'true', Sidekiq retries 25 times) - # - # Relevant options for job retries: - # - # * 'queue' - the queue for the initial job - # * 'retry_queue' - if job retries should be pushed to a different (e.g. lower priority) queue - # * 'retry_count' - number of times we've retried so far. - # * 'error_message' - the message from the exception - # * 'error_class' - the exception class - # * 'failed_at' - the first time it failed - # * 'retried_at' - the last time it was retried - # * 'backtrace' - the number of lines of error backtrace to store - # - # We don't store the backtrace by default as that can add a lot of overhead - # to the job and everyone is using an error service, right? - # - # The default number of retries is 25 which works out to about 3 weeks - # You can change the default maximum number of retries in your initializer: - # - # Sidekiq.options[:max_retries] = 7 - # - # or limit the number of retries for a particular worker and send retries to - # a low priority queue with: - # - # class MyWorker - # include Sidekiq::Worker - # sidekiq_options retry: 10, retry_queue: 'low' - # end - # - class JobRetry - class Handled < ::RuntimeError; end - - class Skip < Handled; end - - include Sidekiq::Util - - DEFAULT_MAX_RETRY_ATTEMPTS = 25 - - def initialize(options = {}) - @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS) - end - - # The global retry handler requires only the barest of data. - # We want to be able to retry as much as possible so we don't - # require the worker to be instantiated. - def global(jobstr, queue) - yield - rescue Handled => ex - raise ex - rescue Sidekiq::Shutdown => ey - # ignore, will be pushed back onto queue during hard_shutdown - raise ey - rescue Exception => e - # ignore, will be pushed back onto queue during hard_shutdown - raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e) - - msg = Sidekiq.load_json(jobstr) - if msg["retry"] - attempt_retry(nil, msg, queue, e) - else - Sidekiq.death_handlers.each do |handler| - handler.call(msg, e) - rescue => handler_ex - handle_exception(handler_ex, {context: "Error calling death handler", job: msg}) - end - end - - raise Handled - end - - # The local retry support means that any errors that occur within - # this block can be associated with the given worker instance. - # This is required to support the `sidekiq_retries_exhausted` block. - # - # Note that any exception from the block is wrapped in the Skip - # exception so the global block does not reprocess the error. The - # Skip exception is unwrapped within Sidekiq::Processor#process before - # calling the handle_exception handlers. - def local(worker, jobstr, queue) - yield - rescue Handled => ex - raise ex - rescue Sidekiq::Shutdown => ey - # ignore, will be pushed back onto queue during hard_shutdown - raise ey - rescue Exception => e - # ignore, will be pushed back onto queue during hard_shutdown - raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e) - - msg = Sidekiq.load_json(jobstr) - if msg["retry"].nil? - msg["retry"] = worker.class.get_sidekiq_options["retry"] - end - - raise e unless msg["retry"] - attempt_retry(worker, msg, queue, e) - # We've handled this error associated with this job, don't - # need to handle it at the global level - raise Skip - end - - private - - # Note that +worker+ can be nil here if an error is raised before we can - # instantiate the worker instance. All access must be guarded and - # best effort. - def attempt_retry(worker, msg, queue, exception) - max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries) - - msg["queue"] = (msg["retry_queue"] || queue) - - m = exception_message(exception) - if m.respond_to?(:scrub!) - m.force_encoding("utf-8") - m.scrub! - end - - msg["error_message"] = m - msg["error_class"] = exception.class.name - count = if msg["retry_count"] - msg["retried_at"] = Time.now.to_f - msg["retry_count"] += 1 - else - msg["failed_at"] = Time.now.to_f - msg["retry_count"] = 0 - end - - if msg["backtrace"] - lines = if msg["backtrace"] == true - exception.backtrace - else - exception.backtrace[0...msg["backtrace"].to_i] - end - - msg["error_backtrace"] = compress_backtrace(lines) - end - - if count < max_retry_attempts - delay = delay_for(worker, count, exception) - # Logging here can break retries if the logging device raises ENOSPC #3979 - # logger.debug { "Failure! Retry #{count} in #{delay} seconds" } - retry_at = Time.now.to_f + delay - payload = Sidekiq.dump_json(msg) - Sidekiq.redis do |conn| - conn.zadd("retry", retry_at.to_s, payload) - end - else - # Goodbye dear message, you (re)tried your best I'm sure. - retries_exhausted(worker, msg, exception) - end - end - - def retries_exhausted(worker, msg, exception) - begin - block = worker&.sidekiq_retries_exhausted_block - block&.call(msg, exception) - rescue => e - handle_exception(e, {context: "Error calling retries_exhausted", job: msg}) - end - - send_to_morgue(msg) unless msg["dead"] == false - - Sidekiq.death_handlers.each do |handler| - handler.call(msg, exception) - rescue => e - handle_exception(e, {context: "Error calling death handler", job: msg}) - end - end - - def send_to_morgue(msg) - logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" } - payload = Sidekiq.dump_json(msg) - DeadSet.new.kill(payload, notify_failure: false) - end - - def retry_attempts_from(msg_retry, default) - if msg_retry.is_a?(Integer) - msg_retry - else - default - end - end - - def delay_for(worker, count, exception) - jitter = rand(10) * (count + 1) - if worker&.sidekiq_retry_in_block - custom_retry_in = retry_in(worker, count, exception).to_i - return custom_retry_in + jitter if custom_retry_in > 0 - end - (count**4) + 15 + jitter - end - - def retry_in(worker, count, exception) - worker.sidekiq_retry_in_block.call(count, exception) - rescue Exception => e - handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default"}) - nil - end - - def exception_caused_by_shutdown?(e, checked_causes = []) - return false unless e.cause - - # Handle circular causes - checked_causes << e.object_id - return false if checked_causes.include?(e.cause.object_id) - - e.cause.instance_of?(Sidekiq::Shutdown) || - exception_caused_by_shutdown?(e.cause, checked_causes) - end - - # Extract message from exception. - # Set a default if the message raises an error - def exception_message(exception) - # App code can stuff all sorts of crazy binary data into the error message - # that won't convert to JSON. - exception.message.to_s[0, 10_000] - rescue - +"!!! ERROR MESSAGE THREW AN ERROR !!!" - end - - def compress_backtrace(backtrace) - serialized = Sidekiq.dump_json(backtrace) - compressed = Zlib::Deflate.deflate(serialized) - Base64.encode64(compressed) - end - end -end diff --git a/lib/sidekiq/job_util.rb b/lib/sidekiq/job_util.rb deleted file mode 100644 index 65e354b2..00000000 --- a/lib/sidekiq/job_util.rb +++ /dev/null @@ -1,65 +0,0 @@ -require "securerandom" -require "time" - -module Sidekiq - module JobUtil - # These functions encapsulate various job utilities. - # They must be simple and free from side effects. - - def validate(item) - raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args") - raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array) - raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String) - raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric) - raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array) - - if Sidekiq.options[:on_complex_arguments] == :raise - msg = <<~EOM - Job arguments to #{item["class"]} must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices. - To disable this error, remove `Sidekiq.strict_args!` from your initializer. - EOM - raise(ArgumentError, msg) unless json_safe?(item) - elsif Sidekiq.options[:on_complex_arguments] == :warn - Sidekiq.logger.warn <<~EOM unless json_safe?(item) - Job arguments to #{item["class"]} do not serialize to JSON safely. This will raise an error in - Sidekiq 7.0. See https://github.com/mperham/sidekiq/wiki/Best-Practices or raise an error today - by calling `Sidekiq.strict_args!` during Sidekiq initialization. - EOM - end - end - - def normalize_item(item) - validate(item) - - # merge in the default sidekiq_options for the item's class and/or wrapped element - # this allows ActiveJobs to control sidekiq_options too. - defaults = normalized_hash(item["class"]) - defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?(:get_sidekiq_options) - item = defaults.merge(item) - - raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == "" - - item["class"] = item["class"].to_s - item["queue"] = item["queue"].to_s - item["jid"] ||= SecureRandom.hex(12) - item["created_at"] ||= Time.now.to_f - - item - end - - def normalized_hash(item_class) - if item_class.is_a?(Class) - raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options) - item_class.get_sidekiq_options - else - Sidekiq.default_worker_options - end - end - - private - - def json_safe?(item) - JSON.parse(JSON.dump(item["args"])) == item["args"] - end - end -end diff --git a/lib/sidekiq/launcher.rb b/lib/sidekiq/launcher.rb deleted file mode 100644 index 73a62468..00000000 --- a/lib/sidekiq/launcher.rb +++ /dev/null @@ -1,263 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq/manager" -require "sidekiq/fetch" -require "sidekiq/scheduled" - -module Sidekiq - # The Launcher starts the Manager and Poller threads and provides the process heartbeat. - class Launcher - include Util - - STATS_TTL = 5 * 365 * 24 * 60 * 60 # 5 years - - PROCTITLES = [ - proc { "sidekiq" }, - proc { Sidekiq::VERSION }, - proc { |me, data| data["tag"] }, - proc { |me, data| "[#{Processor::WORKER_STATE.size} of #{data["concurrency"]} busy]" }, - proc { |me, data| "stopping" if me.stopping? } - ] - - attr_accessor :manager, :poller, :fetcher - - def initialize(options) - options[:fetch] ||= BasicFetch.new(options) - @manager = Sidekiq::Manager.new(options) - @poller = Sidekiq::Scheduled::Poller.new - @done = false - @options = options - end - - def run - @thread = safe_thread("heartbeat", &method(:start_heartbeat)) - @poller.start - @manager.start - end - - # Stops this instance from processing any more jobs, - # - def quiet - @done = true - @manager.quiet - @poller.terminate - end - - # Shuts down the process. This method does not - # return until all work is complete and cleaned up. - # It can take up to the timeout to complete. - def stop - deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + @options[:timeout] - - @done = true - @manager.quiet - @poller.terminate - - @manager.stop(deadline) - - # Requeue everything in case there was a worker who grabbed work while stopped - # This call is a no-op in Sidekiq but necessary for Sidekiq Pro. - strategy = @options[:fetch] - strategy.bulk_requeue([], @options) - - clear_heartbeat - end - - def stopping? - @done - end - - private unless $TESTING - - BEAT_PAUSE = 5 - - def start_heartbeat - loop do - heartbeat - sleep BEAT_PAUSE - end - Sidekiq.logger.info("Heartbeat stopping...") - end - - def clear_heartbeat - # Remove record from Redis since we are shutting down. - # Note we don't stop the heartbeat thread; if the process - # doesn't actually exit, it'll reappear in the Web UI. - Sidekiq.redis do |conn| - conn.pipelined do |pipeline| - pipeline.srem("processes", identity) - pipeline.unlink("#{identity}:workers") - end - end - rescue - # best effort, ignore network errors - end - - def heartbeat - $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") - - ❤ - end - - def self.flush_stats - fails = Processor::FAILURE.reset - procd = Processor::PROCESSED.reset - return if fails + procd == 0 - - nowdate = Time.now.utc.strftime("%Y-%m-%d") - begin - Sidekiq.redis do |conn| - conn.pipelined do |pipeline| - pipeline.incrby("stat:processed", procd) - pipeline.incrby("stat:processed:#{nowdate}", procd) - pipeline.expire("stat:processed:#{nowdate}", STATS_TTL) - - pipeline.incrby("stat:failed", fails) - pipeline.incrby("stat:failed:#{nowdate}", fails) - pipeline.expire("stat:failed:#{nowdate}", STATS_TTL) - end - end - rescue => ex - # we're exiting the process, things might be shut down so don't - # try to handle the exception - Sidekiq.logger.warn("Unable to flush stats: #{ex}") - end - end - at_exit(&method(:flush_stats)) - - def ❤ - key = identity - fails = procd = 0 - - begin - fails = Processor::FAILURE.reset - procd = Processor::PROCESSED.reset - curstate = Processor::WORKER_STATE.dup - - workers_key = "#{key}:workers" - nowdate = Time.now.utc.strftime("%Y-%m-%d") - - Sidekiq.redis do |conn| - conn.multi do |transaction| - transaction.incrby("stat:processed", procd) - transaction.incrby("stat:processed:#{nowdate}", procd) - transaction.expire("stat:processed:#{nowdate}", STATS_TTL) - - transaction.incrby("stat:failed", fails) - transaction.incrby("stat:failed:#{nowdate}", fails) - transaction.expire("stat:failed:#{nowdate}", STATS_TTL) - - transaction.unlink(workers_key) - curstate.each_pair do |tid, hash| - conn.hset(workers_key, tid, Sidekiq.dump_json(hash)) - end - conn.expire(workers_key, 60) - end - end - - rtt = check_rtt - - fails = procd = 0 - kb = memory_usage(::Process.pid) - - _, exists, _, _, msg = Sidekiq.redis { |conn| - conn.multi { |transaction| - transaction.sadd("processes", key) - transaction.exists?(key) - transaction.hmset(key, "info", to_json, - "busy", curstate.size, - "beat", Time.now.to_f, - "rtt_us", rtt, - "quiet", @done, - "rss", kb) - transaction.expire(key, 60) - transaction.rpop("#{key}-signals") - } - } - - # first heartbeat or recovering from an outage and need to reestablish our heartbeat - fire_event(:heartbeat) unless exists - - return unless msg - - ::Process.kill(msg, ::Process.pid) - rescue => e - # ignore all redis/network issues - logger.error("heartbeat: #{e}") - # don't lose the counts if there was a network issue - Processor::PROCESSED.incr(procd) - Processor::FAILURE.incr(fails) - end - end - - # We run the heartbeat every five seconds. - # Capture five samples of RTT, log a warning if each sample - # is above our warning threshold. - RTT_READINGS = RingBuffer.new(5) - RTT_WARNING_LEVEL = 50_000 - - def check_rtt - a = b = 0 - Sidekiq.redis do |x| - a = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond) - x.ping - b = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :microsecond) - end - rtt = b - a - RTT_READINGS << rtt - # Ideal RTT for Redis is < 1000µs - # Workable is < 10,000µs - # Log a warning if it's a disaster. - if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL } - Sidekiq.logger.warn <<~EOM - Your Redis network connection is performing extremely poorly. - Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000. - Ensure Redis is running in the same AZ or datacenter as Sidekiq. - If these values are close to 100,000, that means your Sidekiq process may be - CPU overloaded; see https://github.com/mperham/sidekiq/discussions/5039 - EOM - RTT_READINGS.reset - end - rtt - end - - MEMORY_GRABBER = case RUBY_PLATFORM - when /linux/ - ->(pid) { - IO.readlines("/proc/#{$$}/status").each do |line| - next unless line.start_with?("VmRSS:") - break line.split[1].to_i - end - } - when /darwin|bsd/ - ->(pid) { - `ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i - } - else - ->(pid) { 0 } - end - - def memory_usage(pid) - MEMORY_GRABBER.call(pid) - end - - def to_data - @data ||= { - "hostname" => hostname, - "started_at" => Time.now.to_f, - "pid" => ::Process.pid, - "tag" => @options[:tag] || "", - "concurrency" => @options[:concurrency], - "queues" => @options[:queues].uniq, - "labels" => @options[:labels], - "identity" => identity - } - end - - def to_json - # this data changes infrequently so dump it to a string - # now so we don't need to dump it every heartbeat. - @json ||= Sidekiq.dump_json(to_data) - end - end -end diff --git a/lib/sidekiq/logger.rb b/lib/sidekiq/logger.rb deleted file mode 100644 index 49d435a1..00000000 --- a/lib/sidekiq/logger.rb +++ /dev/null @@ -1,170 +0,0 @@ -# frozen_string_literal: true - -require "logger" -require "time" - -module Sidekiq - module Context - def self.with(hash) - orig_context = current.dup - current.merge!(hash) - yield - ensure - Thread.current[:sidekiq_context] = orig_context - end - - def self.current - Thread.current[:sidekiq_context] ||= {} - end - - def self.add(k, v) - Thread.current[:sidekiq_context][k] = v - end - end - - module LoggingUtils - LEVELS = { - "debug" => 0, - "info" => 1, - "warn" => 2, - "error" => 3, - "fatal" => 4 - } - LEVELS.default_proc = proc do |_, level| - Sidekiq.logger.warn("Invalid log level: #{level.inspect}") - nil - end - - def debug? - level <= 0 - end - - def info? - level <= 1 - end - - def warn? - level <= 2 - end - - def error? - level <= 3 - end - - def fatal? - level <= 4 - end - - def local_level - Thread.current[:sidekiq_log_level] - end - - def local_level=(level) - case level - when Integer - Thread.current[:sidekiq_log_level] = level - when Symbol, String - Thread.current[:sidekiq_log_level] = LEVELS[level.to_s] - when nil - Thread.current[:sidekiq_log_level] = nil - else - raise ArgumentError, "Invalid log level: #{level.inspect}" - end - end - - def level - local_level || super - end - - # Change the thread-local level for the duration of the given block. - def log_at(level) - old_local_level = local_level - self.local_level = level - yield - ensure - self.local_level = old_local_level - end - - # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+. - # FIXME: Remove when the minimum Ruby version supports overriding Logger#level. - def add(severity, message = nil, progname = nil, &block) - severity ||= ::Logger::UNKNOWN - progname ||= @progname - - return true if @logdev.nil? || severity < level - - if message.nil? - if block - message = yield - else - message = progname - progname = @progname - end - end - - @logdev.write format_message(format_severity(severity), Time.now, progname, message) - end - end - - class Logger < ::Logger - include LoggingUtils - - def initialize(*args, **kwargs) - super - self.formatter = Sidekiq.log_formatter - end - - module Formatters - class Base < ::Logger::Formatter - def tid - Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36) - end - - def ctx - Sidekiq::Context.current - end - - def format_context - if ctx.any? - " " + ctx.compact.map { |k, v| - case v - when Array - "#{k}=#{v.join(",")}" - else - "#{k}=#{v}" - end - }.join(" ") - end - end - end - - class Pretty < Base - def call(severity, time, program_name, message) - "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n" - end - end - - class WithoutTimestamp < Pretty - def call(severity, time, program_name, message) - "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n" - end - end - - class JSON < Base - def call(severity, time, program_name, message) - hash = { - ts: time.utc.iso8601(3), - pid: ::Process.pid, - tid: tid, - lvl: severity, - msg: message - } - c = ctx - hash["ctx"] = c unless c.empty? - - Sidekiq.dump_json(hash) << "\n" - end - end - end - end -end diff --git a/lib/sidekiq/manager.rb b/lib/sidekiq/manager.rb deleted file mode 100644 index 4038f373..00000000 --- a/lib/sidekiq/manager.rb +++ /dev/null @@ -1,133 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq/util" -require "sidekiq/processor" -require "sidekiq/fetch" -require "set" - -module Sidekiq - ## - # The Manager is the central coordination point in Sidekiq, controlling - # the lifecycle of the Processors. - # - # Tasks: - # - # 1. start: Spin up Processors. - # 3. processor_died: Handle job failure, throw away Processor, create new one. - # 4. quiet: shutdown idle Processors. - # 5. stop: hard stop the Processors by deadline. - # - # Note that only the last task requires its own Thread since it has to monitor - # the shutdown process. The other tasks are performed by other threads. - # - class Manager - include Util - - attr_reader :workers - attr_reader :options - - def initialize(options = {}) - logger.debug { options.inspect } - @options = options - @count = options[:concurrency] || 10 - raise ArgumentError, "Concurrency of #{@count} is not supported" if @count < 1 - - @done = false - @workers = Set.new - @count.times do - @workers << Processor.new(self, options) - end - @plock = Mutex.new - end - - def start - @workers.each do |x| - x.start - end - end - - def quiet - return if @done - @done = true - - logger.info { "Terminating quiet workers" } - @workers.each { |x| x.terminate } - fire_event(:quiet, reverse: true) - end - - def stop(deadline) - quiet - fire_event(:shutdown, reverse: true) - - # some of the shutdown events can be async, - # we don't have any way to know when they're done but - # give them a little time to take effect - sleep PAUSE_TIME - return if @workers.empty? - - logger.info { "Pausing to allow workers to finish..." } - wait_for(deadline) { @workers.empty? } - return if @workers.empty? - - hard_shutdown - end - - def processor_stopped(processor) - @plock.synchronize do - @workers.delete(processor) - end - end - - def processor_died(processor, reason) - @plock.synchronize do - @workers.delete(processor) - unless @done - p = Processor.new(self, options) - @workers << p - p.start - end - end - end - - def stopped? - @done - end - - private - - def hard_shutdown - # We've reached the timeout and we still have busy workers. - # They must die but their jobs shall live on. - cleanup = nil - @plock.synchronize do - cleanup = @workers.dup - end - - if cleanup.size > 0 - jobs = cleanup.map { |p| p.job }.compact - - logger.warn { "Terminating #{cleanup.size} busy worker threads" } - logger.warn { "Work still in progress #{jobs.inspect}" } - - # Re-enqueue unfinished jobs - # NOTE: You may notice that we may push a job back to redis before - # the worker thread is terminated. This is ok because Sidekiq's - # contract says that jobs are run AT LEAST once. Process termination - # is delayed until we're certain the jobs are back in Redis because - # it is worse to lose a job than to run it twice. - strategy = @options[:fetch] - strategy.bulk_requeue(jobs, @options) - end - - cleanup.each do |processor| - processor.kill - end - - # when this method returns, we immediately call `exit` which may not give - # the remaining threads time to run `ensure` blocks, etc. We pause here up - # to 3 seconds to give threads a minimal amount of time to run `ensure` blocks. - deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + 3 - wait_for(deadline) { @workers.empty? } - end - end -end diff --git a/lib/sidekiq/middleware/chain.rb b/lib/sidekiq/middleware/chain.rb deleted file mode 100644 index c039cb3f..00000000 --- a/lib/sidekiq/middleware/chain.rb +++ /dev/null @@ -1,162 +0,0 @@ -# frozen_string_literal: true - -module Sidekiq - # Middleware is code configured to run before/after - # a message is processed. It is patterned after Rack - # middleware. Middleware exists for the client side - # (pushing jobs onto the queue) as well as the server - # side (when jobs are actually processed). - # - # To add middleware for the client: - # - # Sidekiq.configure_client do |config| - # config.client_middleware do |chain| - # chain.add MyClientHook - # end - # end - # - # To modify middleware for the server, just call - # with another block: - # - # Sidekiq.configure_server do |config| - # config.server_middleware do |chain| - # chain.add MyServerHook - # chain.remove ActiveRecord - # end - # end - # - # To insert immediately preceding another entry: - # - # Sidekiq.configure_client do |config| - # config.client_middleware do |chain| - # chain.insert_before ActiveRecord, MyClientHook - # end - # end - # - # To insert immediately after another entry: - # - # Sidekiq.configure_client do |config| - # config.client_middleware do |chain| - # chain.insert_after ActiveRecord, MyClientHook - # end - # end - # - # This is an example of a minimal server middleware: - # - # class MyServerHook - # def call(worker_instance, msg, queue) - # puts "Before work" - # yield - # puts "After work" - # end - # end - # - # This is an example of a minimal client middleware, note - # the method must return the result or the job will not push - # to Redis: - # - # class MyClientHook - # def call(worker_class, msg, queue, redis_pool) - # puts "Before push" - # result = yield - # puts "After push" - # result - # end - # end - # - module Middleware - class Chain - include Enumerable - - def initialize_copy(copy) - copy.instance_variable_set(:@entries, entries.dup) - end - - def each(&block) - entries.each(&block) - end - - def initialize - @entries = nil - yield self if block_given? - end - - def entries - @entries ||= [] - end - - def remove(klass) - entries.delete_if { |entry| entry.klass == klass } - end - - def add(klass, *args) - remove(klass) - entries << Entry.new(klass, *args) - end - - def prepend(klass, *args) - remove(klass) - entries.insert(0, Entry.new(klass, *args)) - end - - def insert_before(oldklass, newklass, *args) - i = entries.index { |entry| entry.klass == newklass } - new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i) - i = entries.index { |entry| entry.klass == oldklass } || 0 - entries.insert(i, new_entry) - end - - def insert_after(oldklass, newklass, *args) - i = entries.index { |entry| entry.klass == newklass } - new_entry = i.nil? ? Entry.new(newklass, *args) : entries.delete_at(i) - i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1 - entries.insert(i + 1, new_entry) - end - - def exists?(klass) - any? { |entry| entry.klass == klass } - end - - def empty? - @entries.nil? || @entries.empty? - end - - def retrieve - map(&:make_new) - end - - def clear - entries.clear - end - - def invoke(*args) - return yield if empty? - - chain = retrieve - traverse_chain = proc do - if chain.empty? - yield - else - chain.shift.call(*args, &traverse_chain) - end - end - traverse_chain.call - end - end - - private - - class Entry - attr_reader :klass - - def initialize(klass, *args) - @klass = klass - @args = args - end - - def make_new - @klass.new(*@args) - end - end - end -end diff --git a/lib/sidekiq/middleware/current_attributes.rb b/lib/sidekiq/middleware/current_attributes.rb deleted file mode 100644 index f62b587b..00000000 --- a/lib/sidekiq/middleware/current_attributes.rb +++ /dev/null @@ -1,57 +0,0 @@ -require "active_support/current_attributes" - -module Sidekiq - ## - # Automatically save and load any current attributes in the execution context - # so context attributes "flow" from Rails actions into any associated jobs. - # This can be useful for multi-tenancy, i18n locale, timezone, any implicit - # per-request attribute. See +ActiveSupport::CurrentAttributes+. - # - # @example - # - # # in your initializer - # require "sidekiq/middleware/current_attributes" - # Sidekiq::CurrentAttributes.persist(Myapp::Current) - # - module CurrentAttributes - class Save - def initialize(cattr) - @klass = cattr - end - - def call(_, job, _, _) - attrs = @klass.attributes - if job.has_key?("cattr") - job["cattr"].merge!(attrs) - else - job["cattr"] = attrs - end - yield - end - end - - class Load - def initialize(cattr) - @klass = cattr - end - - def call(_, job, _, &block) - if job.has_key?("cattr") - @klass.set(job["cattr"], &block) - else - yield - end - end - end - - def self.persist(klass) - Sidekiq.configure_client do |config| - config.client_middleware.add Save, klass - end - Sidekiq.configure_server do |config| - config.client_middleware.add Save, klass - config.server_middleware.add Load, klass - end - end - end -end diff --git a/lib/sidekiq/middleware/i18n.rb b/lib/sidekiq/middleware/i18n.rb deleted file mode 100644 index 93c60025..00000000 --- a/lib/sidekiq/middleware/i18n.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -# -# Simple middleware to save the current locale and restore it when the job executes. -# Use it by requiring it in your initializer: -# -# require 'sidekiq/middleware/i18n' -# -module Sidekiq::Middleware::I18n - # Get the current locale and store it in the message - # to be sent to Sidekiq. - class Client - def call(_worker, msg, _queue, _redis) - msg["locale"] ||= I18n.locale - yield - end - end - - # Pull the msg locale out and set the current thread to use it. - class Server - def call(_worker, msg, _queue, &block) - I18n.with_locale(msg.fetch("locale", I18n.default_locale), &block) - end - end -end - -Sidekiq.configure_client do |config| - config.client_middleware do |chain| - chain.add Sidekiq::Middleware::I18n::Client - end -end - -Sidekiq.configure_server do |config| - config.client_middleware do |chain| - chain.add Sidekiq::Middleware::I18n::Client - end - config.server_middleware do |chain| - chain.add Sidekiq::Middleware::I18n::Server - end -end diff --git a/lib/sidekiq/monitor.rb b/lib/sidekiq/monitor.rb deleted file mode 100644 index 4d69d045..00000000 --- a/lib/sidekiq/monitor.rb +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env ruby - -require "fileutils" -require "sidekiq/api" - -class Sidekiq::Monitor - class Status - VALID_SECTIONS = %w[all version overview processes queues] - COL_PAD = 2 - - def display(section = nil) - section ||= "all" - unless VALID_SECTIONS.include? section - puts "I don't know how to check the status of '#{section}'!" - puts "Try one of these: #{VALID_SECTIONS.join(", ")}" - return - end - send(section) - rescue => e - puts "Couldn't get status: #{e}" - end - - def all - version - puts - overview - puts - processes - puts - queues - end - - def version - puts "Sidekiq #{Sidekiq::VERSION}" - puts Time.now.utc - end - - def overview - puts "---- Overview ----" - puts " Processed: #{delimit stats.processed}" - puts " Failed: #{delimit stats.failed}" - puts " Busy: #{delimit stats.workers_size}" - puts " Enqueued: #{delimit stats.enqueued}" - puts " Retries: #{delimit stats.retry_size}" - puts " Scheduled: #{delimit stats.scheduled_size}" - puts " Dead: #{delimit stats.dead_size}" - end - - def processes - puts "---- Processes (#{process_set.size}) ----" - process_set.each_with_index do |process, index| - puts "#{process["identity"]} #{tags_for(process)}" - puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})" - puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)" - puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}" - puts "" unless (index + 1) == process_set.size - end - end - - def queues - puts "---- Queues (#{queue_data.size}) ----" - columns = { - name: [:ljust, (["name"] + queue_data.map(&:name)).map(&:length).max + COL_PAD], - size: [:rjust, (["size"] + queue_data.map(&:size)).map(&:length).max + COL_PAD], - latency: [:rjust, (["latency"] + queue_data.map(&:latency)).map(&:length).max + COL_PAD] - } - columns.each { |col, (dir, width)| print col.to_s.upcase.public_send(dir, width) } - puts - queue_data.each do |q| - columns.each do |col, (dir, width)| - print q.send(col).public_send(dir, width) - end - puts - end - end - - private - - def delimit(number) - number.to_s.reverse.scan(/.{1,3}/).join(",").reverse - end - - def split_multiline(values, opts = {}) - return "none" unless values - pad = opts[:pad] || 0 - max_length = opts[:max_length] || (80 - pad) - out = [] - line = "" - values.each do |value| - if (line.length + value.length) > max_length - out << line - line = " " * pad - end - line << value + ", " - end - out << line[0..-3] - out.join("\n") - end - - def tags_for(process) - tags = [ - process["tag"], - process["labels"], - (process["quiet"] == "true" ? "quiet" : nil) - ].flatten.compact - tags.any? ? "[#{tags.join("] [")}]" : nil - end - - def time_ago(timestamp) - seconds = Time.now - Time.at(timestamp) - return "just now" if seconds < 60 - return "a minute ago" if seconds < 120 - return "#{seconds.floor / 60} minutes ago" if seconds < 3600 - return "an hour ago" if seconds < 7200 - "#{seconds.floor / 60 / 60} hours ago" - end - - QUEUE_STRUCT = Struct.new(:name, :size, :latency) - def queue_data - @queue_data ||= Sidekiq::Queue.all.map { |q| - QUEUE_STRUCT.new(q.name, q.size.to_s, sprintf("%#.2f", q.latency)) - } - end - - def process_set - @process_set ||= Sidekiq::ProcessSet.new - end - - def stats - @stats ||= Sidekiq::Stats.new - end - end -end diff --git a/lib/sidekiq/paginator.rb b/lib/sidekiq/paginator.rb deleted file mode 100644 index e9417420..00000000 --- a/lib/sidekiq/paginator.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -module Sidekiq - module Paginator - def page(key, pageidx = 1, page_size = 25, opts = nil) - current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i - pageidx = current_page - 1 - total_size = 0 - items = [] - starting = pageidx * page_size - ending = starting + page_size - 1 - - Sidekiq.redis do |conn| - type = conn.type(key) - rev = opts && opts[:reverse] - - case type - when "zset" - total_size, items = conn.multi { |transaction| - transaction.zcard(key) - if rev - transaction.zrevrange(key, starting, ending, with_scores: true) - else - transaction.zrange(key, starting, ending, with_scores: true) - end - } - [current_page, total_size, items] - when "list" - total_size, items = conn.multi { |transaction| - conn.llen(key) - if rev - transaction.lrange(key, -ending - 1, -starting - 1) - else - transaction.lrange(key, starting, ending) - end - } - items.reverse! if rev - [current_page, total_size, items] - when "none" - [1, 0, []] - else - raise "can't page a #{type}" - end - end - end - end -end diff --git a/lib/sidekiq/processor.rb b/lib/sidekiq/processor.rb deleted file mode 100644 index 39d3752c..00000000 --- a/lib/sidekiq/processor.rb +++ /dev/null @@ -1,280 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq/util" -require "sidekiq/fetch" -require "sidekiq/job_logger" -require "sidekiq/job_retry" - -module Sidekiq - ## - # The Processor is a standalone thread which: - # - # 1. fetches a job from Redis - # 2. executes the job - # a. instantiate the Worker - # b. run the middleware chain - # c. call #perform - # - # A Processor can exit due to shutdown (processor_stopped) - # or due to an error during job execution (processor_died) - # - # If an error occurs in the job execution, the - # Processor calls the Manager to create a new one - # to replace itself and exits. - # - class Processor - include Util - - attr_reader :thread - attr_reader :job - - def initialize(mgr, options) - @mgr = mgr - @down = false - @done = false - @job = nil - @thread = nil - @strategy = options[:fetch] - @reloader = options[:reloader] || proc { |&block| block.call } - @job_logger = (options[:job_logger] || Sidekiq::JobLogger).new - @retrier = Sidekiq::JobRetry.new - end - - def terminate(wait = false) - @done = true - return unless @thread - @thread.value if wait - end - - def kill(wait = false) - @done = true - return unless @thread - # unlike the other actors, terminate does not wait - # for the thread to finish because we don't know how - # long the job will take to finish. Instead we - # provide a `kill` method to call after the shutdown - # timeout passes. - @thread.raise ::Sidekiq::Shutdown - @thread.value if wait - end - - def start - @thread ||= safe_thread("processor", &method(:run)) - end - - private unless $TESTING - - def run - process_one until @done - @mgr.processor_stopped(self) - rescue Sidekiq::Shutdown - @mgr.processor_stopped(self) - rescue Exception => ex - @mgr.processor_died(self, ex) - end - - def process_one - @job = fetch - process(@job) if @job - @job = nil - end - - def get_one - work = @strategy.retrieve_work - if @down - logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" } - @down = nil - end - work - rescue Sidekiq::Shutdown - rescue => ex - handle_fetch_exception(ex) - end - - def fetch - j = get_one - if j && @done - j.requeue - nil - else - j - end - end - - def handle_fetch_exception(ex) - unless @down - @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - logger.error("Error fetching job: #{ex}") - handle_exception(ex) - end - sleep(1) - nil - end - - def dispatch(job_hash, queue, jobstr) - # since middleware can mutate the job hash - # we need to clone it to report the original - # job structure to the Web UI - # or to push back to redis when retrying. - # To avoid costly and, most of the time, useless cloning here, - # we pass original String of JSON to respected methods - # to re-parse it there if we need access to the original, untouched job - - @job_logger.prepare(job_hash) do - @retrier.global(jobstr, queue) do - @job_logger.call(job_hash, queue) do - stats(jobstr, queue) do - # Rails 5 requires a Reloader to wrap code execution. In order to - # constantize the worker and instantiate an instance, we have to call - # the Reloader. It handles code loading, db connection management, etc. - # Effectively this block denotes a "unit of work" to Rails. - @reloader.call do - klass = constantize(job_hash["class"]) - worker = klass.new - worker.jid = job_hash["jid"] - @retrier.local(worker, jobstr, queue) do - yield worker - end - end - end - end - end - end - end - - def process(work) - jobstr = work.job - queue = work.queue_name - - # Treat malformed JSON as a special case: job goes straight to the morgue. - job_hash = nil - begin - job_hash = Sidekiq.load_json(jobstr) - rescue => ex - handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr}) - # we can't notify because the job isn't a valid hash payload. - DeadSet.new.kill(jobstr, notify_failure: false) - return work.acknowledge - end - - ack = false - begin - dispatch(job_hash, queue, jobstr) do |worker| - Sidekiq.server_middleware.invoke(worker, job_hash, queue) do - execute_job(worker, job_hash["args"]) - end - end - ack = true - rescue Sidekiq::Shutdown - # Had to force kill this job because it didn't finish - # within the timeout. Don't acknowledge the work since - # we didn't properly finish it. - rescue Sidekiq::JobRetry::Handled => h - # this is the common case: job raised error and Sidekiq::JobRetry::Handled - # signals that we created a retry successfully. We can acknowlege the job. - ack = true - e = h.cause || h - handle_exception(e, {context: "Job raised exception", job: job_hash, jobstr: jobstr}) - raise e - rescue Exception => ex - # Unexpected error! This is very bad and indicates an exception that got past - # the retry subsystem (e.g. network partition). We won't acknowledge the job - # so it can be rescued when using Sidekiq Pro. - handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr}) - raise ex - ensure - if ack - # We don't want a shutdown signal to interrupt job acknowledgment. - Thread.handle_interrupt(Sidekiq::Shutdown => :never) do - work.acknowledge - end - end - end - end - - def execute_job(worker, cloned_args) - worker.perform(*cloned_args) - end - - # Ruby doesn't provide atomic counters out of the box so we'll - # implement something simple ourselves. - # https://bugs.ruby-lang.org/issues/14706 - class Counter - def initialize - @value = 0 - @lock = Mutex.new - end - - def incr(amount = 1) - @lock.synchronize { @value += amount } - end - - def reset - @lock.synchronize { - val = @value - @value = 0 - val - } - end - end - - # jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here - class SharedWorkerState - def initialize - @worker_state = {} - @lock = Mutex.new - end - - def set(tid, hash) - @lock.synchronize { @worker_state[tid] = hash } - end - - def delete(tid) - @lock.synchronize { @worker_state.delete(tid) } - end - - def dup - @lock.synchronize { @worker_state.dup } - end - - def size - @lock.synchronize { @worker_state.size } - end - - def clear - @lock.synchronize { @worker_state.clear } - end - end - - PROCESSED = Counter.new - FAILURE = Counter.new - WORKER_STATE = SharedWorkerState.new - - def stats(jobstr, queue) - WORKER_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i}) - - begin - yield - rescue Exception - FAILURE.incr - raise - ensure - WORKER_STATE.delete(tid) - PROCESSED.incr - end - end - - def constantize(str) - return Object.const_get(str) unless str.include?("::") - - names = str.split("::") - names.shift if names.empty? || names.first.empty? - - names.inject(Object) do |constant, name| - # the false flag limits search for name to under the constant namespace - # which mimics Rails' behaviour - constant.const_get(name, false) - end - end - end -end diff --git a/lib/sidekiq/rails.rb b/lib/sidekiq/rails.rb deleted file mode 100644 index cd467426..00000000 --- a/lib/sidekiq/rails.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq/worker" - -module Sidekiq - class Rails < ::Rails::Engine - class Reloader - def initialize(app = ::Rails.application) - @app = app - end - - def call - @app.reloader.wrap do - yield - end - end - - def inspect - "#" - end - end - - # By including the Options module, we allow AJs to directly control sidekiq features - # via the *sidekiq_options* class method and, for instance, not use AJ's retry system. - # AJ retries don't show up in the Sidekiq UI Retries tab, save any error data, can't be - # manually retried, don't automatically die, etc. - # - # class SomeJob < ActiveJob::Base - # queue_as :default - # sidekiq_options retry: 3, backtrace: 10 - # def perform - # end - # end - initializer "sidekiq.active_job_integration" do - ActiveSupport.on_load(:active_job) do - include ::Sidekiq::Worker::Options unless respond_to?(:sidekiq_options) - end - end - - initializer "sidekiq.rails_logger" do - Sidekiq.configure_server do |_| - # This is the integration code necessary so that if code uses `Rails.logger.info "Hello"`, - # it will appear in the Sidekiq console with all of the job context. See #5021 and - # https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84 - unless ::Rails.logger == ::Sidekiq.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout) - ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(::Sidekiq.logger)) - end - end - end - - # This hook happens after all initializers are run, just before returning - # from config/environment.rb back to sidekiq/cli.rb. - # - # None of this matters on the client-side, only within the Sidekiq process itself. - config.after_initialize do - Sidekiq.configure_server do |_| - Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new - end - end - end -end diff --git a/lib/sidekiq/redis_connection.rb b/lib/sidekiq/redis_connection.rb deleted file mode 100644 index d44f0b87..00000000 --- a/lib/sidekiq/redis_connection.rb +++ /dev/null @@ -1,144 +0,0 @@ -# frozen_string_literal: true - -require "connection_pool" -require "redis" -require "uri" - -module Sidekiq - class RedisConnection - class << self - def create(options = {}) - symbolized_options = options.transform_keys(&:to_sym) - - if !symbolized_options[:url] && (u = determine_redis_provider) - symbolized_options[:url] = u - end - - size = if symbolized_options[:size] - symbolized_options[:size] - elsif Sidekiq.server? - # Give ourselves plenty of connections. pool is lazy - # so we won't create them until we need them. - Sidekiq.options[:concurrency] + 5 - elsif ENV["RAILS_MAX_THREADS"] - Integer(ENV["RAILS_MAX_THREADS"]) - else - 5 - end - - verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server? - - pool_timeout = symbolized_options[:pool_timeout] || 1 - log_info(symbolized_options) - - ConnectionPool.new(timeout: pool_timeout, size: size) do - build_client(symbolized_options) - end - end - - private - - # Sidekiq needs a lot of concurrent Redis connections. - # - # We need a connection for each Processor. - # We need a connection for Pro's real-time change listener - # We need a connection to various features to call Redis every few seconds: - # - the process heartbeat. - # - enterprise's leader election - # - enterprise's cron support - def verify_sizing(size, concurrency) - raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but must have at least #{concurrency + 2}" if size < (concurrency + 2) - end - - def build_client(options) - namespace = options[:namespace] - - client = Redis.new client_opts(options) - if namespace - begin - require "redis/namespace" - Redis::Namespace.new(namespace, redis: client) - rescue LoadError - Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \ - "Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.") - exit(-127) - end - else - client - end - end - - def client_opts(options) - opts = options.dup - if opts[:namespace] - opts.delete(:namespace) - end - - if opts[:network_timeout] - opts[:timeout] = opts[:network_timeout] - opts.delete(:network_timeout) - end - - opts[:driver] ||= Redis::Connection.drivers.last || "ruby" - - # Issue #3303, redis-rb will silently retry an operation. - # This can lead to duplicate jobs if Sidekiq::Client's LPUSH - # is performed twice but I believe this is much, much rarer - # than the reconnect silently fixing a problem; we keep it - # on by default. - opts[:reconnect_attempts] ||= 1 - - opts - end - - def log_info(options) - redacted = "REDACTED" - - # Deep clone so we can muck with these options all we want and exclude - # params from dump-and-load that may contain objects that Marshal is - # unable to safely dump. - keys = options.keys - [:logger, :ssl_params] - scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys))) - if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password - uri.password = redacted - scrubbed_options[:url] = uri.to_s - end - if scrubbed_options[:password] - scrubbed_options[:password] = redacted - end - scrubbed_options[:sentinels]&.each do |sentinel| - sentinel[:password] = redacted if sentinel[:password] - end - if Sidekiq.server? - Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}") - else - Sidekiq.logger.debug("#{Sidekiq::NAME} client with redis options #{scrubbed_options}") - end - end - - def determine_redis_provider - # If you have this in your environment: - # MY_REDIS_URL=redis://hostname.example.com:1238/4 - # then set: - # REDIS_PROVIDER=MY_REDIS_URL - # and Sidekiq will find your custom URL variable with no custom - # initialization code at all. - # - p = ENV["REDIS_PROVIDER"] - if p && p =~ /:/ - raise <<~EOM - REDIS_PROVIDER should be set to the name of the variable which contains the Redis URL, not a URL itself. - Platforms like Heroku will sell addons that publish a *_URL variable. You need to tell Sidekiq with REDIS_PROVIDER, e.g.: - - REDISTOGO_URL=redis://somehost.example.com:6379/4 - REDIS_PROVIDER=REDISTOGO_URL - EOM - end - - ENV[ - p || "REDIS_URL" - ] - end - end - end -end diff --git a/lib/sidekiq/scheduled.rb b/lib/sidekiq/scheduled.rb deleted file mode 100644 index 761dab5d..00000000 --- a/lib/sidekiq/scheduled.rb +++ /dev/null @@ -1,208 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq" -require "sidekiq/util" -require "sidekiq/api" - -module Sidekiq - module Scheduled - SETS = %w[retry schedule] - - class Enq - LUA_ZPOPBYSCORE = <<~LUA - local key, now = KEYS[1], ARGV[1] - local jobs = redis.call("zrangebyscore", key, "-inf", now, "limit", 0, 1) - if jobs[1] then - redis.call("zrem", key, jobs[1]) - return jobs[1] - end - LUA - - def initialize - @done = false - @lua_zpopbyscore_sha = nil - end - - def enqueue_jobs(sorted_sets = SETS) - # A job's "score" in Redis is the time at which it should be processed. - # Just check Redis for the set of jobs with a timestamp before now. - Sidekiq.redis do |conn| - sorted_sets.each do |sorted_set| - # Get next item in the queue with score (time to execute) <= now. - # We need to go through the list one at a time to reduce the risk of something - # going wrong between the time jobs are popped from the scheduled queue and when - # they are pushed onto a work queue and losing the jobs. - while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s])) - Sidekiq::Client.push(Sidekiq.load_json(job)) - Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" } - end - end - end - end - - def terminate - @done = true - end - - private - - def zpopbyscore(conn, keys: nil, argv: nil) - if @lua_zpopbyscore_sha.nil? - raw_conn = conn.respond_to?(:redis) ? conn.redis : conn - @lua_zpopbyscore_sha = raw_conn.script(:load, LUA_ZPOPBYSCORE) - end - - conn.evalsha(@lua_zpopbyscore_sha, keys: keys, argv: argv) - rescue Redis::CommandError => e - raise unless e.message.start_with?("NOSCRIPT") - - @lua_zpopbyscore_sha = nil - retry - end - end - - ## - # The Poller checks Redis every N seconds for jobs in the retry or scheduled - # set have passed their timestamp and should be enqueued. If so, it - # just pops the job back onto its original queue so the - # workers can pick it up like any other job. - class Poller - include Util - - INITIAL_WAIT = 10 - - def initialize - @enq = (Sidekiq.options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new - @sleeper = ConnectionPool::TimedStack.new - @done = false - @thread = nil - @count_calls = 0 - end - - # Shut down this instance, will pause until the thread is dead. - def terminate - @done = true - @enq.terminate if @enq.respond_to?(:terminate) - - if @thread - t = @thread - @thread = nil - @sleeper << 0 - t.value - end - end - - def start - @thread ||= safe_thread("scheduler") { - initial_wait - - until @done - enqueue - wait - end - Sidekiq.logger.info("Scheduler exiting...") - } - end - - def enqueue - @enq.enqueue_jobs - rescue => ex - # Most likely a problem with redis networking. - # Punt and try again at the next interval - logger.error ex.message - handle_exception(ex) - end - - private - - def wait - @sleeper.pop(random_poll_interval) - rescue Timeout::Error - # expected - rescue => ex - # if poll_interval_average hasn't been calculated yet, we can - # raise an error trying to reach Redis. - logger.error ex.message - handle_exception(ex) - sleep 5 - end - - def random_poll_interval - # We want one Sidekiq process to schedule jobs every N seconds. We have M processes - # and **don't** want to coordinate. - # - # So in N*M second timespan, we want each process to schedule once. The basic loop is: - # - # * sleep a random amount within that N*M timespan - # * wake up and schedule - # - # We want to avoid one edge case: imagine a set of 2 processes, scheduling every 5 seconds, - # so N*M = 10. Each process decides to randomly sleep 8 seconds, now we've failed to meet - # that 5 second average. Thankfully each schedule cycle will sleep randomly so the next - # iteration could see each process sleep for 1 second, undercutting our average. - # - # So below 10 processes, we special case and ensure the processes sleep closer to the average. - # In the example above, each process should schedule every 10 seconds on average. We special - # case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds. - # As we run more processes, the scheduling interval average will approach an even spread - # between 0 and poll interval so we don't need this artifical boost. - # - if process_count < 10 - # For small clusters, calculate a random interval that is ±50% the desired average. - poll_interval_average * rand + poll_interval_average.to_f / 2 - else - # With 10+ processes, we should have enough randomness to get decent polling - # across the entire timespan - poll_interval_average * rand - end - end - - # We do our best to tune the poll interval to the size of the active Sidekiq - # cluster. If you have 30 processes and poll every 15 seconds, that means one - # Sidekiq is checking Redis every 0.5 seconds - way too often for most people - # and really bad if the retry or scheduled sets are large. - # - # Instead try to avoid polling more than once every 15 seconds. If you have - # 30 Sidekiq processes, we'll poll every 30 * 15 or 450 seconds. - # To keep things statistically random, we'll sleep a random amount between - # 225 and 675 seconds for each poll or 450 seconds on average. Otherwise restarting - # all your Sidekiq processes at the same time will lead to them all polling at - # the same time: the thundering herd problem. - # - # We only do this if poll_interval_average is unset (the default). - def poll_interval_average - Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval - end - - # Calculates an average poll interval based on the number of known Sidekiq processes. - # This minimizes a single point of failure by dispersing check-ins but without taxing - # Redis if you run many Sidekiq processes. - def scaled_poll_interval - process_count * Sidekiq.options[:average_scheduled_poll_interval] - end - - def process_count - # The work buried within Sidekiq::ProcessSet#cleanup can be - # expensive at scale. Cut it down by 90% with this counter. - # NB: This method is only called by the scheduler thread so we - # don't need to worry about the thread safety of +=. - pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size - pcount = 1 if pcount == 0 - @count_calls += 1 - pcount - end - - def initial_wait - # Have all processes sleep between 5-15 seconds. 10 seconds - # to give time for the heartbeat to register (if the poll interval is going to be calculated by the number - # of workers), and 5 random seconds to ensure they don't all hit Redis at the same time. - total = 0 - total += INITIAL_WAIT unless Sidekiq.options[:poll_interval_average] - total += (5 * rand) - - @sleeper.pop(total) - rescue Timeout::Error - end - end - end -end diff --git a/lib/sidekiq/sd_notify.rb b/lib/sidekiq/sd_notify.rb deleted file mode 100644 index 26f6863c..00000000 --- a/lib/sidekiq/sd_notify.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -# The MIT License -# -# Copyright (c) 2017, 2018, 2019, 2020 Agis Anastasopoulos -# -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -# This is a copy of https://github.com/agis/ruby-sdnotify as of commit a7d52ee -# The only changes made was "rehoming" it within the Sidekiq module to avoid -# namespace collisions and applying standard's code formatting style. - -require "socket" - -# SdNotify is a pure-Ruby implementation of sd_notify(3). It can be used to -# notify systemd about state changes. Methods of this package are no-op on -# non-systemd systems (eg. Darwin). -# -# The API maps closely to the original implementation of sd_notify(3), -# therefore be sure to check the official man pages prior to using SdNotify. -# -# @see https://www.freedesktop.org/software/systemd/man/sd_notify.html -module Sidekiq - module SdNotify - # Exception raised when there's an error writing to the notification socket - class NotifyError < RuntimeError; end - - READY = "READY=1" - RELOADING = "RELOADING=1" - STOPPING = "STOPPING=1" - STATUS = "STATUS=" - ERRNO = "ERRNO=" - MAINPID = "MAINPID=" - WATCHDOG = "WATCHDOG=1" - FDSTORE = "FDSTORE=1" - - def self.ready(unset_env = false) - notify(READY, unset_env) - end - - def self.reloading(unset_env = false) - notify(RELOADING, unset_env) - end - - def self.stopping(unset_env = false) - notify(STOPPING, unset_env) - end - - # @param status [String] a custom status string that describes the current - # state of the service - def self.status(status, unset_env = false) - notify("#{STATUS}#{status}", unset_env) - end - - # @param errno [Integer] - def self.errno(errno, unset_env = false) - notify("#{ERRNO}#{errno}", unset_env) - end - - # @param pid [Integer] - def self.mainpid(pid, unset_env = false) - notify("#{MAINPID}#{pid}", unset_env) - end - - def self.watchdog(unset_env = false) - notify(WATCHDOG, unset_env) - end - - def self.fdstore(unset_env = false) - notify(FDSTORE, unset_env) - end - - # @return [Boolean] true if the service manager expects watchdog keep-alive - # notification messages to be sent from this process. - # - # If the $WATCHDOG_USEC environment variable is set, - # and the $WATCHDOG_PID variable is unset or set to the PID of the current - # process - # - # @note Unlike sd_watchdog_enabled(3), this method does not mutate the - # environment. - def self.watchdog? - wd_usec = ENV["WATCHDOG_USEC"] - wd_pid = ENV["WATCHDOG_PID"] - - return false unless wd_usec - - begin - wd_usec = Integer(wd_usec) - rescue - return false - end - - return false if wd_usec <= 0 - return true if !wd_pid || wd_pid == $$.to_s - - false - end - - # Notify systemd with the provided state, via the notification socket, if - # any. - # - # Generally this method will be used indirectly through the other methods - # of the library. - # - # @param state [String] - # @param unset_env [Boolean] - # - # @return [Fixnum, nil] the number of bytes written to the notification - # socket or nil if there was no socket to report to (eg. the program wasn't - # started by systemd) - # - # @raise [NotifyError] if there was an error communicating with the systemd - # socket - # - # @see https://www.freedesktop.org/software/systemd/man/sd_notify.html - def self.notify(state, unset_env = false) - sock = ENV["NOTIFY_SOCKET"] - - return nil unless sock - - ENV.delete("NOTIFY_SOCKET") if unset_env - - begin - Addrinfo.unix(sock, :DGRAM).connect do |s| - s.close_on_exec = true - s.write(state) - end - rescue => e - raise NotifyError, "#{e.class}: #{e.message}", e.backtrace - end - end - end -end diff --git a/lib/sidekiq/systemd.rb b/lib/sidekiq/systemd.rb deleted file mode 100644 index 7e6472f5..00000000 --- a/lib/sidekiq/systemd.rb +++ /dev/null @@ -1,24 +0,0 @@ -# -# Sidekiq's systemd integration allows Sidekiq to inform systemd: -# 1. when it has successfully started -# 2. when it is starting shutdown -# 3. periodically for a liveness check with a watchdog thread -# -module Sidekiq - def self.start_watchdog - usec = Integer(ENV["WATCHDOG_USEC"]) - return Sidekiq.logger.error("systemd Watchdog too fast: " + usec) if usec < 1_000_000 - - sec_f = usec / 1_000_000.0 - # "It is recommended that a daemon sends a keep-alive notification message - # to the service manager every half of the time returned here." - ping_f = sec_f / 2 - Sidekiq.logger.info "Pinging systemd watchdog every #{ping_f.round(1)} sec" - Thread.new do - loop do - sleep ping_f - Sidekiq::SdNotify.watchdog - end - end - end -end diff --git a/lib/sidekiq/testing.rb b/lib/sidekiq/testing.rb deleted file mode 100644 index 25c0ee28..00000000 --- a/lib/sidekiq/testing.rb +++ /dev/null @@ -1,342 +0,0 @@ -# frozen_string_literal: true - -require "securerandom" -require "sidekiq" - -module Sidekiq - class Testing - class << self - attr_accessor :__test_mode - - def __set_test_mode(mode) - if block_given? - current_mode = __test_mode - begin - self.__test_mode = mode - yield - ensure - self.__test_mode = current_mode - end - else - self.__test_mode = mode - end - end - - def disable!(&block) - __set_test_mode(:disable, &block) - end - - def fake!(&block) - __set_test_mode(:fake, &block) - end - - def inline!(&block) - __set_test_mode(:inline, &block) - end - - def enabled? - __test_mode != :disable - end - - def disabled? - __test_mode == :disable - end - - def fake? - __test_mode == :fake - end - - def inline? - __test_mode == :inline - end - - def server_middleware - @server_chain ||= Middleware::Chain.new - yield @server_chain if block_given? - @server_chain - end - - def constantize(str) - names = str.split("::") - names.shift if names.empty? || names.first.empty? - - names.inject(Object) do |constant, name| - constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name) - end - end - end - end - - # Default to fake testing to keep old behavior - Sidekiq::Testing.fake! - - class EmptyQueueError < RuntimeError; end - - module TestingClient - def raw_push(payloads) - if Sidekiq::Testing.fake? - payloads.each do |job| - job = Sidekiq.load_json(Sidekiq.dump_json(job)) - job["enqueued_at"] = Time.now.to_f unless job["at"] - Queues.push(job["queue"], job["class"], job) - end - true - elsif Sidekiq::Testing.inline? - payloads.each do |job| - klass = Sidekiq::Testing.constantize(job["class"]) - job["id"] ||= SecureRandom.hex(12) - job_hash = Sidekiq.load_json(Sidekiq.dump_json(job)) - klass.process_job(job_hash) - end - true - else - super - end - end - end - - Sidekiq::Client.prepend TestingClient - - module Queues - ## - # The Queues class is only for testing the fake queue implementation. - # There are 2 data structures involved in tandem. This is due to the - # Rspec syntax of change(QueueWorker.jobs, :size). It keeps a reference - # to the array. Because the array was dervied from a filter of the total - # jobs enqueued, it appeared as though the array didn't change. - # - # To solve this, we'll keep 2 hashes containing the jobs. One with keys based - # on the queue, and another with keys of the worker names, so the array for - # QueueWorker.jobs is a straight reference to a real array. - # - # Queue-based hash: - # - # { - # "default"=>[ - # { - # "class"=>"TestTesting::QueueWorker", - # "args"=>[1, 2], - # "retry"=>true, - # "queue"=>"default", - # "jid"=>"abc5b065c5c4b27fc1102833", - # "created_at"=>1447445554.419934 - # } - # ] - # } - # - # Worker-based hash: - # - # { - # "TestTesting::QueueWorker"=>[ - # { - # "class"=>"TestTesting::QueueWorker", - # "args"=>[1, 2], - # "retry"=>true, - # "queue"=>"default", - # "jid"=>"abc5b065c5c4b27fc1102833", - # "created_at"=>1447445554.419934 - # } - # ] - # } - # - # Example: - # - # require 'sidekiq/testing' - # - # assert_equal 0, Sidekiq::Queues["default"].size - # HardWorker.perform_async(:something) - # assert_equal 1, Sidekiq::Queues["default"].size - # assert_equal :something, Sidekiq::Queues["default"].first['args'][0] - # - # You can also clear all workers' jobs: - # - # assert_equal 0, Sidekiq::Queues["default"].size - # HardWorker.perform_async(:something) - # Sidekiq::Queues.clear_all - # assert_equal 0, Sidekiq::Queues["default"].size - # - # This can be useful to make sure jobs don't linger between tests: - # - # RSpec.configure do |config| - # config.before(:each) do - # Sidekiq::Queues.clear_all - # end - # end - # - class << self - def [](queue) - jobs_by_queue[queue] - end - - def push(queue, klass, job) - jobs_by_queue[queue] << job - jobs_by_worker[klass] << job - end - - def jobs_by_queue - @jobs_by_queue ||= Hash.new { |hash, key| hash[key] = [] } - end - - def jobs_by_worker - @jobs_by_worker ||= Hash.new { |hash, key| hash[key] = [] } - end - - def delete_for(jid, queue, klass) - jobs_by_queue[queue.to_s].delete_if { |job| job["jid"] == jid } - jobs_by_worker[klass].delete_if { |job| job["jid"] == jid } - end - - def clear_for(queue, klass) - jobs_by_queue[queue].clear - jobs_by_worker[klass].clear - end - - def clear_all - jobs_by_queue.clear - jobs_by_worker.clear - end - end - end - - module Worker - ## - # The Sidekiq testing infrastructure overrides perform_async - # so that it does not actually touch the network. Instead it - # stores the asynchronous jobs in a per-class array so that - # their presence/absence can be asserted by your tests. - # - # This is similar to ActionMailer's :test delivery_method and its - # ActionMailer::Base.deliveries array. - # - # Example: - # - # require 'sidekiq/testing' - # - # assert_equal 0, HardWorker.jobs.size - # HardWorker.perform_async(:something) - # assert_equal 1, HardWorker.jobs.size - # assert_equal :something, HardWorker.jobs[0]['args'][0] - # - # assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size - # MyMailer.delay.send_welcome_email('foo@example.com') - # assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size - # - # You can also clear and drain all workers' jobs: - # - # assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size - # assert_equal 0, Sidekiq::Extensions::DelayedModel.jobs.size - # - # MyMailer.delay.send_welcome_email('foo@example.com') - # MyModel.delay.do_something_hard - # - # assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size - # assert_equal 1, Sidekiq::Extensions::DelayedModel.jobs.size - # - # Sidekiq::Worker.clear_all # or .drain_all - # - # assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size - # assert_equal 0, Sidekiq::Extensions::DelayedModel.jobs.size - # - # This can be useful to make sure jobs don't linger between tests: - # - # RSpec.configure do |config| - # config.before(:each) do - # Sidekiq::Worker.clear_all - # end - # end - # - # or for acceptance testing, i.e. with cucumber: - # - # AfterStep do - # Sidekiq::Worker.drain_all - # end - # - # When I sign up as "foo@example.com" - # Then I should receive a welcome email to "foo@example.com" - # - module ClassMethods - # Queue for this worker - def queue - get_sidekiq_options["queue"] - end - - # Jobs queued for this worker - def jobs - Queues.jobs_by_worker[to_s] - end - - # Clear all jobs for this worker - def clear - Queues.clear_for(queue, to_s) - end - - # Drain and run all jobs for this worker - def drain - while jobs.any? - next_job = jobs.first - Queues.delete_for(next_job["jid"], next_job["queue"], to_s) - process_job(next_job) - end - end - - # Pop out a single job and perform it - def perform_one - raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty? - next_job = jobs.first - Queues.delete_for(next_job["jid"], queue, to_s) - process_job(next_job) - end - - def process_job(job) - worker = new - worker.jid = job["jid"] - worker.bid = job["bid"] if worker.respond_to?(:bid=) - Sidekiq::Testing.server_middleware.invoke(worker, job, job["queue"]) do - execute_job(worker, job["args"]) - end - end - - def execute_job(worker, args) - worker.perform(*args) - end - end - - class << self - def jobs # :nodoc: - Queues.jobs_by_queue.values.flatten - end - - # Clear all queued jobs across all workers - def clear_all - Queues.clear_all - end - - # Drain all queued jobs across all workers - def drain_all - while jobs.any? - worker_classes = jobs.map { |job| job["class"] }.uniq - - worker_classes.each do |worker_class| - Sidekiq::Testing.constantize(worker_class).drain - end - end - end - end - end - - module TestingExtensions - def jobs_for(klass) - jobs.select do |job| - marshalled = job["args"][0] - marshalled.index(klass.to_s) && YAML.load(marshalled)[0] == klass - end - end - end - - Sidekiq::Extensions::DelayedMailer.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedMailer) - Sidekiq::Extensions::DelayedModel.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedModel) -end - -if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING - warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1) -end diff --git a/lib/sidekiq/testing/inline.rb b/lib/sidekiq/testing/inline.rb deleted file mode 100644 index d83e929c..00000000 --- a/lib/sidekiq/testing/inline.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq/testing" - -## -# The Sidekiq inline infrastructure overrides perform_async so that it -# actually calls perform instead. This allows workers to be run inline in a -# testing environment. -# -# This is similar to `Resque.inline = true` functionality. -# -# Example: -# -# require 'sidekiq/testing/inline' -# -# $external_variable = 0 -# -# class ExternalWorker -# include Sidekiq::Worker -# -# def perform -# $external_variable = 1 -# end -# end -# -# assert_equal 0, $external_variable -# ExternalWorker.perform_async -# assert_equal 1, $external_variable -# -Sidekiq::Testing.inline! diff --git a/lib/sidekiq/util.rb b/lib/sidekiq/util.rb deleted file mode 100644 index 42d4055a..00000000 --- a/lib/sidekiq/util.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -require "forwardable" -require "socket" -require "securerandom" -require "sidekiq/exception_handler" - -module Sidekiq - ## - # This module is part of Sidekiq core and not intended for extensions. - # - - class RingBuffer - include Enumerable - extend Forwardable - def_delegators :@buf, :[], :each, :size - - def initialize(size, default = 0) - @size = size - @buf = Array.new(size, default) - @index = 0 - end - - def <<(element) - @buf[@index % @size] = element - @index += 1 - element - end - - def buffer - @buf - end - - def reset(default = 0) - @buf.fill(default) - end - end - - module Util - include ExceptionHandler - - # hack for quicker development / testing environment #2774 - PAUSE_TIME = $stdout.tty? ? 0.1 : 0.5 - - # Wait for the orblock to be true or the deadline passed. - def wait_for(deadline, &condblock) - remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - while remaining > PAUSE_TIME - return if condblock.call - sleep PAUSE_TIME - remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - end - end - - def watchdog(last_words) - yield - rescue Exception => ex - handle_exception(ex, {context: last_words}) - raise ex - end - - def safe_thread(name, &block) - Thread.new do - Thread.current.name = name - watchdog(name, &block) - end - end - - def logger - Sidekiq.logger - end - - def redis(&block) - Sidekiq.redis(&block) - end - - def tid - Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36) - end - - def hostname - ENV["DYNO"] || Socket.gethostname - end - - def process_nonce - @@process_nonce ||= SecureRandom.hex(6) - end - - def identity - @@identity ||= "#{hostname}:#{::Process.pid}:#{process_nonce}" - end - - def fire_event(event, options = {}) - reverse = options[:reverse] - reraise = options[:reraise] - - arr = Sidekiq.options[:lifecycle_events][event] - arr.reverse! if reverse - arr.each do |block| - block.call - rescue => ex - handle_exception(ex, {context: "Exception during Sidekiq lifecycle event.", event: event}) - raise ex if reraise - end - arr.clear - end - end -end diff --git a/lib/sidekiq/version.rb b/lib/sidekiq/version.rb deleted file mode 100644 index a3b28e61..00000000 --- a/lib/sidekiq/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -module Sidekiq - VERSION = "6.4.1" -end diff --git a/lib/sidekiq/web.rb b/lib/sidekiq/web.rb deleted file mode 100644 index 04e7ef45..00000000 --- a/lib/sidekiq/web.rb +++ /dev/null @@ -1,169 +0,0 @@ -# frozen_string_literal: true - -require "erb" - -require "sidekiq" -require "sidekiq/api" -require "sidekiq/paginator" -require "sidekiq/web/helpers" - -require "sidekiq/web/router" -require "sidekiq/web/action" -require "sidekiq/web/application" -require "sidekiq/web/csrf_protection" - -require "rack/content_length" -require "rack/builder" -require "rack/static" - -module Sidekiq - class Web - ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../web") - VIEWS = "#{ROOT}/views" - LOCALES = ["#{ROOT}/locales"] - LAYOUT = "#{VIEWS}/layout.erb" - ASSETS = "#{ROOT}/assets" - - DEFAULT_TABS = { - "Dashboard" => "", - "Busy" => "busy", - "Queues" => "queues", - "Retries" => "retries", - "Scheduled" => "scheduled", - "Dead" => "morgue" - } - - class << self - def settings - self - end - - def default_tabs - DEFAULT_TABS - end - - def custom_tabs - @custom_tabs ||= {} - end - alias_method :tabs, :custom_tabs - - def locales - @locales ||= LOCALES - end - - def views - @views ||= VIEWS - end - - def enable(*opts) - opts.each { |key| set(key, true) } - end - - def disable(*opts) - opts.each { |key| set(key, false) } - end - - def middlewares - @middlewares ||= [] - end - - def use(*args, &block) - middlewares << [args, block] - end - - def set(attribute, value) - send(:"#{attribute}=", value) - end - - def sessions=(val) - puts "WARNING: Sidekiq::Web.sessions= is no longer relevant and will be removed in Sidekiq 7.0. #{caller(1..1).first}" - end - - def session_secret=(val) - puts "WARNING: Sidekiq::Web.session_secret= is no longer relevant and will be removed in Sidekiq 7.0. #{caller(1..1).first}" - end - - attr_accessor :app_url, :redis_pool - attr_writer :locales, :views - end - - def self.inherited(child) - child.app_url = app_url - child.redis_pool = redis_pool - end - - def settings - self.class.settings - end - - def middlewares - @middlewares ||= self.class.middlewares - end - - def use(*args, &block) - middlewares << [args, block] - end - - def call(env) - app.call(env) - end - - def self.call(env) - @app ||= new - @app.call(env) - end - - def app - @app ||= build - end - - def enable(*opts) - opts.each { |key| set(key, true) } - end - - def disable(*opts) - opts.each { |key| set(key, false) } - end - - def set(attribute, value) - send(:"#{attribute}=", value) - end - - def sessions=(val) - puts "Sidekiq::Web#sessions= is no longer relevant and will be removed in Sidekiq 7.0. #{caller[2..2].first}" - end - - def self.register(extension) - extension.registered(WebApplication) - end - - private - - def build - klass = self.class - m = middlewares - - rules = [] - rules = [[:all, {"Cache-Control" => "public, max-age=86400"}]] unless ENV["SIDEKIQ_WEB_TESTING"] - - ::Rack::Builder.new do - use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"], - root: ASSETS, - cascade: true, - header_rules: rules - m.each { |middleware, block| use(*middleware, &block) } - use Sidekiq::Web::CsrfProtection unless $TESTING - run WebApplication.new(klass) - end - end - end - - Sidekiq::WebApplication.helpers WebHelpers - Sidekiq::WebApplication.helpers Sidekiq::Paginator - - Sidekiq::WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _render - #{ERB.new(File.read(Web::LAYOUT)).src} - end - RUBY -end diff --git a/lib/sidekiq/web/action.rb b/lib/sidekiq/web/action.rb deleted file mode 100644 index 48133685..00000000 --- a/lib/sidekiq/web/action.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -module Sidekiq - class WebAction - RACK_SESSION = "rack.session" - - attr_accessor :env, :block, :type - - def settings - Web.settings - end - - def request - @request ||= ::Rack::Request.new(env) - end - - def halt(res) - throw :halt, [res, {"Content-Type" => "text/plain"}, [res.to_s]] - end - - def redirect(location) - throw :halt, [302, {"Location" => "#{request.base_url}#{location}"}, []] - end - - def params - indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key } - - indifferent_hash.merge! request.params - route_params.each { |k, v| indifferent_hash[k.to_s] = v } - - indifferent_hash - end - - def route_params - env[WebRouter::ROUTE_PARAMS] - end - - def session - env[RACK_SESSION] - end - - def erb(content, options = {}) - if content.is_a? Symbol - unless respond_to?(:"_erb_#{content}") - src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src - WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _erb_#{content} - #{src} - end - RUBY - end - end - - if @_erb - _erb(content, options[:locals]) - else - @_erb = true - content = _erb(content, options[:locals]) - - _render { content } - end - end - - def render(engine, content, options = {}) - raise "Only erb templates are supported" if engine != :erb - - erb(content, options) - end - - def json(payload) - [200, {"Content-Type" => "application/json", "Cache-Control" => "private, no-store"}, [Sidekiq.dump_json(payload)]] - end - - def initialize(env, block) - @_erb = false - @env = env - @block = block - @files ||= {} - end - - private - - def _erb(file, locals) - locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k } - - if file.is_a?(String) - ERB.new(file).result(binding) - else - send(:"_erb_#{file}") - end - end - end -end diff --git a/lib/sidekiq/web/application.rb b/lib/sidekiq/web/application.rb deleted file mode 100644 index 8debac91..00000000 --- a/lib/sidekiq/web/application.rb +++ /dev/null @@ -1,366 +0,0 @@ -# frozen_string_literal: true - -module Sidekiq - class WebApplication - extend WebRouter - - REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human] - CSP_HEADER = [ - "default-src 'self' https: http:", - "child-src 'self'", - "connect-src 'self' https: http: wss: ws:", - "font-src 'self' https: http:", - "frame-src 'self'", - "img-src 'self' https: http: data:", - "manifest-src 'self'", - "media-src 'self'", - "object-src 'none'", - "script-src 'self' https: http: 'unsafe-inline'", - "style-src 'self' https: http: 'unsafe-inline'", - "worker-src 'self'", - "base-uri 'self'" - ].join("; ").freeze - - def initialize(klass) - @klass = klass - end - - def settings - @klass.settings - end - - def self.settings - Sidekiq::Web.settings - end - - def self.tabs - Sidekiq::Web.tabs - end - - def self.set(key, val) - # nothing, backwards compatibility - end - - head "/" do - # HEAD / is the cheapest heartbeat possible, - # it hits Redis to ensure connectivity - Sidekiq.redis { |c| c.llen("queue:default") } - "" - end - - get "/" do - @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k } - days = (params["days"] || 30).to_i - return halt(401) if days < 1 || days > 180 - - stats_history = Sidekiq::Stats::History.new(days) - @processed_history = stats_history.processed - @failed_history = stats_history.failed - - erb(:dashboard) - end - - get "/busy" do - erb(:busy) - end - - post "/busy" do - if params["identity"] - p = Sidekiq::Process.new("identity" => params["identity"]) - p.quiet! if params["quiet"] - p.stop! if params["stop"] - else - processes.each do |pro| - pro.quiet! if params["quiet"] - pro.stop! if params["stop"] - end - end - - redirect "#{root_path}busy" - end - - get "/queues" do - @queues = Sidekiq::Queue.all - - erb(:queues) - end - - QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i - - get "/queues/:name" do - @name = route_params[:name] - - halt(404) if !@name || @name !~ QUEUE_NAME - - @count = (params["count"] || 25).to_i - @queue = Sidekiq::Queue.new(@name) - (@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc") - @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) } - - erb(:queue) - end - - post "/queues/:name" do - queue = Sidekiq::Queue.new(route_params[:name]) - - if Sidekiq.pro? && params["pause"] - queue.pause! - elsif Sidekiq.pro? && params["unpause"] - queue.unpause! - else - queue.clear - end - - redirect "#{root_path}queues" - end - - post "/queues/:name/delete" do - name = route_params[:name] - Sidekiq::JobRecord.new(params["key_val"], name).delete - - redirect_with_query("#{root_path}queues/#{CGI.escape(name)}") - end - - get "/morgue" do - @count = (params["count"] || 25).to_i - (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true) - @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) } - - erb(:morgue) - end - - get "/morgue/:key" do - key = route_params[:key] - halt(404) unless key - - @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first - - if @dead.nil? - redirect "#{root_path}morgue" - else - erb(:dead) - end - end - - post "/morgue" do - redirect(request.path) unless params["key"] - - params["key"].each do |key| - job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first - retry_or_delete_or_kill job, params if job - end - - redirect_with_query("#{root_path}morgue") - end - - post "/morgue/all/delete" do - Sidekiq::DeadSet.new.clear - - redirect "#{root_path}morgue" - end - - post "/morgue/all/retry" do - Sidekiq::DeadSet.new.retry_all - - redirect "#{root_path}morgue" - end - - post "/morgue/:key" do - key = route_params[:key] - halt(404) unless key - - job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first - retry_or_delete_or_kill job, params if job - - redirect_with_query("#{root_path}morgue") - end - - get "/retries" do - @count = (params["count"] || 25).to_i - (@current_page, @total_size, @retries) = page("retry", params["page"], @count) - @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) } - - erb(:retries) - end - - get "/retries/:key" do - @retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first - - if @retry.nil? - redirect "#{root_path}retries" - else - erb(:retry) - end - end - - post "/retries" do - redirect(request.path) unless params["key"] - - params["key"].each do |key| - job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first - retry_or_delete_or_kill job, params if job - end - - redirect_with_query("#{root_path}retries") - end - - post "/retries/all/delete" do - Sidekiq::RetrySet.new.clear - - redirect "#{root_path}retries" - end - - post "/retries/all/retry" do - Sidekiq::RetrySet.new.retry_all - - redirect "#{root_path}retries" - end - - post "/retries/all/kill" do - Sidekiq::RetrySet.new.kill_all - - redirect "#{root_path}retries" - end - - post "/retries/:key" do - job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first - - retry_or_delete_or_kill job, params if job - - redirect_with_query("#{root_path}retries") - end - - get "/scheduled" do - @count = (params["count"] || 25).to_i - (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count) - @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) } - - erb(:scheduled) - end - - get "/scheduled/:key" do - @job = Sidekiq::ScheduledSet.new.fetch(*parse_params(route_params[:key])).first - - if @job.nil? - redirect "#{root_path}scheduled" - else - erb(:scheduled_job_info) - end - end - - post "/scheduled" do - redirect(request.path) unless params["key"] - - params["key"].each do |key| - job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first - delete_or_add_queue job, params if job - end - - redirect_with_query("#{root_path}scheduled") - end - - post "/scheduled/:key" do - key = route_params[:key] - halt(404) unless key - - job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first - delete_or_add_queue job, params if job - - redirect_with_query("#{root_path}scheduled") - end - - get "/dashboard/stats" do - redirect "#{root_path}stats" - end - - get "/stats" do - sidekiq_stats = Sidekiq::Stats.new - redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k } - json( - sidekiq: { - processed: sidekiq_stats.processed, - failed: sidekiq_stats.failed, - busy: sidekiq_stats.workers_size, - processes: sidekiq_stats.processes_size, - enqueued: sidekiq_stats.enqueued, - scheduled: sidekiq_stats.scheduled_size, - retries: sidekiq_stats.retry_size, - dead: sidekiq_stats.dead_size, - default_latency: sidekiq_stats.default_queue_latency - }, - redis: redis_stats, - server_utc_time: server_utc_time - ) - end - - get "/stats/queues" do - json Sidekiq::Stats::Queues.new.lengths - end - - def call(env) - action = self.class.match(env) - return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found"]] unless action - - app = @klass - resp = catch(:halt) do - self.class.run_befores(app, action) - action.instance_exec env, &action.block - ensure - self.class.run_afters(app, action) - end - - case resp - when Array - # redirects go here - resp - else - # rendered content goes here - headers = { - "Content-Type" => "text/html", - "Cache-Control" => "private, no-store", - "Content-Language" => action.locale, - "Content-Security-Policy" => CSP_HEADER - } - # we'll let Rack calculate Content-Length for us. - [200, headers, [resp]] - end - end - - def self.helpers(mod = nil, &block) - if block - WebAction.class_eval(&block) - else - WebAction.send(:include, mod) - end - end - - def self.before(path = nil, &block) - befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block] - end - - def self.after(path = nil, &block) - afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block] - end - - def self.run_befores(app, action) - run_hooks(befores, app, action) - end - - def self.run_afters(app, action) - run_hooks(afters, app, action) - end - - def self.run_hooks(hooks, app, action) - hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] } - .each { |_, b| action.instance_exec(action.env, app, &b) } - end - - def self.befores - @befores ||= [] - end - - def self.afters - @afters ||= [] - end - end -end diff --git a/lib/sidekiq/web/csrf_protection.rb b/lib/sidekiq/web/csrf_protection.rb deleted file mode 100644 index 2a260a59..00000000 --- a/lib/sidekiq/web/csrf_protection.rb +++ /dev/null @@ -1,180 +0,0 @@ -# frozen_string_literal: true - -# this file originally based on authenticity_token.rb from the sinatra/rack-protection project -# -# The MIT License (MIT) -# -# Copyright (c) 2011-2017 Konstantin Haase -# Copyright (c) 2015-2017 Zachary Scott -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# 'Software'), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -require "securerandom" -require "base64" -require "rack/request" - -module Sidekiq - class Web - class CsrfProtection - def initialize(app, options = nil) - @app = app - end - - def call(env) - accept?(env) ? admit(env) : deny(env) - end - - private - - def admit(env) - # On each successful request, we create a fresh masked token - # which will be used in any forms rendered for this request. - s = session(env) - s[:csrf] ||= SecureRandom.base64(TOKEN_LENGTH) - env[:csrf_token] = mask_token(s[:csrf]) - @app.call(env) - end - - def safe?(env) - %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"] - end - - def logger(env) - @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"])) - end - - def deny(env) - logger(env).warn "attack prevented by #{self.class}" - [403, {"Content-Type" => "text/plain"}, ["Forbidden"]] - end - - def session(env) - env["rack.session"] || fail(<<~EOM) - Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app, - make sure you mount Sidekiq::Web *inside* your application routes: - - - Rails.application.routes.draw do - mount Sidekiq::Web => "/sidekiq" - .... - end - - - If this is a Rails app in API mode, you need to enable sessions. - - https://guides.rubyonrails.org/api_app.html#using-session-middlewares - - If this is a bare Rack app, use a session middleware before Sidekiq::Web: - - # first, use IRB to create a shared secret key for sessions and commit it - require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) } - - # now use the secret with a session cookie middleware - use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400 - run Sidekiq::Web - - EOM - end - - def accept?(env) - return true if safe?(env) - - giventoken = ::Rack::Request.new(env).params["authenticity_token"] - valid_token?(env, giventoken) - end - - TOKEN_LENGTH = 32 - - # Checks that the token given to us as a parameter matches - # the token stored in the session. - def valid_token?(env, giventoken) - return false if giventoken.nil? || giventoken.empty? - - begin - token = decode_token(giventoken) - rescue ArgumentError # client input is invalid - return false - end - - sess = session(env) - localtoken = sess[:csrf] - - # Checks that Rack::Session::Cookie actualy contains the csrf toekn - return false if localtoken.nil? - - # Rotate the session token after every use - sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH) - - # See if it's actually a masked token or not. We should be able - # to handle any unmasked tokens that we've issued without error. - - if unmasked_token?(token) - compare_with_real_token token, localtoken - elsif masked_token?(token) - unmasked = unmask_token(token) - compare_with_real_token unmasked, localtoken - else - false # Token is malformed - end - end - - # Creates a masked version of the authenticity token that varies - # on each request. The masking is used to mitigate SSL attacks - # like BREACH. - def mask_token(token) - token = decode_token(token) - one_time_pad = SecureRandom.random_bytes(token.length) - encrypted_token = xor_byte_strings(one_time_pad, token) - masked_token = one_time_pad + encrypted_token - Base64.strict_encode64(masked_token) - end - - # Essentially the inverse of +mask_token+. - def unmask_token(masked_token) - # Split the token into the one-time pad and the encrypted - # value and decrypt it - token_length = masked_token.length / 2 - one_time_pad = masked_token[0...token_length] - encrypted_token = masked_token[token_length..-1] - xor_byte_strings(one_time_pad, encrypted_token) - end - - def unmasked_token?(token) - token.length == TOKEN_LENGTH - end - - def masked_token?(token) - token.length == TOKEN_LENGTH * 2 - end - - def compare_with_real_token(token, local) - ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s) - end - - def decode_token(token) - Base64.strict_decode64(token) - end - - def xor_byte_strings(s1, s2) - s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*") - end - end - end -end diff --git a/lib/sidekiq/web/helpers.rb b/lib/sidekiq/web/helpers.rb deleted file mode 100644 index efb5f1b3..00000000 --- a/lib/sidekiq/web/helpers.rb +++ /dev/null @@ -1,342 +0,0 @@ -# frozen_string_literal: true - -require "uri" -require "set" -require "yaml" -require "cgi" - -module Sidekiq - # This is not a public API - module WebHelpers - def strings(lang) - @strings ||= {} - - # Allow sidekiq-web extensions to add locale paths - # so extensions can be localized - @strings[lang] ||= settings.locales.each_with_object({}) do |path, global| - find_locale_files(lang).each do |file| - strs = YAML.load(File.open(file)) - global.merge!(strs[lang]) - end - end - end - - def singularize(str, count) - if count == 1 && str.respond_to?(:singularize) # rails - str.singularize - else - str - end - end - - def clear_caches - @strings = nil - @locale_files = nil - @available_locales = nil - end - - def locale_files - @locale_files ||= settings.locales.flat_map { |path| - Dir["#{path}/*.yml"] - } - end - - def available_locales - @available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq - end - - def find_locale_files(lang) - locale_files.select { |file| file =~ /\/#{lang}\.yml$/ } - end - - # This is a hook for a Sidekiq Pro feature. Please don't touch. - def filtering(*) - end - - # This view helper provide ability display you html code in - # to head of page. Example: - # - # <% add_to_head do %> - # - # - # <% end %> - # - def add_to_head - @head_html ||= [] - @head_html << yield.dup if block_given? - end - - def display_custom_head - @head_html.join if defined?(@head_html) - end - - def text_direction - get_locale["TextDirection"] || "ltr" - end - - def rtl? - text_direction == "rtl" - end - - # See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 - def user_preferred_languages - languages = env["HTTP_ACCEPT_LANGUAGE"] - languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language| - locale, quality = language.split(";q=", 2) - locale = nil if locale == "*" # Ignore wildcards - quality = quality ? quality.to_f : 1.0 - [locale, quality] - }.sort { |(_, left), (_, right)| - right <=> left - }.map(&:first).compact - end - - # Given an Accept-Language header like "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2" - # this method will try to best match the available locales to the user's preferred languages. - # - # Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb - def locale - @locale ||= begin - matched_locale = user_preferred_languages.map { |preferred| - preferred_language = preferred.split("-", 2).first - - lang_group = available_locales.select { |available| - preferred_language == available.split("-", 2).first - } - - lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length) - }.compact.first - - matched_locale || "en" - end - end - - # within is used by Sidekiq Pro - def display_tags(job, within = nil) - job.tags.map { |tag| - "#{::Rack::Utils.escape_html(tag)}" - }.join(" ") - end - - # mperham/sidekiq#3243 - def unfiltered? - yield unless env["PATH_INFO"].start_with?("/filter/") - end - - def get_locale - strings(locale) - end - - def t(msg, options = {}) - string = get_locale[msg] || strings("en")[msg] || msg - if options.empty? - string - else - string % options - end - end - - def sort_direction_label - params[:direction] == "asc" ? "↑" : "↓" - end - - def workers - @workers ||= Sidekiq::Workers.new - end - - def processes - @processes ||= Sidekiq::ProcessSet.new - end - - def stats - @stats ||= Sidekiq::Stats.new - end - - def redis_connection - Sidekiq.redis do |conn| - conn.connection[:id] - end - end - - def namespace - @ns ||= Sidekiq.redis { |conn| conn.respond_to?(:namespace) ? conn.namespace : nil } - end - - def redis_info - Sidekiq.redis_info - end - - def root_path - "#{env["SCRIPT_NAME"]}/" - end - - def current_path - @current_path ||= request.path_info.gsub(/^\//, "") - end - - def current_status - workers.size == 0 ? "idle" : "active" - end - - def relative_time(time) - stamp = time.getutc.iso8601 - %() - end - - def job_params(job, score) - "#{score}-#{job["jid"]}" - end - - def parse_params(params) - score, jid = params.split("-", 2) - [score.to_f, jid] - end - - SAFE_QPARAMS = %w[page direction] - - # Merge options with current params, filter safe params, and stringify to query string - def qparams(options) - stringified_options = options.transform_keys(&:to_s) - - to_query_string(params.merge(stringified_options)) - end - - def to_query_string(params) - params.map { |key, value| - SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next - }.compact.join("&") - end - - def truncate(text, truncate_after_chars = 2000) - truncate_after_chars && text.size > truncate_after_chars ? "#{text[0..truncate_after_chars]}..." : text - end - - def display_args(args, truncate_after_chars = 2000) - return "Invalid job payload, args is nil" if args.nil? - return "Invalid job payload, args must be an Array, not #{args.class.name}" unless args.is_a?(Array) - - begin - args.map { |arg| - h(truncate(to_display(arg), truncate_after_chars)) - }.join(", ") - rescue - "Illegal job arguments: #{h args.inspect}" - end - end - - def csrf_tag - "" - end - - def to_display(arg) - arg.inspect - rescue - begin - arg.to_s - rescue => ex - "Cannot display argument: [#{ex.class.name}] #{ex.message}" - end - end - - RETRY_JOB_KEYS = Set.new(%w[ - queue class args retry_count retried_at failed_at - jid error_message error_class backtrace - error_backtrace enqueued_at retry wrapped - created_at tags display_class - ]) - - def retry_extra_items(retry_job) - @retry_extra_items ||= {}.tap do |extra| - retry_job.item.each do |key, value| - extra[key] = value unless RETRY_JOB_KEYS.include?(key) - end - end - end - - def format_memory(rss_kb) - return "0" if rss_kb.nil? || rss_kb == 0 - - if rss_kb < 100_000 - "#{number_with_delimiter(rss_kb)} KB" - elsif rss_kb < 10_000_000 - "#{number_with_delimiter((rss_kb / 1024.0).to_i)} MB" - else - "#{number_with_delimiter((rss_kb / (1024.0 * 1024.0)).round(1))} GB" - end - end - - def number_with_delimiter(number) - return "" if number.nil? - - begin - Float(number) - rescue ArgumentError, TypeError - return number - end - - options = {delimiter: ",", separator: "."} - parts = number.to_s.to_str.split(".") - parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") - parts.join(options[:separator]) - end - - def h(text) - ::Rack::Utils.escape_html(text) - rescue ArgumentError => e - raise unless e.message.eql?("invalid byte sequence in UTF-8") - text.encode!("UTF-16", "UTF-8", invalid: :replace, replace: "").encode!("UTF-8", "UTF-16") - retry - end - - # Any paginated list that performs an action needs to redirect - # back to the proper page after performing that action. - def redirect_with_query(url) - r = request.referer - if r && r =~ /\?/ - ref = URI(r) - redirect("#{url}?#{ref.query}") - else - redirect url - end - end - - def environment_title_prefix - environment = Sidekiq.options[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" - - "[#{environment.upcase}] " unless environment == "production" - end - - def product_version - "Sidekiq v#{Sidekiq::VERSION}" - end - - def server_utc_time - Time.now.utc.strftime("%H:%M:%S UTC") - end - - def redis_connection_and_namespace - @redis_connection_and_namespace ||= begin - namespace_suffix = namespace.nil? ? "" : "##{namespace}" - "#{redis_connection}#{namespace_suffix}" - end - end - - def retry_or_delete_or_kill(job, params) - if params["retry"] - job.retry - elsif params["delete"] - job.delete - elsif params["kill"] - job.kill - end - end - - def delete_or_add_queue(job, params) - if params["delete"] - job.delete - elsif params["add_to_queue"] - job.add_to_queue - end - end - end -end diff --git a/lib/sidekiq/web/router.rb b/lib/sidekiq/web/router.rb deleted file mode 100644 index 34d586ea..00000000 --- a/lib/sidekiq/web/router.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -require "rack" - -module Sidekiq - module WebRouter - GET = "GET" - DELETE = "DELETE" - POST = "POST" - PUT = "PUT" - PATCH = "PATCH" - HEAD = "HEAD" - - ROUTE_PARAMS = "rack.route_params" - REQUEST_METHOD = "REQUEST_METHOD" - PATH_INFO = "PATH_INFO" - - def head(path, &block) - route(HEAD, path, &block) - end - - def get(path, &block) - route(GET, path, &block) - end - - def post(path, &block) - route(POST, path, &block) - end - - def put(path, &block) - route(PUT, path, &block) - end - - def patch(path, &block) - route(PATCH, path, &block) - end - - def delete(path, &block) - route(DELETE, path, &block) - end - - def route(method, path, &block) - @routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []} - - @routes[method] << WebRoute.new(method, path, block) - end - - def match(env) - request_method = env[REQUEST_METHOD] - path_info = ::Rack::Utils.unescape env[PATH_INFO] - - # There are servers which send an empty string when requesting the root. - # These servers should be ashamed of themselves. - path_info = "/" if path_info == "" - - @routes[request_method].each do |route| - params = route.match(request_method, path_info) - if params - env[ROUTE_PARAMS] = params - - return WebAction.new(env, route.block) - end - end - - nil - end - end - - class WebRoute - attr_accessor :request_method, :pattern, :block, :name - - NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/ - - def initialize(request_method, pattern, block) - @request_method = request_method - @pattern = pattern - @block = block - end - - def matcher - @matcher ||= compile - end - - def compile - if pattern.match?(NAMED_SEGMENTS_PATTERN) - p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)') - - Regexp.new("\\A#{p}\\Z") - else - pattern - end - end - - def match(request_method, path) - case matcher - when String - {} if path == matcher - else - path_match = path.match(matcher) - path_match&.named_captures&.transform_keys(&:to_sym) - end - end - end -end diff --git a/lib/sidekiq/worker.rb b/lib/sidekiq/worker.rb deleted file mode 100644 index 4e0140a8..00000000 --- a/lib/sidekiq/worker.rb +++ /dev/null @@ -1,362 +0,0 @@ -# frozen_string_literal: true - -require "sidekiq/client" - -module Sidekiq - ## - # Include this module in your worker class and you can easily create - # asynchronous jobs: - # - # class HardWorker - # include Sidekiq::Worker - # sidekiq_options queue: 'critical', retry: 5 - # - # def perform(*args) - # # do some work - # end - # end - # - # Then in your Rails app, you can do this: - # - # HardWorker.perform_async(1, 2, 3) - # - # Note that perform_async is a class method, perform is an instance method. - # - # Sidekiq::Worker also includes several APIs to provide compatibility with - # ActiveJob. - # - # class SomeWorker - # include Sidekiq::Worker - # queue_as :critical - # - # def perform(...) - # end - # end - # - # SomeWorker.set(wait_until: 1.hour).perform_async(123) - # - # Note that arguments passed to the job must still obey Sidekiq's - # best practice for simple, JSON-native data types. Sidekiq will not - # implement ActiveJob's more complex argument serialization. For - # this reason, we don't implement `perform_later` as our call semantics - # are very different. - # - module Worker - ## - # The Options module is extracted so we can include it in ActiveJob::Base - # and allow native AJs to configure Sidekiq features/internals. - module Options - def self.included(base) - base.extend(ClassMethods) - base.sidekiq_class_attribute :sidekiq_options_hash - base.sidekiq_class_attribute :sidekiq_retry_in_block - base.sidekiq_class_attribute :sidekiq_retries_exhausted_block - end - - module ClassMethods - ACCESSOR_MUTEX = Mutex.new - - ## - # Allows customization for this type of Worker. - # Legal options: - # - # queue - name of queue to use for this job type, default *default* - # retry - enable retries for this Worker in case of error during execution, - # *true* to use the default or *Integer* count - # backtrace - whether to save any error backtrace in the retry payload to display in web UI, - # can be true, false or an integer number of lines to save, default *false* - # - # In practice, any option is allowed. This is the main mechanism to configure the - # options for a specific job. - def sidekiq_options(opts = {}) - opts = opts.transform_keys(&:to_s) # stringify - self.sidekiq_options_hash = get_sidekiq_options.merge(opts) - end - - def sidekiq_retry_in(&block) - self.sidekiq_retry_in_block = block - end - - def sidekiq_retries_exhausted(&block) - self.sidekiq_retries_exhausted_block = block - end - - def get_sidekiq_options # :nodoc: - self.sidekiq_options_hash ||= Sidekiq.default_worker_options - end - - def sidekiq_class_attribute(*attrs) - instance_reader = true - instance_writer = true - - attrs.each do |name| - synchronized_getter = "__synchronized_#{name}" - - singleton_class.instance_eval do - undef_method(name) if method_defined?(name) || private_method_defined?(name) - end - - define_singleton_method(synchronized_getter) { nil } - singleton_class.class_eval do - private(synchronized_getter) - end - - define_singleton_method(name) { ACCESSOR_MUTEX.synchronize { send synchronized_getter } } - - ivar = "@#{name}" - - singleton_class.instance_eval do - m = "#{name}=" - undef_method(m) if method_defined?(m) || private_method_defined?(m) - end - define_singleton_method("#{name}=") do |val| - singleton_class.class_eval do - ACCESSOR_MUTEX.synchronize do - undef_method(synchronized_getter) if method_defined?(synchronized_getter) || private_method_defined?(synchronized_getter) - define_method(synchronized_getter) { val } - end - end - - if singleton_class? - class_eval do - undef_method(name) if method_defined?(name) || private_method_defined?(name) - define_method(name) do - if instance_variable_defined? ivar - instance_variable_get ivar - else - singleton_class.send name - end - end - end - end - val - end - - if instance_reader - undef_method(name) if method_defined?(name) || private_method_defined?(name) - define_method(name) do - if instance_variable_defined?(ivar) - instance_variable_get ivar - else - self.class.public_send name - end - end - end - - if instance_writer - m = "#{name}=" - undef_method(m) if method_defined?(m) || private_method_defined?(m) - attr_writer name - end - end - end - end - end - - attr_accessor :jid - - def self.included(base) - raise ArgumentError, "Sidekiq::Worker cannot be included in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" } - - base.include(Options) - base.extend(ClassMethods) - end - - def logger - Sidekiq.logger - end - - # This helper class encapsulates the set options for `set`, e.g. - # - # SomeWorker.set(queue: 'foo').perform_async(....) - # - class Setter - include Sidekiq::JobUtil - - def initialize(klass, opts) - @klass = klass - @opts = opts - - # ActiveJob compatibility - interval = @opts.delete(:wait_until) || @opts.delete(:wait) - at(interval) if interval - end - - def set(options) - interval = options.delete(:wait_until) || options.delete(:wait) - @opts.merge!(options) - at(interval) if interval - self - end - - def perform_async(*args) - if @opts["sync"] == true - perform_inline(*args) - else - @klass.client_push(@opts.merge("args" => args, "class" => @klass)) - end - end - - # Explicit inline execution of a job. Returns nil if the job did not - # execute, true otherwise. - def perform_inline(*args) - raw = @opts.merge("args" => args, "class" => @klass).transform_keys(&:to_s) - - # validate and normalize payload - item = normalize_item(raw) - queue = item["queue"] - - # run client-side middleware - result = Sidekiq.client_middleware.invoke(item["class"], item, queue, Sidekiq.redis_pool) do - item - end - return nil unless result - - # round-trip the payload via JSON - msg = Sidekiq.load_json(Sidekiq.dump_json(item)) - - # prepare the job instance - klass = msg["class"].constantize - job = klass.new - job.jid = msg["jid"] - job.bid = msg["bid"] if job.respond_to?(:bid) - - # run the job through server-side middleware - result = Sidekiq.server_middleware.invoke(job, msg, msg["queue"]) do - # perform it - job.perform(*msg["args"]) - true - end - return nil unless result - # jobs do not return a result. they should store any - # modified state. - true - end - alias_method :perform_sync, :perform_inline - - def perform_bulk(args, batch_size: 1_000) - hash = @opts.transform_keys(&:to_s) - pool = Thread.current[:sidekiq_via_pool] || @klass.get_sidekiq_options["pool"] || Sidekiq.redis_pool - client = Sidekiq::Client.new(pool) - result = args.each_slice(batch_size).flat_map do |slice| - client.push_bulk(hash.merge("class" => @klass, "args" => slice)) - end - - result.is_a?(Enumerator::Lazy) ? result.force : result - end - - # +interval+ must be a timestamp, numeric or something that acts - # numeric (like an activesupport time interval). - def perform_in(interval, *args) - at(interval).perform_async(*args) - end - alias_method :perform_at, :perform_in - - private - - def at(interval) - int = interval.to_f - now = Time.now.to_f - ts = (int < 1_000_000_000 ? now + int : int) - # Optimization to enqueue something now that is scheduled to go out now or in the past - @opts["at"] = ts if ts > now - self - end - end - - module ClassMethods - def delay(*args) - raise ArgumentError, "Do not call .delay on a Sidekiq::Worker class, call .perform_async" - end - - def delay_for(*args) - raise ArgumentError, "Do not call .delay_for on a Sidekiq::Worker class, call .perform_in" - end - - def delay_until(*args) - raise ArgumentError, "Do not call .delay_until on a Sidekiq::Worker class, call .perform_at" - end - - def queue_as(q) - sidekiq_options("queue" => q.to_s) - end - - def set(options) - Setter.new(self, options) - end - - def perform_async(*args) - Setter.new(self, {}).perform_async(*args) - end - - # Inline execution of job's perform method after passing through Sidekiq.client_middleware and Sidekiq.server_middleware - def perform_inline(*args) - Setter.new(self, {}).perform_inline(*args) - end - - ## - # Push a large number of jobs to Redis, while limiting the batch of - # each job payload to 1,000. This method helps cut down on the number - # of round trips to Redis, which can increase the performance of enqueueing - # large numbers of jobs. - # - # +items+ must be an Array of Arrays. - # - # For finer-grained control, use `Sidekiq::Client.push_bulk` directly. - # - # Example (3 Redis round trips): - # - # SomeWorker.perform_async(1) - # SomeWorker.perform_async(2) - # SomeWorker.perform_async(3) - # - # Would instead become (1 Redis round trip): - # - # SomeWorker.perform_bulk([[1], [2], [3]]) - # - def perform_bulk(*args, **kwargs) - Setter.new(self, {}).perform_bulk(*args, **kwargs) - end - - # +interval+ must be a timestamp, numeric or something that acts - # numeric (like an activesupport time interval). - def perform_in(interval, *args) - int = interval.to_f - now = Time.now.to_f - ts = (int < 1_000_000_000 ? now + int : int) - - item = {"class" => self, "args" => args} - - # Optimization to enqueue something now that is scheduled to go out now or in the past - item["at"] = ts if ts > now - - client_push(item) - end - alias_method :perform_at, :perform_in - - ## - # Allows customization for this type of Worker. - # Legal options: - # - # queue - use a named queue for this Worker, default 'default' - # retry - enable the RetryJobs middleware for this Worker, *true* to use the default - # or *Integer* count - # backtrace - whether to save any error backtrace in the retry payload to display in web UI, - # can be true, false or an integer number of lines to save, default *false* - # pool - use the given Redis connection pool to push this type of job to a given shard. - # - # In practice, any option is allowed. This is the main mechanism to configure the - # options for a specific job. - def sidekiq_options(opts = {}) - super - end - - def client_push(item) # :nodoc: - pool = Thread.current[:sidekiq_via_pool] || get_sidekiq_options["pool"] || Sidekiq.redis_pool - stringified_item = item.transform_keys(&:to_s) - - Sidekiq::Client.new(pool).push(stringified_item) - end - end - end -end diff --git a/myapp/Gemfile b/myapp/Gemfile index bbd59cef..1b6cc51e 100644 --- a/myapp/Gemfile +++ b/myapp/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -gem 'sidekiq', :path => '..' +gem 'sidekiq-delay_extensions', :path => '..' gem 'rails' gem 'puma' diff --git a/myapp/config/initializers/sidekiq.rb b/myapp/config/initializers/sidekiq.rb index 0fe9f539..8053418e 100644 --- a/myapp/config/initializers/sidekiq.rb +++ b/myapp/config/initializers/sidekiq.rb @@ -32,7 +32,7 @@ def perform(start) end end -Sidekiq::Extensions.enable_delay! +Sidekiq::DelayExtensions.enable_delay! module Myapp class Current < ActiveSupport::CurrentAttributes diff --git a/sidekiq-delay_extensions.gemspec b/sidekiq-delay_extensions.gemspec new file mode 100644 index 00000000..1e6013d2 --- /dev/null +++ b/sidekiq-delay_extensions.gemspec @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative "lib/sidekiq/delay_extensions/version" + +Gem::Specification.new do |spec| + spec.name = "sidekiq-delay_extensions" + spec.version = Sidekiq::DelayExtensions::VERSION + spec.authors = ["Mike Perham", "Benjamin Fleischer"] + spec.email = ["mperham@gmail.com", "github@benjaminfleischer.com"] + + spec.summary = "Sidekiq Delay Extensions" + spec.description = "Extracted from Sidekiq 6.0" + spec.homepage = "https://github.com/gemhome/sidekiq-delay_extensions/wiki/Delayed-extensions" + spec.license = "LGPL-3.0" + + spec.files = Dir.glob("{bin,lib,config}/**/*") + %w[Gemfile sidekiq-delay_extensions.gemspec README.md Changes.md LICENSE] + + spec.bindir = "exe" + spec.executables = [] + spec.require_paths = ["lib"] + spec.required_ruby_version = ">= 2.5.0" + + spec.metadata = { + "homepage_uri" => "https://github.com/gemhome/sidekiq-delay_extensions/wiki/Delayed-extensions", + "bug_tracker_uri" => "https://github.com/gemhome/sidekiq-delay_extensions/issues", + "documentation_uri" => "https://github.com/gemhome/sidekiq-delay_extensions/wiki", + "changelog_uri" => "https://github.com/gemhome/sidekiq-delay_extensions/blob/main/Changes.md", + "source_code_uri" => "https://github.com/gemhome/sidekiq-delay_extensions" + } + + spec.add_dependency "sidekiq", ">= 6.4.1" +end diff --git a/sidekiq.gemspec b/sidekiq.gemspec deleted file mode 100644 index e3d5bf76..00000000 --- a/sidekiq.gemspec +++ /dev/null @@ -1,28 +0,0 @@ -require_relative "lib/sidekiq/version" - -Gem::Specification.new do |gem| - gem.authors = ["Mike Perham"] - gem.email = ["mperham@gmail.com"] - gem.summary = "Simple, efficient background processing for Ruby" - gem.description = "Simple, efficient background processing for Ruby." - gem.homepage = "https://sidekiq.org" - gem.license = "LGPL-3.0" - - gem.executables = ["sidekiq", "sidekiqmon"] - gem.files = ["sidekiq.gemspec", "README.md", "Changes.md", "LICENSE"] + `git ls-files | grep -E '^(bin|lib|web)'`.split("\n") - gem.name = "sidekiq" - gem.version = Sidekiq::VERSION - gem.required_ruby_version = ">= 2.5.0" - - gem.metadata = { - "homepage_uri" => "https://sidekiq.org", - "bug_tracker_uri" => "https://github.com/mperham/sidekiq/issues", - "documentation_uri" => "https://github.com/mperham/sidekiq/wiki", - "changelog_uri" => "https://github.com/mperham/sidekiq/blob/main/Changes.md", - "source_code_uri" => "https://github.com/mperham/sidekiq" - } - - gem.add_dependency "redis", ">= 4.2.0" - gem.add_dependency "connection_pool", ">= 2.2.2" - gem.add_dependency "rack", "~> 2.0" -end diff --git a/test/test_actors.rb b/test/test_actors.rb deleted file mode 100644 index 63cbfac4..00000000 --- a/test/test_actors.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/cli' -require 'sidekiq/fetch' -require 'sidekiq/scheduled' -require 'sidekiq/processor' - -describe 'Actors' do - class JoeWorker - include Sidekiq::Worker - def perform(slp) - raise "boom" if slp == "boom" - sleep(slp) if slp > 0 - $count += 1 - end - end - - before do - Sidekiq.redis {|c| c.flushdb} - end - - describe 'scheduler' do - it 'can start and stop' do - f = Sidekiq::Scheduled::Poller.new - f.start - f.terminate - end - - it 'can schedule' do - ss = Sidekiq::ScheduledSet.new - q = Sidekiq::Queue.new - - JoeWorker.perform_in(0.01, 0) - - assert_equal 0, q.size - assert_equal 1, ss.size - - sleep 0.015 - s = Sidekiq::Scheduled::Poller.new - s.enqueue - assert_equal 1, q.size - assert_equal 0, ss.size - s.terminate - end - end - - describe 'processor' do - before do - $count = 0 - end - - it 'can start and stop' do - m = Mgr.new - f = Sidekiq::Processor.new(m, m.options) - f.terminate - end - - class Mgr - attr_reader :latest_error - attr_reader :mutex - attr_reader :cond - def initialize - @mutex = ::Mutex.new - @cond = ::ConditionVariable.new - end - def processor_died(inst, err) - @latest_error = err - @mutex.synchronize do - @cond.signal - end - end - def processor_stopped(inst) - @mutex.synchronize do - @cond.signal - end - end - def options - opts = { :concurrency => 3, :queues => ['default'] } - opts[:fetch] = Sidekiq::BasicFetch.new(opts) - opts - end - end - - it 'can process' do - mgr = Mgr.new - - p = Sidekiq::Processor.new(mgr, mgr.options) - JoeWorker.perform_async(0) - - a = $count - p.process_one - b = $count - assert_equal a + 1, b - end - - it 'deals with errors' do - mgr = Mgr.new - - p = Sidekiq::Processor.new(mgr, mgr.options) - JoeWorker.perform_async("boom") - q = Sidekiq::Queue.new - assert_equal 1, q.size - - a = $count - mgr.mutex.synchronize do - p.start - mgr.cond.wait(mgr.mutex) - end - b = $count - assert_equal a, b - - sleep 0.001 - assert_equal false, p.thread.status - p.terminate(true) - refute_nil mgr.latest_error - assert_equal RuntimeError, mgr.latest_error.class - end - - it 'gracefully kills' do - mgr = Mgr.new - - p = Sidekiq::Processor.new(mgr, mgr.options) - JoeWorker.perform_async(1) - q = Sidekiq::Queue.new - assert_equal 1, q.size - - a = $count - p.start - sleep(0.05) - p.terminate - p.kill(true) - - b = $count - assert_equal a, b - assert_equal false, p.thread.status - refute mgr.latest_error, mgr.latest_error.to_s - end - end -end diff --git a/test/test_api.rb b/test/test_api.rb index 5342ebd1..101986df 100644 --- a/test/test_api.rb +++ b/test/test_api.rb @@ -274,7 +274,7 @@ class WorkerWithTags end it 'unwraps delayed jobs' do - Sidekiq::Extensions.enable_delay! + Sidekiq::DelayExtensions.enable_delay! Sidekiq::Queue.delay.foo(1,2,3) q = Sidekiq::Queue.new x = q.first diff --git a/test/test_cli.rb b/test/test_cli.rb deleted file mode 100644 index 7fce9077..00000000 --- a/test/test_cli.rb +++ /dev/null @@ -1,591 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'sidekiq/cli' - -describe Sidekiq::CLI do - describe '#parse' do - before do - Sidekiq.options = Sidekiq::DEFAULTS.dup - @logger = Sidekiq.logger - @logdev = StringIO.new - Sidekiq.logger = Logger.new(@logdev) - end - - after do - Sidekiq.logger = @logger - end - - subject { Sidekiq::CLI.new } - - def logdev - @logdev ||= StringIO.new - end - - describe '#parse' do - describe 'options' do - describe 'require' do - it 'accepts with -r' do - subject.parse(%w[sidekiq -r ./test/fake_env.rb]) - - assert_equal './test/fake_env.rb', Sidekiq.options[:require] - end - end - - describe 'concurrency' do - it 'accepts with -c' do - subject.parse(%w[sidekiq -c 60 -r ./test/fake_env.rb]) - - assert_equal 60, Sidekiq.options[:concurrency] - end - - describe 'when concurrency is empty and RAILS_MAX_THREADS env var is set' do - before do - ENV['RAILS_MAX_THREADS'] = '9' - end - - after do - ENV.delete('RAILS_MAX_THREADS') - end - - it 'sets concurrency from RAILS_MAX_THREADS env var' do - subject.parse(%w[sidekiq -r ./test/fake_env.rb]) - - assert_equal 9, Sidekiq.options[:concurrency] - end - - it 'option overrides RAILS_MAX_THREADS env var' do - subject.parse(%w[sidekiq -c 60 -r ./test/fake_env.rb]) - - assert_equal 60, Sidekiq.options[:concurrency] - end - end - end - - describe 'setting internal options via the config file' do - describe 'setting the `strict` option via the config file' do - it 'discards the `strict` option specified via the config file' do - subject.parse(%w[sidekiq -C ./test/config_with_internal_options.yml]) - - assert_equal true, !!Sidekiq.options[:strict] - end - end - end - - describe 'queues' do - it 'accepts with -q' do - subject.parse(%w[sidekiq -q foo -r ./test/fake_env.rb]) - - assert_equal ['foo'], Sidekiq.options[:queues] - end - - describe 'when weights are not present' do - it 'accepts queues without weights' do - subject.parse(%w[sidekiq -q foo -q bar -r ./test/fake_env.rb]) - - assert_equal ['foo', 'bar'], Sidekiq.options[:queues] - end - - it 'sets strictly ordered queues' do - subject.parse(%w[sidekiq -q foo -q bar -r ./test/fake_env.rb]) - - assert_equal true, !!Sidekiq.options[:strict] - end - end - - describe 'when weights are present' do - it 'accepts queues with weights' do - subject.parse(%w[sidekiq -q foo,3 -q bar -r ./test/fake_env.rb]) - - assert_equal ['foo', 'foo', 'foo', 'bar'], Sidekiq.options[:queues] - end - - it 'does not set strictly ordered queues' do - subject.parse(%w[sidekiq -q foo,3 -q bar -r ./test/fake_env.rb]) - - assert_equal false, !!Sidekiq.options[:strict] - end - end - - it 'accepts queues with multi-word names' do - subject.parse(%w[sidekiq -q queue_one -q queue-two -r ./test/fake_env.rb]) - - assert_equal ['queue_one', 'queue-two'], Sidekiq.options[:queues] - end - - it 'accepts queues with dots in the name' do - subject.parse(%w[sidekiq -q foo.bar -r ./test/fake_env.rb]) - - assert_equal ['foo.bar'], Sidekiq.options[:queues] - end - - describe 'when duplicate queue names' do - it 'raises an argument error' do - assert_raises(ArgumentError) { subject.parse(%w[sidekiq -q foo -q foo -r ./test/fake_env.rb]) } - assert_raises(ArgumentError) { subject.parse(%w[sidekiq -q foo,3 -q foo,1 -r ./test/fake_env.rb]) } - end - end - - describe 'when queues are empty' do - describe 'when no queues are specified via -q' do - it "sets 'default' queue" do - subject.parse(%w[sidekiq -r ./test/fake_env.rb]) - - assert_equal ['default'], Sidekiq.options[:queues] - end - end - - describe 'when no queues are specified via the config file' do - it "sets 'default' queue" do - subject.parse(%w[sidekiq -C ./test/config_empty.yml -r ./test/fake_env.rb]) - - assert_equal ['default'], Sidekiq.options[:queues] - end - end - end - end - - describe 'timeout' do - it 'accepts with -t' do - subject.parse(%w[sidekiq -t 30 -r ./test/fake_env.rb]) - - assert_equal 30, Sidekiq.options[:timeout] - end - end - - describe 'verbose' do - it 'accepts with -v' do - subject.parse(%w[sidekiq -v -r ./test/fake_env.rb]) - - assert_equal Logger::DEBUG, Sidekiq.logger.level - end - end - - describe 'config file' do - it 'accepts with -C' do - subject.parse(%w[sidekiq -C ./test/config.yml]) - - assert_equal './test/config.yml', Sidekiq.options[:config_file] - refute Sidekiq.options[:verbose] - assert_equal './test/fake_env.rb', Sidekiq.options[:require] - assert_nil Sidekiq.options[:environment] - assert_equal 50, Sidekiq.options[:concurrency] - assert_equal 2, Sidekiq.options[:queues].count { |q| q == 'very_often' } - assert_equal 1, Sidekiq.options[:queues].count { |q| q == 'seldom' } - end - - it 'accepts stringy keys' do - subject.parse(%w[sidekiq -C ./test/config_string.yml]) - - assert_equal './test/config_string.yml', Sidekiq.options[:config_file] - refute Sidekiq.options[:verbose] - assert_equal './test/fake_env.rb', Sidekiq.options[:require] - assert_nil Sidekiq.options[:environment] - assert_equal 50, Sidekiq.options[:concurrency] - assert_equal 2, Sidekiq.options[:queues].count { |q| q == 'very_often' } - assert_equal 1, Sidekiq.options[:queues].count { |q| q == 'seldom' } - end - - it 'accepts environment specific config' do - subject.parse(%w[sidekiq -e staging -C ./test/config_environment.yml]) - - assert_equal './test/config_environment.yml', Sidekiq.options[:config_file] - refute Sidekiq.options[:verbose] - assert_equal './test/fake_env.rb', Sidekiq.options[:require] - assert_equal 'staging', Sidekiq.options[:environment] - assert_equal 50, Sidekiq.options[:concurrency] - assert_equal 2, Sidekiq.options[:queues].count { |q| q == 'very_often' } - assert_equal 1, Sidekiq.options[:queues].count { |q| q == 'seldom' } - end - - it 'accepts environment specific config with alias' do - subject.parse(%w[sidekiq -e staging -C ./test/config_with_alias.yml]) - assert_equal './test/config_with_alias.yml', Sidekiq.options[:config_file] - refute Sidekiq.options[:verbose] - assert_equal './test/fake_env.rb', Sidekiq.options[:require] - assert_equal 'staging', Sidekiq.options[:environment] - assert_equal 50, Sidekiq.options[:concurrency] - assert_equal 2, Sidekiq.options[:queues].count { |q| q == 'very_often' } - assert_equal 1, Sidekiq.options[:queues].count { |q| q == 'seldom' } - - subject.parse(%w[sidekiq -e production -C ./test/config_with_alias.yml]) - assert_equal './test/config_with_alias.yml', Sidekiq.options[:config_file] - assert Sidekiq.options[:verbose] - assert_equal './test/fake_env.rb', Sidekiq.options[:require] - assert_equal 'production', Sidekiq.options[:environment] - assert_equal 50, Sidekiq.options[:concurrency] - assert_equal 2, Sidekiq.options[:queues].count { |q| q == 'very_often' } - assert_equal 1, Sidekiq.options[:queues].count { |q| q == 'seldom' } - end - - it 'exposes ERB expected __FILE__ and __dir__' do - given_path = './test/config__FILE__and__dir__.yml' - expected_file = File.expand_path(given_path) - # As per Ruby's Kernel module docs, __dir__ is equivalent to File.dirname(File.realpath(__FILE__)) - expected_dir = File.dirname(File.realpath(expected_file)) - - subject.parse(%W[sidekiq -C #{given_path}]) - - assert_equal(expected_file, Sidekiq.options.fetch(:__FILE__)) - assert_equal(expected_dir, Sidekiq.options.fetch(:__dir__)) - end - end - - describe 'default config file' do - describe 'when required path is a directory' do - it 'tries config/sidekiq.yml from required diretory' do - subject.parse(%w[sidekiq -r ./test/dummy]) - - assert_equal './test/dummy/config/sidekiq.yml', Sidekiq.options[:config_file] - assert_equal 25, Sidekiq.options[:concurrency] - end - end - - describe 'when required path is a file' do - it 'tries config/sidekiq.yml from current diretory' do - Sidekiq.options[:require] = './test/dummy' # stub current dir – ./ - - subject.parse(%w[sidekiq -r ./test/fake_env.rb]) - - assert_equal './test/dummy/config/sidekiq.yml', Sidekiq.options[:config_file] - assert_equal 25, Sidekiq.options[:concurrency] - end - end - - describe 'without any required path' do - it 'tries config/sidekiq.yml from current diretory' do - Sidekiq.options[:require] = './test/dummy' # stub current dir – ./ - - subject.parse(%w[sidekiq]) - - assert_equal './test/dummy/config/sidekiq.yml', Sidekiq.options[:config_file] - assert_equal 25, Sidekiq.options[:concurrency] - end - end - - describe 'when config file and flags' do - it 'merges options' do - subject.parse(%w[sidekiq -C ./test/config.yml - -e snoop - -c 100 - -r ./test/fake_env.rb - -q often,7 - -q seldom,3]) - - assert_equal './test/config.yml', Sidekiq.options[:config_file] - refute Sidekiq.options[:verbose] - assert_equal './test/fake_env.rb', Sidekiq.options[:require] - assert_equal 'snoop', Sidekiq.options[:environment] - assert_equal 100, Sidekiq.options[:concurrency] - assert_equal 7, Sidekiq.options[:queues].count { |q| q == 'often' } - assert_equal 3, Sidekiq.options[:queues].count { |q| q == 'seldom' } - end - - describe 'when the config file specifies queues with weights' do - describe 'when -q specifies queues without weights' do - it 'sets strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config.yml - -r ./test/fake_env.rb - -q foo -q bar]) - - assert_equal true, !!Sidekiq.options[:strict] - end - end - - describe 'when -q specifies no queues' do - it 'does not set strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config.yml - -r ./test/fake_env.rb]) - - assert_equal false, !!Sidekiq.options[:strict] - end - end - - describe 'when -q specifies queues with weights' do - it 'does not set strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config.yml - -r ./test/fake_env.rb - -q foo,2 -q bar,3]) - - assert_equal false, !!Sidekiq.options[:strict] - end - end - end - - describe 'when the config file specifies queues without weights' do - describe 'when -q specifies queues without weights' do - it 'sets strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config_queues_without_weights.yml - -r ./test/fake_env.rb - -q foo -q bar]) - - assert_equal true, !!Sidekiq.options[:strict] - end - end - - describe 'when -q specifies no queues' do - it 'sets strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config_queues_without_weights.yml - -r ./test/fake_env.rb]) - - assert_equal true, !!Sidekiq.options[:strict] - end - end - - describe 'when -q specifies queues with weights' do - it 'does not set strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config_queues_without_weights.yml - -r ./test/fake_env.rb - -q foo,2 -q bar,3]) - - assert_equal false, !!Sidekiq.options[:strict] - end - end - end - - describe 'when the config file specifies no queues' do - describe 'when -q specifies queues without weights' do - it 'sets strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config_empty.yml - -r ./test/fake_env.rb - -q foo -q bar]) - - assert_equal true, !!Sidekiq.options[:strict] - end - end - - describe 'when -q specifies no queues' do - it 'sets strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config_empty.yml - -r ./test/fake_env.rb]) - - assert_equal true, !!Sidekiq.options[:strict] - end - end - - describe 'when -q specifies queues with weights' do - it 'does not set strictly ordered queues' do - subject.parse(%w[sidekiq -C ./test/config_empty.yml - -r ./test/fake_env.rb - -q foo,2 -q bar,3]) - - assert_equal false, !!Sidekiq.options[:strict] - end - end - end - end - - describe 'default config file' do - describe 'when required path is a directory' do - it 'tries config/sidekiq.yml' do - subject.parse(%w[sidekiq -r ./test/dummy]) - - assert_equal 'sidekiq.yml', File.basename(Sidekiq.options[:config_file]) - assert_equal 25, Sidekiq.options[:concurrency] - end - end - end - end - end - - describe 'validation' do - describe 'when required application path does not exist' do - it 'exits with status 1' do - exit = assert_raises(SystemExit) { subject.parse(%w[sidekiq -r /non/existent/path]) } - assert_equal 1, exit.status - end - end - - describe 'when required path is a directory without config/application.rb' do - it 'exits with status 1' do - exit = assert_raises(SystemExit) { subject.parse(%w[sidekiq -r ./test/fixtures]) } - assert_equal 1, exit.status - end - - describe 'when config file path does not exist' do - it 'raises argument error' do - assert_raises(ArgumentError) do - subject.parse(%w[sidekiq -r ./test/fake_env.rb -C /non/existent/path]) - end - end - end - end - - describe 'when concurrency is not valid' do - describe 'when set to 0' do - it 'raises argument error' do - assert_raises(ArgumentError) do - subject.parse(%w[sidekiq -r ./test/fake_env.rb -c 0]) - end - end - end - - describe 'when set to a negative number' do - it 'raises argument error' do - assert_raises(ArgumentError) do - subject.parse(%w[sidekiq -r ./test/fake_env.rb -c -2]) - end - end - end - end - - describe 'when timeout is not valid' do - describe 'when set to 0' do - it 'raises argument error' do - assert_raises(ArgumentError) do - subject.parse(%w[sidekiq -r ./test/fake_env.rb -t 0]) - end - end - end - - describe 'when set to a negative number' do - it 'raises argument error' do - assert_raises(ArgumentError) do - subject.parse(%w[sidekiq -r ./test/fake_env.rb -t -2]) - end - end - end - end - end - end - - describe '#run' do - before do - Sidekiq.options[:concurrency] = 2 - Sidekiq.options[:require] = './test/fake_env.rb' - end - - describe 'require workers' do - describe 'when path is a rails directory' do - before do - Sidekiq.options[:require] = './test/dummy' - subject.environment = 'test' - end - - it 'requires sidekiq railtie and rails application with environment' do - subject.stub(:launch, nil) do - subject.run - end - - assert defined?(Sidekiq::Rails) - assert defined?(Dummy::Application) - end - - it 'tags with the app directory name' do - subject.stub(:launch, nil) do - subject.run - end - - assert_equal 'dummy', Sidekiq.options[:tag] - end - end - - describe 'when path is file' do - it 'requires application' do - subject.stub(:launch, nil) do - subject.run - end - - assert $LOADED_FEATURES.any? { |x| x =~ /test\/fake_env/ } - end - end - end - - describe 'when development environment and stdout tty' do - it 'prints banner' do - subject.stub(:environment, 'development') do - assert_output(/#{Regexp.escape(Sidekiq::CLI.banner)}/) do - $stdout.stub(:tty?, true) do - subject.stub(:launch, nil) do - subject.run - end - end - end - end - end - end - - it 'prints rails info' do - subject.stub(:environment, 'production') do - subject.stub(:launch, nil) do - subject.run - end - assert_includes @logdev.string, "Booted Rails #{::Rails.version} application in production environment" - end - end - - describe 'checking maxmemory policy' do - it 'warns if the policy is not noeviction' do - redis_info = { "maxmemory_policy" => "allkeys-lru", "redis_version" => "6" } - - Sidekiq.stub(:redis_info, redis_info) do - subject.stub(:launch, nil) do - subject.run - end - end - - assert_includes @logdev.string, "allkeys-lru" - end - - it 'silent if the policy is noeviction' do - redis_info = { "maxmemory_policy" => "noeviction", "redis_version" => "6" } - - Sidekiq.stub(:redis_info, redis_info) do - subject.stub(:launch, nil) do - subject.run - end - end - - refute_includes @logdev.string, "noeviction" - end - end - end - - describe 'signal handling' do - %w(INT TERM).each do |sig| - describe sig do - it 'raises interrupt error' do - assert_raises Interrupt do - subject.handle_signal(sig) - end - end - end - end - - describe "TSTP" do - it 'quiets with a corresponding event' do - quiet = false - - Sidekiq.on(:quiet) do - quiet = true - end - - subject.launcher = Sidekiq::Launcher.new(Sidekiq.options) - subject.handle_signal("TSTP") - - assert_match(/Got TSTP signal/, logdev.string) - assert_equal true, quiet - end - end - - describe 'TTIN' do - it 'prints backtraces for all threads in the process to the logfile' do - subject.handle_signal('TTIN') - - assert_match(/Got TTIN signal/, logdev.string) - assert_match(/\bbacktrace\b/, logdev.string) - end - end - - describe 'UNKNOWN' do - it 'logs about' do - # subject.parse(%w[sidekiq -r ./test/fake_env.rb]) - subject.handle_signal('UNKNOWN') - - assert_match(/Got UNKNOWN signal/, logdev.string) - assert_match(/No signal handler registered/, logdev.string) - end - end - end - end -end diff --git a/test/test_client.rb b/test/test_client.rb deleted file mode 100644 index 443dc0e6..00000000 --- a/test/test_client.rb +++ /dev/null @@ -1,446 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/api' - -describe Sidekiq::Client do - describe 'errors' do - it 'raises ArgumentError with invalid params' do - assert_raises ArgumentError do - Sidekiq::Client.push('foo', 1) - end - - assert_raises ArgumentError do - Sidekiq::Client.push('foo', :class => 'Foo', :noargs => [1, 2]) - end - - assert_raises ArgumentError do - Sidekiq::Client.push('queue' => 'foo', 'class' => MyWorker, 'noargs' => [1, 2]) - end - - assert_raises ArgumentError do - Sidekiq::Client.push('queue' => 'foo', 'class' => 42, 'args' => [1, 2]) - end - - assert_raises ArgumentError do - Sidekiq::Client.push('queue' => 'foo', 'class' => MyWorker, 'args' => :not_an_array) - end - - assert_raises ArgumentError do - Sidekiq::Client.push('queue' => 'foo', 'class' => MyWorker, 'args' => [1], 'at' => :not_a_numeric) - end - - assert_raises ArgumentError do - Sidekiq::Client.push('queue' => 'foo', 'class' => MyWorker, 'args' => [1], 'tags' => :not_an_array) - end - end - end - - describe 'as instance' do - it 'handles nil queue' do - assert_raises ArgumentError do - Sidekiq::Client.push('class' => 'Blah', 'args' => [1,2,3], 'queue' => "") - end - end - - it 'can push' do - client = Sidekiq::Client.new - jid = client.push('class' => 'Blah', 'args' => [1,2,3]) - assert_equal 24, jid.size - end - - it 'allows middleware to stop bulk jobs' do - mware = Class.new do - def call(worker_klass,msg,q,r) - msg['args'][0] == 1 ? yield : false - end - end - client = Sidekiq::Client.new - client.middleware do |chain| - chain.add mware - end - q = Sidekiq::Queue.new - q.clear - result = client.push_bulk('class' => 'Blah', 'args' => [[1],[2],[3]]) - assert_equal 1, result.size - assert_equal 1, q.size - end - - it 'allows local middleware modification' do - $called = false - mware = Class.new { def call(worker_klass,msg,q,r); $called = true; msg;end } - client = Sidekiq::Client.new - client.middleware do |chain| - chain.add mware - end - client.push('class' => 'Blah', 'args' => [1,2,3]) - - assert $called - assert client.middleware.exists?(mware) - refute Sidekiq.client_middleware.exists?(mware) - end - end - - describe 'client' do - it 'pushes messages to redis' do - q = Sidekiq::Queue.new('foo') - pre = q.size - jid = Sidekiq::Client.push('queue' => 'foo', 'class' => MyWorker, 'args' => [1, 2]) - assert jid - assert_equal 24, jid.size - assert_equal pre + 1, q.size - end - - it 'pushes messages to redis using a String class' do - q = Sidekiq::Queue.new('foo') - pre = q.size - jid = Sidekiq::Client.push('queue' => 'foo', 'class' => 'MyWorker', 'args' => [1, 2]) - assert jid - assert_equal 24, jid.size - assert_equal pre + 1, q.size - end - - class MyWorker - include Sidekiq::Worker - end - - class QueuedWorker - include Sidekiq::Worker - sidekiq_options :queue => :flimflam - end - - it 'enqueues' do - Sidekiq.redis {|c| c.flushdb } - assert_equal Sidekiq.default_worker_options, MyWorker.get_sidekiq_options - assert MyWorker.perform_async(1, 2) - assert Sidekiq::Client.enqueue(MyWorker, 1, 2) - assert Sidekiq::Client.enqueue_to(:custom_queue, MyWorker, 1, 2) - assert_equal 1, Sidekiq::Queue.new('custom_queue').size - assert Sidekiq::Client.enqueue_to_in(:custom_queue, 3, MyWorker, 1, 2) - assert Sidekiq::Client.enqueue_to_in(:custom_queue, -3, MyWorker, 1, 2) - assert_equal 2, Sidekiq::Queue.new('custom_queue').size - assert Sidekiq::Client.enqueue_in(3, MyWorker, 1, 2) - assert QueuedWorker.perform_async(1, 2) - assert_equal 1, Sidekiq::Queue.new('flimflam').size - end - - describe 'argument checking' do - class InterestingWorker - include Sidekiq::Worker - - def perform(an_argument) - end - end - - it 'enqueues jobs with a symbol as an argument' do - InterestingWorker.perform_async(:symbol) - end - - it 'enqueues jobs with a Date as an argument' do - InterestingWorker.perform_async(Date.new(2021, 1, 1)) - end - - it 'enqueues jobs with a Hash with symbols and string as keys as an argument' do - InterestingWorker.perform_async( - { - some: 'hash', - 'with' => 'different_keys' - } - ) - end - - it 'enqueues jobs with a Struct as an argument' do - InterestingWorker.perform_async( - Struct.new(:x, :y).new(0, 0) - ) - end - - it 'works with a JSON-friendly deep, nested structure' do - InterestingWorker.perform_async( - { - 'foo' => ['a', 'b', 'c'], - 'bar' => ['x', 'y', 'z'] - } - ) - end - - describe 'strict args is enabled' do - before do - Sidekiq.strict_args! - end - - after do - Sidekiq.strict_args!(false) - end - - it 'raises an error when using a symbol as an argument' do - assert_raises ArgumentError do - InterestingWorker.perform_async(:symbol) - end - end - - it 'raises an error when using a Date as an argument' do - assert_raises ArgumentError do - InterestingWorker.perform_async(Date.new(2021, 1, 1)) - end - end - - it 'raises an error when using a Hash with symbols and string as keys as an argument' do - assert_raises ArgumentError do - InterestingWorker.perform_async( - { - some: 'hash', - 'with' => 'different_keys' - } - ) - end - end - - it 'raises an error when using a Struct as an argument' do - assert_raises ArgumentError do - InterestingWorker.perform_async( - Struct.new(:x, :y).new(0, 0) - ) - end - end - - it 'works with a JSON-friendly deep, nested structure' do - InterestingWorker.perform_async( - { - 'foo' => ['a', 'b', 'c'], - 'bar' => ['x', 'y', 'z'] - } - ) - end - - describe 'worker that takes deep, nested structures' do - it 'raises an error on JSON-unfriendly structures' do - assert_raises ArgumentError do - InterestingWorker.perform_async( - { - 'foo' => [:a, :b, :c], - bar: ['x', 'y', 'z'] - } - ) - end - end - end - end - end - end - - describe 'bulk' do - after do - Sidekiq::Queue.new.clear - end - - it 'can push a large set of jobs at once' do - jids = Sidekiq::Client.push_bulk('class' => QueuedWorker, 'args' => (1..1_000).to_a.map { |x| Array(x) }) - assert_equal 1_000, jids.size - end - - it 'can push a large set of jobs at once using a String class' do - jids = Sidekiq::Client.push_bulk('class' => 'QueuedWorker', 'args' => (1..1_000).to_a.map { |x| Array(x) }) - assert_equal 1_000, jids.size - end - - it 'can push jobs scheduled at different times' do - first_at = Time.new(2019, 1, 1) - second_at = Time.new(2019, 1, 2) - jids = Sidekiq::Client.push_bulk('class' => QueuedWorker, 'args' => [[1], [2]], 'at' => [first_at.to_f, second_at.to_f]) - (first_jid, second_jid) = jids - assert_equal first_at, Sidekiq::ScheduledSet.new.find_job(first_jid).at - assert_equal second_at, Sidekiq::ScheduledSet.new.find_job(second_jid).at - end - - it 'can push jobs scheduled using ActiveSupport::Duration' do - jids = Sidekiq::Client.push_bulk('class' => QueuedWorker, 'args' => [[1], [2]], 'at' => [1.seconds, 111.seconds]) - assert_equal 2, jids.size - end - - it 'returns the jids for the jobs' do - Sidekiq::Client.push_bulk('class' => 'QueuedWorker', 'args' => (1..2).to_a.map { |x| Array(x) }).each do |jid| - assert_match(/[0-9a-f]{12}/, jid) - end - end - - it 'handles no jobs' do - result = Sidekiq::Client.push_bulk('class' => 'QueuedWorker', 'args' => []) - assert_equal 0, result.size - end - - describe 'errors' do - it 'raises ArgumentError with invalid params' do - assert_raises ArgumentError do - Sidekiq::Client.push_bulk('class' => 'QueuedWorker', 'args' => [[1], 2]) - end - - assert_raises ArgumentError do - Sidekiq::Client.push_bulk('class' => 'QueuedWorker', 'args' => [[1], [2]], 'at' => [Time.now.to_f, :not_a_numeric]) - end - - assert_raises ArgumentError do - Sidekiq::Client.push_bulk('class' => QueuedWorker, 'args' => [[1], [2]], 'at' => [Time.now.to_f]) - end - - assert_raises ArgumentError do - Sidekiq::Client.push_bulk('class' => QueuedWorker, 'args' => [[1]], 'at' => [Time.now.to_f, Time.now.to_f]) - end - end - end - - describe '.perform_bulk' do - it 'pushes a large set of jobs' do - jids = MyWorker.perform_bulk((1..1_001).to_a.map { |x| Array(x) }) - assert_equal 1_001, jids.size - end - - it 'pushes a large set of jobs with a different batch size' do - jids = MyWorker.perform_bulk((1..1_001).to_a.map { |x| Array(x) }, batch_size: 100) - assert_equal 1_001, jids.size - end - - it 'handles no jobs' do - jids = MyWorker.perform_bulk([]) - assert_equal 0, jids.size - end - - describe 'errors' do - it 'raises ArgumentError with invalid params' do - assert_raises ArgumentError do - Sidekiq::Client.push_bulk('class' => 'MyWorker', 'args' => [[1], 2]) - end - end - end - - describe 'lazy enumerator' do - it 'enqueues the jobs by evaluating the enumerator' do - lazy_array = (1..1_001).to_a.map { |x| Array(x) }.lazy - jids = MyWorker.perform_bulk(lazy_array) - assert_equal 1_001, jids.size - end - end - end - end - - class BaseWorker - include Sidekiq::Worker - sidekiq_options 'retry' => 'base' - end - class AWorker < BaseWorker - end - class BWorker < BaseWorker - sidekiq_options 'retry' => 'b' - end - class CWorker < BaseWorker - sidekiq_options 'retry' => 2 - end - - describe 'client middleware' do - class Stopper - def call(worker_class, job, queue, r) - raise ArgumentError unless r - yield if job['args'].first.odd? - end - end - - it 'can stop some of the jobs from pushing' do - client = Sidekiq::Client.new - client.middleware do |chain| - chain.add Stopper - end - - assert_nil client.push('class' => MyWorker, 'args' => [0]) - assert_match(/[0-9a-f]{12}/, client.push('class' => MyWorker, 'args' => [1])) - client.push_bulk('class' => MyWorker, 'args' => [[0], [1]]).each do |jid| - assert_match(/[0-9a-f]{12}/, jid) - end - end - end - - describe 'inheritance' do - it 'inherits sidekiq options' do - assert_equal 'base', AWorker.get_sidekiq_options['retry'] - assert_equal 'b', BWorker.get_sidekiq_options['retry'] - end - end - - describe 'sharding' do - class DWorker < BaseWorker - end - - it 'allows sidekiq_options to point to different Redi' do - conn = MiniTest::Mock.new - conn.expect(:pipelined, [0, 1]) - DWorker.sidekiq_options('pool' => ConnectionPool.new(size: 1) { conn }) - DWorker.perform_async(1,2,3) - conn.verify - end - - it 'allows #via to point to same Redi' do - conn = MiniTest::Mock.new - conn.expect(:pipelined, [0, 1]) - sharded_pool = ConnectionPool.new(size: 1) { conn } - Sidekiq::Client.via(sharded_pool) do - Sidekiq::Client.via(sharded_pool) do - CWorker.perform_async(1,2,3) - end - end - conn.verify - end - - it 'allows #via to point to different Redi' do - default = Sidekiq::Client.new.redis_pool - - moo = MiniTest::Mock.new - moo.expect(:pipelined, [0, 1]) - beef = ConnectionPool.new(size: 1) { moo } - - oink = MiniTest::Mock.new - oink.expect(:pipelined, [0, 1]) - pork = ConnectionPool.new(size: 1) { oink } - - Sidekiq::Client.via(beef) do - CWorker.perform_async(1,2,3) - assert_equal beef, Sidekiq::Client.new.redis_pool - Sidekiq::Client.via(pork) do - assert_equal pork, Sidekiq::Client.new.redis_pool - CWorker.perform_async(1,2,3) - end - assert_equal beef, Sidekiq::Client.new.redis_pool - end - assert_equal default, Sidekiq::Client.new.redis_pool - moo.verify - oink.verify - end - - it 'allows Resque helpers to point to different Redi' do - conn = MiniTest::Mock.new - conn.expect(:pipelined, []) { |*args, &block| block.call(conn) } - conn.expect(:zadd, 1, [String, Array]) - DWorker.sidekiq_options('pool' => ConnectionPool.new(size: 1) { conn }) - Sidekiq::Client.enqueue_in(10, DWorker, 3) - conn.verify - end - end - - describe 'class attribute race conditions' do - new_class = -> { - Class.new do - class_eval('include Sidekiq::Worker') - - define_method(:foo) { get_sidekiq_options } - end - } - - it 'does not explode when new initializing classes from multiple threads' do - 100.times do - klass = new_class.call - - t1 = Thread.new { klass.sidekiq_options({}) } - t2 = Thread.new { klass.sidekiq_options({}) } - t1.join - t2.join - end - end - end -end diff --git a/test/test_csrf.rb b/test/test_csrf.rb deleted file mode 100644 index 1b2a6975..00000000 --- a/test/test_csrf.rb +++ /dev/null @@ -1,115 +0,0 @@ -require_relative './helper' -require 'sidekiq/web/csrf_protection' - -class TestCsrf < Minitest::Test - def session - @session ||= {} - end - - def env(method=:get, form_hash={}, rack_session=session) - imp = StringIO.new("") - { - "REQUEST_METHOD" => method.to_s.upcase, - "rack.session" => rack_session, - "rack.logger" => ::Logger.new(@logio ||= StringIO.new("")), - "rack.input" => imp, - "rack.request.form_input" => imp, - "rack.request.form_hash" => form_hash, - } - end - - def call(env, &block) - Sidekiq::Web::CsrfProtection.new(block).call(env) - end - - def test_get - ok = [200, {}, ["OK"]] - first = 1 - second = 1 - result = call(env) do |envy| - refute_nil envy[:csrf_token] - assert_equal 88, envy[:csrf_token].size - first = envy[:csrf_token] - ok - end - assert_equal ok, result - - result = call(env) do |envy| - refute_nil envy[:csrf_token] - assert_equal 88, envy[:csrf_token].size - second = envy[:csrf_token] - ok - end - assert_equal ok, result - - # verify masked token changes on every valid request - refute_equal first, second - end - - def test_bad_post - result = call(env(:post)) do - raise "Shouldnt be called" - end - refute_nil result - assert_equal 403, result[0] - assert_equal ["Forbidden"], result[2] - - @logio.rewind - assert_match(/attack prevented/, @logio.string) - end - - def test_good_and_bad_posts - # Make a GET to set up the session with a good token - goodtoken = call(env) do |envy| - envy[:csrf_token] - end - assert goodtoken - - # Make a POST with the known good token - result = call(env(:post, "authenticity_token" => goodtoken)) do - [200, {}, ["OK"]] - end - refute_nil result - assert_equal 200, result[0] - assert_equal ["OK"], result[2] - - # Make a POST with a known bad token - result = call(env(:post, "authenticity_token"=>"N0QRBD34tU61d7fi+0ZaF/35JLW/9K+8kk8dc1TZoK/0pTl7GIHap5gy7BWGsoKlzbMLRp1yaDpCDFwTJtxWAg==")) do - raise "shouldnt be called" - end - refute_nil result - assert_equal 403, result[0] - assert_equal ["Forbidden"], result[2] - end - - def test_empty_session_post - # Make a GET to set up the session with a good token - goodtoken = call(env) do |envy| - envy[:csrf_token] - end - assert goodtoken - - # Make a POST with an empty session data and good token - result = call(env(:post, { "authenticity_token" => goodtoken }, {})) do - raise "shouldnt be called" - end - refute_nil result - assert_equal 403, result[0] - assert_equal ["Forbidden"], result[2] - end - - def test_empty_csrf_session_post - goodtoken = call(env) do |envy| - envy[:csrf_token] - end - assert goodtoken - - # Make a POST without csrf session data and good token - result = call(env(:post, { "authenticity_token" => goodtoken }, { 'session_id' => 'foo' })) do - raise "shouldnt be called" - end - refute_nil result - assert_equal 403, result[0] - assert_equal ["Forbidden"], result[2] - end -end diff --git a/test/test_current_attributes.rb b/test/test_current_attributes.rb deleted file mode 100644 index 9c499726..00000000 --- a/test/test_current_attributes.rb +++ /dev/null @@ -1,64 +0,0 @@ -require_relative "./helper" -require "sidekiq/middleware/current_attributes" - -module Myapp - class Current < ActiveSupport::CurrentAttributes - attribute :user_id - end -end - -class TestCurrentAttributes < Minitest::Test - def test_save - cm = Sidekiq::CurrentAttributes::Save.new(Myapp::Current) - job = {} - with_context(:user_id, 123) do - cm.call(nil, job, nil, nil) do - assert_equal 123, job["cattr"][:user_id] - end - end - end - - def test_load - cm = Sidekiq::CurrentAttributes::Load.new(Myapp::Current) - - job = { "cattr" => { "user_id" => 123 } } - assert_nil Myapp::Current.user_id - cm.call(nil, job, nil) do - assert_equal 123, Myapp::Current.user_id - end - # the Rails reloader is responsible for reseting Current after every unit of work - end - - def test_persist - begin - Sidekiq::CurrentAttributes.persist(Myapp::Current) - job_hash = {} - with_context(:user_id, 16) do - Sidekiq.client_middleware.invoke(nil, job_hash, nil, nil) do - assert_equal 16, job_hash["cattr"][:user_id] - end - end - - assert_nil Myapp::Current.user_id - Sidekiq.server_middleware.invoke(nil, job_hash, nil) do - assert_equal 16, job_hash["cattr"][:user_id] - assert_equal 16, Myapp::Current.user_id - end - assert_nil Myapp::Current.user_id - ensure - Sidekiq.client_middleware.clear - Sidekiq.server_middleware.clear - end - end - - private - - def with_context(attr, value) - begin - Myapp::Current.send("#{attr}=", value) - yield - ensure - Myapp::Current.reset_all - end - end -end diff --git a/test/test_dead_set.rb b/test/test_dead_set.rb deleted file mode 100644 index e6e1d7be..00000000 --- a/test/test_dead_set.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/api' - -describe 'DeadSet' do - def dead_set - Sidekiq::DeadSet.new - end - - it 'should put passed serialized job to the "dead" sorted set' do - serialized_job = Sidekiq.dump_json(jid: '123123', class: 'SomeWorker', args: []) - dead_set.kill(serialized_job) - - assert_equal dead_set.find_job('123123').value, serialized_job - end - - it 'should remove dead jobs older than Sidekiq::DeadSet.timeout' do - Sidekiq::DeadSet.stub(:timeout, 10) do - Time.stub(:now, Time.now - 11) do - dead_set.kill(Sidekiq.dump_json(jid: '000103', class: 'MyWorker3', args: [])) # the oldest - end - - Time.stub(:now, Time.now - 9) do - dead_set.kill(Sidekiq.dump_json(jid: '000102', class: 'MyWorker2', args: [])) - end - - dead_set.kill(Sidekiq.dump_json(jid: '000101', class: 'MyWorker1', args: [])) - end - - assert_nil dead_set.find_job('000103') - assert dead_set.find_job('000102') - assert dead_set.find_job('000101') - end - - it 'should remove all but last Sidekiq::DeadSet.max_jobs-1 jobs' do - Sidekiq::DeadSet.stub(:max_jobs, 3) do - dead_set.kill(Sidekiq.dump_json(jid: '000101', class: 'MyWorker1', args: [])) - dead_set.kill(Sidekiq.dump_json(jid: '000102', class: 'MyWorker2', args: [])) - dead_set.kill(Sidekiq.dump_json(jid: '000103', class: 'MyWorker3', args: [])) - end - - assert_nil dead_set.find_job('000101') - assert dead_set.find_job('000102') - assert dead_set.find_job('000103') - end -end diff --git a/test/test_exception_handler.rb b/test/test_exception_handler.rb deleted file mode 100644 index cb945cdf..00000000 --- a/test/test_exception_handler.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/exception_handler' -require 'stringio' -require 'logger' - -ExceptionHandlerTestException = Class.new(StandardError) -TEST_EXCEPTION = ExceptionHandlerTestException.new("Something didn't work!") - -class Component - include Sidekiq::ExceptionHandler - - def invoke_exception(args) - raise TEST_EXCEPTION - rescue ExceptionHandlerTestException => e - handle_exception(e,args) - end -end - -describe Sidekiq::ExceptionHandler do - describe "with mock logger" do - before do - @old_logger = Sidekiq.logger - @str_logger = StringIO.new - Sidekiq.logger = Logger.new(@str_logger) - end - - after do - Sidekiq.logger = @old_logger - end - - it "logs the exception to Sidekiq.logger" do - Component.new.invoke_exception(:a => 1) - @str_logger.rewind - log = @str_logger.readlines - assert_match(/"a":1/, log[0], "didn't include the context") - assert_match(/Something didn't work!/, log[1], "didn't include the exception message") - assert_match(/test\/test_exception_handler.rb/, log[2], "didn't include the backtrace") - end - - describe "when the exception does not have a backtrace" do - it "does not fail" do - exception = ExceptionHandlerTestException.new - assert_nil exception.backtrace - - begin - Component.new.handle_exception exception - pass - rescue StandardError - flunk "failed handling a nil backtrace" - end - end - end - end - -end diff --git a/test/test_extensions.rb b/test/test_extensions.rb index dd9f65a3..4cec62df 100644 --- a/test/test_extensions.rb +++ b/test/test_extensions.rb @@ -3,9 +3,9 @@ require 'sidekiq/api' require 'active_record' require 'action_mailer' -Sidekiq::Extensions.enable_delay! +Sidekiq::DelayExtensions.enable_delay! -describe Sidekiq::Extensions do +describe Sidekiq::DelayExtensions do before do Sidekiq.redis {|c| c.flushdb } end diff --git a/test/test_fetch.rb b/test/test_fetch.rb deleted file mode 100644 index 65d9106b..00000000 --- a/test/test_fetch.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/fetch' -require 'sidekiq/api' - -describe Sidekiq::BasicFetch do - before do - @prev_redis = Sidekiq.instance_variable_get(:@redis) || {} - Sidekiq.redis = { :namespace => 'fuzzy' } - Sidekiq.redis do |conn| - conn.redis.flushdb - conn.rpush('queue:basic', 'msg') - end - end - - after do - Sidekiq.redis = @prev_redis - end - - it 'retrieves' do - fetch = Sidekiq::BasicFetch.new(:queues => ['basic', 'bar']) - uow = fetch.retrieve_work - refute_nil uow - assert_equal 'basic', uow.queue_name - assert_equal 'msg', uow.job - q = Sidekiq::Queue.new('basic') - assert_equal 0, q.size - uow.requeue - assert_equal 1, q.size - assert_nil uow.acknowledge - end - - it 'retrieves with strict setting' do - fetch = Sidekiq::BasicFetch.new(:queues => ['basic', 'bar', 'bar'], :strict => true) - cmd = fetch.queues_cmd - assert_equal cmd, ['queue:basic', 'queue:bar', Sidekiq::BasicFetch::TIMEOUT] - end - - it 'bulk requeues' do - Sidekiq.redis do |conn| - conn.rpush('queue:foo', ['bob', 'bar']) - conn.rpush('queue:bar', 'widget') - end - - q1 = Sidekiq::Queue.new('foo') - q2 = Sidekiq::Queue.new('bar') - assert_equal 2, q1.size - assert_equal 1, q2.size - - fetch = Sidekiq::BasicFetch.new(:queues => ['foo', 'bar']) - works = 3.times.map { fetch.retrieve_work } - assert_equal 0, q1.size - assert_equal 0, q2.size - - fetch.bulk_requeue(works, {:queues => []}) - assert_equal 2, q1.size - assert_equal 1, q2.size - end - - it 'sleeps when no queues are active' do - fetch = Sidekiq::BasicFetch.new(:queues => []) - mock = Minitest::Mock.new - mock.expect(:call, nil, [Sidekiq::BasicFetch::TIMEOUT]) - fetch.stub(:sleep, mock) { assert_nil fetch.retrieve_work } - mock.verify - end -end diff --git a/test/test_job.rb b/test/test_job.rb deleted file mode 100644 index 5a6de693..00000000 --- a/test/test_job.rb +++ /dev/null @@ -1,13 +0,0 @@ -require_relative "helper" -require "sidekiq/job" - -class TestJob < Minitest::Test - class SomeJob - include Sidekiq::Job - end - - def test_sidekiq_job - SomeJob.perform_async - assert_equal "TestJob::SomeJob", Sidekiq::Queue.new.first.klass - end -end diff --git a/test/test_job_generator.rb b/test/test_job_generator.rb deleted file mode 100644 index 41d746cc..00000000 --- a/test/test_job_generator.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require_relative 'dummy/config/environment' -require 'rails/generators/test_case' -require 'generators/sidekiq/job_generator' - -class JobGeneratorTest < Rails::Generators::TestCase - tests Sidekiq::Generators::JobGenerator - destination File.expand_path('../../tmp', __FILE__) - setup :prepare_destination - - test 'all files are properly created' do - run_generator ['foo'] - assert_file 'app/sidekiq/foo_job.rb' - assert_file 'test/sidekiq/foo_job_test.rb' - end - - test 'gracefully handles extra job suffix' do - run_generator ['foo_job'] - assert_no_file 'app/sidekiq/foo_job_job.rb' - assert_no_file 'test/sidekiq/foo_job_job_test.rb' - - assert_file 'app/sidekiq/foo_job.rb' - assert_file 'test/sidekiq/foo_job_test.rb' - end - - test 'respects rails config test_framework option' do - Rails.application.config.generators do |g| - g.test_framework false - end - - run_generator ['foo'] - - assert_file 'app/sidekiq/foo_job.rb' - assert_no_file 'test/sidekiq/foo_job_test.rb' - ensure - Rails.application.config.generators do |g| - g.test_framework :test_case - end - end - - test 'respects rails config test_framework option for rspec' do - Rails.application.config.generators do |g| - g.test_framework :rspec - end - - run_generator ['foo'] - - assert_file 'app/sidekiq/foo_job.rb' - assert_file 'spec/sidekiq/foo_job_spec.rb' - ensure - Rails.application.config.generators do |g| - g.test_framework :test_case - end - end -end diff --git a/test/test_job_logger.rb b/test/test_job_logger.rb deleted file mode 100644 index d7065568..00000000 --- a/test/test_job_logger.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'sidekiq/job_logger' - -class TestJobLogger < Minitest::Test - def setup - @old = Sidekiq.logger - @output = StringIO.new - @logger = Sidekiq::Logger.new(@output, level: :info) - Sidekiq.logger = @logger - - Thread.current[:sidekiq_context] = nil - Thread.current[:sidekiq_tid] = nil - end - - def teardown - Thread.current[:sidekiq_context] = nil - Thread.current[:sidekiq_tid] = nil - Sidekiq.logger = @old - end - - def test_pretty_output - jl = Sidekiq::JobLogger.new(@logger) - - # pretty - p = @logger.formatter = Sidekiq::Logger::Formatters::Pretty.new - job = {"jid"=>"1234abc", "wrapped"=>"FooWorker", "class"=>"Wrapper", "tags" => ["bar", "baz"]} - # this mocks what Processor does - jl.prepare(job) do - jl.call(job, 'queue') {} - end - - a, b = @output.string.lines - assert a - assert b - - expected = /pid=#{$$} tid=#{p.tid} class=FooWorker jid=1234abc tags=bar,baz/ - assert_match(expected, a) - assert_match(expected, b) - assert_match(/#{Time.now.utc.to_date}.+Z pid=#{$$} tid=#{p.tid} .+INFO: done/, b) - end - - def test_json_output - # json - @logger.formatter = Sidekiq::Logger::Formatters::JSON.new - jl = Sidekiq::JobLogger.new(@logger) - job = {"jid"=>"1234abc", "wrapped"=>"Wrapper", "class"=>"FooWorker", "bid"=>"b-xyz", "tags" => ["bar", "baz"]} - # this mocks what Processor does - jl.prepare(job) do - jl.call(job, 'queue') {} - end - a, b = @output.string.lines - assert a - assert b - hsh = JSON.parse(a) - keys = hsh.keys.sort - assert_equal(["ctx", "lvl", "msg", "pid", "tid", "ts"], keys) - keys = hsh["ctx"].keys.sort - assert_equal(["bid", "class", "jid", "tags"], keys) - end - - def test_custom_log_level - jl = Sidekiq::JobLogger.new(@logger) - job = {"class"=>"FooWorker", "log_level"=>"debug"} - - assert @logger.info? - jl.prepare(job) do - jl.call(job, "queue") do - assert @logger.debug? - @logger.debug("debug message") - end - end - assert @logger.info? - - a, b, c = @output.string.lines - assert_match(/INFO: start/, a) - assert_match(/DEBUG: debug message/, b) - assert_match(/INFO: done/, c) - end - - def test_custom_log_level_uses_default_log_level_for_invalid_value - jl = Sidekiq::JobLogger.new(@logger) - job = {"class"=>"FooWorker", "log_level"=>"non_existent"} - - assert @logger.info? - jl.prepare(job) do - jl.call(job, "queue") do - assert @logger.info? - end - end - assert @logger.info? - log_level_warning = @output.string.lines[0] - assert_match(/WARN: Invalid log level/, log_level_warning) - end -end diff --git a/test/test_launcher.rb b/test/test_launcher.rb deleted file mode 100644 index 39f1de0d..00000000 --- a/test/test_launcher.rb +++ /dev/null @@ -1,165 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'sidekiq/launcher' - -describe Sidekiq::Launcher do - subject { Sidekiq::Launcher.new(options) } - before do - Sidekiq.redis {|c| c.flushdb } - end - - def new_manager(opts) - Sidekiq::Manager.new(opts) - end - - describe 'memory collection' do - it 'works in any test environment' do - kb = Sidekiq::Launcher::MEMORY_GRABBER.call($$) - refute_nil kb - assert kb > 0 - end - end - - describe 'heartbeat' do - before do - @mgr = new_manager(options) - @launcher = Sidekiq::Launcher.new(options) - @launcher.manager = @mgr - @id = @launcher.identity - - Sidekiq::Processor::WORKER_STATE.set('a', {'b' => 1}) - - @proctitle = $0 - end - - after do - Sidekiq::Processor::WORKER_STATE.clear - $0 = @proctitle - end - - describe '#heartbeat' do - describe 'run' do - it 'sets sidekiq version, tag and the number of busy workers to proctitle' do - subject.heartbeat - - assert_equal "sidekiq #{Sidekiq::VERSION} myapp [1 of 3 busy]", $0 - end - - it 'stores process info in redis' do - subject.heartbeat - - workers, rtt = Sidekiq.redis { |c| c.hmget(subject.identity, 'busy', 'rtt_us') } - - assert_equal "1", workers - refute_nil rtt - assert_in_delta 1000, rtt.to_i, 1000 - - expires = Sidekiq.redis { |c| c.pttl(subject.identity) } - - assert_in_delta 60000, expires, 500 - end - - describe 'events' do - before do - @cnt = 0 - - Sidekiq.on(:heartbeat) do - @cnt += 1 - end - end - - it 'fires start heartbeat event only once' do - assert_equal 0, @cnt - subject.heartbeat - assert_equal 1, @cnt - subject.heartbeat - assert_equal 1, @cnt - end - end - end - - describe 'quiet' do - before do - subject.quiet - end - - it 'sets stopping proctitle' do - subject.heartbeat - - assert_equal "sidekiq #{Sidekiq::VERSION} myapp [1 of 3 busy] stopping", $0 - end - - it 'stores process info in redis' do - subject.heartbeat - - info = Sidekiq.redis { |c| c.hmget(subject.identity, 'busy') } - - assert_equal ["1"], info - - expires = Sidekiq.redis { |c| c.pttl(subject.identity) } - - assert_in_delta 60000, expires, 50 - end - end - - it 'fires new heartbeat events' do - i = 0 - Sidekiq.on(:heartbeat) do - i += 1 - end - assert_equal 0, i - @launcher.heartbeat - assert_equal 1, i - @launcher.heartbeat - assert_equal 1, i - end - - describe 'when manager is active' do - before do - Sidekiq::Launcher::PROCTITLES << proc { "xyz" } - @launcher.heartbeat - Sidekiq::Launcher::PROCTITLES.pop - end - - it 'sets useful info to proctitle' do - assert_equal "sidekiq #{Sidekiq::VERSION} myapp [1 of 3 busy] xyz", $0 - end - - it 'stores process info in redis' do - info = Sidekiq.redis { |c| c.hmget(@id, 'busy') } - assert_equal ["1"], info - expires = Sidekiq.redis { |c| c.pttl(@id) } - assert_in_delta 60000, expires, 500 - end - end - end - - describe 'when manager is stopped' do - before do - @launcher.quiet - @launcher.heartbeat - end - - #after do - #puts system('redis-cli -n 15 keys "*" | while read LINE ; do TTL=`redis-cli -n 15 ttl "$LINE"`; if [ "$TTL" -eq -1 ]; then echo "$LINE"; fi; done;') - #end - - it 'indicates stopping status in proctitle' do - assert_equal "sidekiq #{Sidekiq::VERSION} myapp [1 of 3 busy] stopping", $0 - end - - it 'stores process info in redis' do - info = Sidekiq.redis { |c| c.hmget(@id, 'busy') } - assert_equal ["1"], info - expires = Sidekiq.redis { |c| c.pttl(@id) } - assert_in_delta 60000, expires, 50 - end - end - end - - def options - { :concurrency => 3, :queues => ['default'], :tag => 'myapp' } - end - -end diff --git a/test/test_logger.rb b/test/test_logger.rb deleted file mode 100644 index fbbea0ab..00000000 --- a/test/test_logger.rb +++ /dev/null @@ -1,147 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'sidekiq/logger' - -class TestLogger < Minitest::Test - def setup - @output = StringIO.new - @logger = Sidekiq::Logger.new(@output) - - Sidekiq.log_formatter = nil - Thread.current[:sidekiq_context] = nil - Thread.current[:sidekiq_tid] = nil - end - - def teardown - Sidekiq.log_formatter = nil - Thread.current[:sidekiq_context] = nil - Thread.current[:sidekiq_tid] = nil - end - - def test_default_log_formatter - assert_kind_of Sidekiq::Logger::Formatters::Pretty, Sidekiq::Logger.new(@output).formatter - end - - def test_heroku_log_formatter - begin - ENV['DYNO'] = 'dyno identifier' - assert_kind_of Sidekiq::Logger::Formatters::WithoutTimestamp, Sidekiq::Logger.new(@output).formatter - ensure - ENV['DYNO'] = nil - end - end - - def test_json_log_formatter - Sidekiq.log_formatter = Sidekiq::Logger::Formatters::JSON.new - - assert_kind_of Sidekiq::Logger::Formatters::JSON, Sidekiq::Logger.new(@output).formatter - end - - def test_with_context - subject = Sidekiq::Context - assert_equal({}, subject.current) - - subject.with(a: 1) do - assert_equal({ a: 1 }, subject.current) - end - - assert_equal({}, subject.current) - end - - def test_with_overlapping_context - subject = Sidekiq::Context - subject.current.merge!({ foo: 'bar' }) - assert_equal({ foo: 'bar' }, subject.current) - - subject.with(foo: 'bingo') do - assert_equal({ foo: 'bingo' }, subject.current) - end - - assert_equal({ foo: 'bar' }, subject.current) - end - - def test_nested_contexts - subject = Sidekiq::Context - assert_equal({}, subject.current) - - subject.with(a: 1) do - assert_equal({ a: 1 }, subject.current) - - subject.with(b: 2, c: 3) do - assert_equal({ a: 1, b: 2, c: 3 }, subject.current) - end - - assert_equal({ a: 1 }, subject.current) - end - - assert_equal({}, subject.current) - end - - def test_formatted_output - @logger.info("hello world") - assert_match(/INFO: hello world/, @output.string) - reset(@output) - - formats = [ Sidekiq::Logger::Formatters::Pretty, - Sidekiq::Logger::Formatters::WithoutTimestamp, - Sidekiq::Logger::Formatters::JSON, ] - formats.each do |fmt| - @logger.formatter = fmt.new - Sidekiq::Context.with(class: 'HaikuWorker', bid: 'b-1234abc') do - @logger.info("hello context") - end - assert_match(/INFO/, @output.string) - assert_match(/hello context/, @output.string) - assert_match(/b-1234abc/, @output.string) - reset(@output) - end - end - - def test_json_output_is_parsable - @logger.formatter = Sidekiq::Logger::Formatters::JSON.new - - @logger.debug("boom") - Sidekiq::Context.with(class: 'HaikuWorker', jid: '1234abc') do - @logger.info("json format") - end - a, b = @output.string.lines - hash = JSON.parse(a) - keys = hash.keys.sort - assert_equal ["lvl", "msg", "pid", "tid", "ts"], keys - assert_nil hash["ctx"] - assert_equal hash["lvl"], "DEBUG" - - hash = JSON.parse(b) - keys = hash.keys.sort - assert_equal ["ctx", "lvl", "msg", "pid", "tid", "ts"], keys - refute_nil hash["ctx"] - assert_equal "1234abc", hash["ctx"]["jid"] - assert_equal "INFO", hash["lvl"] - end - - def test_forwards_logger_kwargs - assert_silent do - logger = Sidekiq::Logger.new('/dev/null', level: Logger::INFO) - - assert_equal Logger::INFO, logger.level - end - end - - def test_log_level_query_methods - logger = Sidekiq::Logger.new('/dev/null', level: Logger::INFO) - - refute_predicate logger, :debug? - assert_predicate logger, :info? - assert_predicate logger, :warn? - - logger.level = Logger::WARN - refute_predicate logger, :info? - assert_predicate logger, :warn? - end - - def reset(io) - io.truncate(0) - io.rewind - end -end diff --git a/test/test_manager.rb b/test/test_manager.rb deleted file mode 100644 index 0d165985..00000000 --- a/test/test_manager.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/manager' - -describe Sidekiq::Manager do - before do - Sidekiq.redis {|c| c.flushdb } - end - - def new_manager(opts) - Sidekiq::Manager.new(opts.merge(fetch: Sidekiq::BasicFetch.new(opts))) - end - - it 'creates N processor instances' do - mgr = new_manager(options) - assert_equal options[:concurrency], mgr.workers.size - end - - it 'shuts down the system' do - mgr = new_manager(options) - mgr.stop(::Process.clock_gettime(::Process::CLOCK_MONOTONIC)) - end - - it 'throws away dead processors' do - mgr = new_manager(options) - init_size = mgr.workers.size - processor = mgr.workers.first - begin - mgr.processor_died(processor, 'ignored') - - assert_equal init_size, mgr.workers.size - refute mgr.workers.include?(processor) - ensure - mgr.workers.each {|p| p.terminate(true) } - end - end - - it 'does not support invalid concurrency' do - assert_raises(ArgumentError) { new_manager(concurrency: 0) } - assert_raises(ArgumentError) { new_manager(concurrency: -1) } - end - - def options - { :concurrency => 3, :queues => ['default'] } - end - -end diff --git a/test/test_middleware.rb b/test/test_middleware.rb deleted file mode 100644 index 4fdf3c18..00000000 --- a/test/test_middleware.rb +++ /dev/null @@ -1,169 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/middleware/chain' -require 'sidekiq/processor' - -describe Sidekiq::Middleware do - before do - $errors = [] - end - - class CustomMiddleware - def initialize(name, recorder) - @name = name - @recorder = recorder - end - - def call(*args) - @recorder << [@name, 'before'] - yield - @recorder << [@name, 'after'] - end - end - - it 'supports custom middleware' do - chain = Sidekiq::Middleware::Chain.new - chain.add CustomMiddleware, 1, [] - - assert_equal CustomMiddleware, chain.entries.last.klass - end - - class CustomWorker - $recorder = [] - include Sidekiq::Worker - def perform(recorder) - $recorder << ['work_performed'] - end - end - - class NonYieldingMiddleware - def call(*args) - end - end - - class ArgumentYieldingMiddleware - def call(*args) - yield 1 - end - end - - class AnotherCustomMiddleware - def initialize(name, recorder) - @name = name - @recorder = recorder - end - - def call(*args) - @recorder << [@name, 'before'] - yield - @recorder << [@name, 'after'] - end - end - - class YetAnotherCustomMiddleware - def initialize(name, recorder) - @name = name - @recorder = recorder - end - - def call(*args) - @recorder << [@name, 'before'] - yield - @recorder << [@name, 'after'] - end - end - - it 'executes middleware in the proper order' do - msg = Sidekiq.dump_json({ 'class' => CustomWorker.to_s, 'args' => [$recorder] }) - - Sidekiq.server_middleware do |chain| - # should only add once, second should replace the first - 2.times { |i| chain.add CustomMiddleware, i.to_s, $recorder } - chain.insert_before CustomMiddleware, AnotherCustomMiddleware, '2', $recorder - chain.insert_after AnotherCustomMiddleware, YetAnotherCustomMiddleware, '3', $recorder - end - - boss = Minitest::Mock.new - opts = {:queues => ['default'] } - processor = Sidekiq::Processor.new(boss, opts) - boss.expect(:processor_done, nil, [processor]) - processor.process(Sidekiq::BasicFetch::UnitOfWork.new('queue:default', msg)) - assert_equal %w(2 before 3 before 1 before work_performed 1 after 3 after 2 after), $recorder.flatten - end - - it 'correctly replaces middleware when using middleware with options in the initializer' do - chain = Sidekiq::Middleware::Chain.new - chain.add NonYieldingMiddleware - chain.add NonYieldingMiddleware, {:foo => 5} - assert_equal 1, chain.count - end - - it 'correctly prepends middleware' do - chain = Sidekiq::Middleware::Chain.new - chain_entries = chain.entries - chain.add CustomMiddleware - chain.prepend YetAnotherCustomMiddleware - assert_equal YetAnotherCustomMiddleware, chain_entries.first.klass - assert_equal CustomMiddleware, chain_entries.last.klass - end - - it 'allows middleware to abruptly stop processing rest of chain' do - recorder = [] - chain = Sidekiq::Middleware::Chain.new - chain.add NonYieldingMiddleware - chain.add CustomMiddleware, 1, recorder - - final_action = nil - chain.invoke { final_action = true } - assert_nil final_action - assert_equal [], recorder - end - - it 'allows middleware to yield arguments' do - chain = Sidekiq::Middleware::Chain.new - chain.add ArgumentYieldingMiddleware - - final_action = nil - chain.invoke { final_action = true } - assert_equal true, final_action - end - - describe 'I18n' do - before do - require 'i18n' - I18n.enforce_available_locales = false - require 'sidekiq/middleware/i18n' - end - - it 'saves and restores locale' do - I18n.locale = 'fr' - msg = {} - mw = Sidekiq::Middleware::I18n::Client.new - mw.call(nil, msg, nil, nil) { } - assert_equal :fr, msg['locale'] - - msg['locale'] = 'jp' - I18n.locale = I18n.default_locale - assert_equal :en, I18n.locale - mw = Sidekiq::Middleware::I18n::Server.new - mw.call(nil, msg, nil) do - assert_equal :jp, I18n.locale - end - assert_equal :en, I18n.locale - end - - it 'supports I18n.enforce_available_locales = true' do - I18n.enforce_available_locales = true - I18n.available_locales = [:en, :jp] - - msg = { 'locale' => 'jp' } - mw = Sidekiq::Middleware::I18n::Server.new - mw.call(nil, msg, nil) do - assert_equal :jp, I18n.locale - end - - I18n.enforce_available_locales = false - I18n.available_locales = nil - end - end -end diff --git a/test/test_processor.rb b/test/test_processor.rb deleted file mode 100644 index ae1fcc4d..00000000 --- a/test/test_processor.rb +++ /dev/null @@ -1,367 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/fetch' -require 'sidekiq/cli' -require 'sidekiq/processor' - -describe Sidekiq::Processor do - TestProcessorException = Class.new(StandardError) - TEST_PROC_EXCEPTION = TestProcessorException.new("kerboom!") - - before do - $invokes = 0 - @mgr = Minitest::Mock.new - opts = {:queues => ['default']} - opts[:fetch] = Sidekiq::BasicFetch.new(opts) - @processor = ::Sidekiq::Processor.new(@mgr, opts) - end - - class MockWorker - include Sidekiq::Worker - def perform(args) - raise TEST_PROC_EXCEPTION if args.to_s == 'boom' - args.pop if args.is_a? Array - $invokes += 1 - end - end - - def work(msg, queue='queue:default') - Sidekiq::BasicFetch::UnitOfWork.new(queue, msg) - end - - it 'processes as expected' do - msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['myarg'] }) - @processor.process(work(msg)) - assert_equal 1, $invokes - end - - it 'executes a worker as expected' do - worker = Minitest::Mock.new - worker.expect(:perform, nil, [1, 2, 3]) - @processor.execute_job(worker, [1, 2, 3]) - end - - it 're-raises exceptions after handling' do - msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['boom'] }) - re_raise = false - - begin - @processor.process(work(msg)) - flunk "Expected exception" - rescue TestProcessorException - re_raise = true - end - - assert_equal 0, $invokes - assert re_raise, "does not re-raise exceptions after handling" - end - - it 'does not modify original arguments' do - msg = { 'class' => MockWorker.to_s, 'args' => [['myarg']] } - msgstr = Sidekiq.dump_json(msg) - @mgr.expect(:processor_done, nil, [@processor]) - @processor.process(work(msgstr)) - assert_equal [['myarg']], msg['args'] - end - - describe 'exception handling' do - let(:errors) { [] } - let(:error_handler) do - proc do |exception, context| - errors << { exception: exception, context: context } - end - end - - before do - Sidekiq.error_handlers << error_handler - end - - after do - Sidekiq.error_handlers.pop - end - - it 'handles invalid JSON' do - ds = Sidekiq::DeadSet.new - ds.clear - job_hash = { 'class' => MockWorker.to_s, 'args' => ['boom'] } - msg = Sidekiq.dump_json(job_hash) - job = work(msg[0...-2]) - ds = Sidekiq::DeadSet.new - assert_equal 0, ds.size - begin - @processor.instance_variable_set(:'@job', job) - @processor.process(job) - rescue JSON::ParserError - end - assert_equal 1, ds.size - end - - it 'handles exceptions raised by the job' do - job_hash = { 'class' => MockWorker.to_s, 'args' => ['boom'], 'jid' => '123987123' } - msg = Sidekiq.dump_json(job_hash) - job = work(msg) - begin - @processor.instance_variable_set(:'@job', job) - @processor.process(job) - rescue TestProcessorException - end - assert_equal 1, errors.count - assert_instance_of TestProcessorException, errors.first[:exception] - assert_equal msg, errors.first[:context][:jobstr] - assert_equal job_hash['jid'], errors.first[:context][:job]['jid'] - end - - it 'handles exceptions raised by the reloader' do - job_hash = { 'class' => MockWorker.to_s, 'args' => ['boom'] } - msg = Sidekiq.dump_json(job_hash) - @processor.instance_variable_set(:'@reloader', proc { raise TEST_PROC_EXCEPTION }) - job = work(msg) - begin - @processor.instance_variable_set(:'@job', job) - @processor.process(job) - rescue TestProcessorException - end - assert_equal 1, errors.count - assert_instance_of TestProcessorException, errors.first[:exception] - assert_equal msg, errors.first[:context][:jobstr] - assert_equal job_hash, errors.first[:context][:job] - end - - it 'handles exceptions raised during fetch' do - fetch_stub = lambda { raise StandardError, "fetch exception" } - # swallow logging because actually care about the added exception handler - capture_logging do - @processor.instance_variable_get('@strategy').stub(:retrieve_work, fetch_stub) do - @processor.process_one - end - end - - assert_instance_of StandardError, errors.last[:exception] - end - end - - describe 'acknowledgement' do - class ExceptionRaisingMiddleware - def initialize(raise_before_yield, raise_after_yield, skip) - @raise_before_yield = raise_before_yield - @raise_after_yield = raise_after_yield - @skip = skip - end - - def call(worker, item, queue) - raise TEST_PROC_EXCEPTION if @raise_before_yield - yield unless @skip - raise TEST_PROC_EXCEPTION if @raise_after_yield - end - end - - let(:raise_before_yield) { false } - let(:raise_after_yield) { false } - let(:skip_job) { false } - let(:worker_args) { ['myarg'] } - let(:work) { MiniTest::Mock.new } - - before do - work.expect(:queue_name, 'queue:default') - work.expect(:job, Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => worker_args })) - Sidekiq.server_middleware do |chain| - chain.prepend ExceptionRaisingMiddleware, raise_before_yield, raise_after_yield, skip_job - end - end - - after do - Sidekiq.server_middleware do |chain| - chain.remove ExceptionRaisingMiddleware - end - work.verify - end - - describe 'middleware throws an exception before processing the work' do - let(:raise_before_yield) { true } - - it 'acks the job' do - work.expect(:acknowledge, nil) - begin - @processor.process(work) - flunk "Expected #process to raise exception" - rescue TestProcessorException - end - end - end - - describe 'middleware throws an exception after processing the work' do - let(:raise_after_yield) { true } - - it 'acks the job' do - work.expect(:acknowledge, nil) - begin - @processor.process(work) - flunk "Expected #process to raise exception" - rescue TestProcessorException - end - end - end - - describe 'middleware decides to skip work' do - let(:skip_job) { true } - - it 'acks the job' do - work.expect(:acknowledge, nil) - @mgr.expect(:processor_done, nil, [@processor]) - @processor.process(work) - end - end - - describe 'worker raises an exception' do - let(:worker_args) { ['boom'] } - - it 'acks the job' do - work.expect(:acknowledge, nil) - begin - @processor.process(work) - flunk "Expected #process to raise exception" - rescue TestProcessorException - end - end - end - - describe 'everything goes well' do - it 'acks the job' do - work.expect(:acknowledge, nil) - @mgr.expect(:processor_done, nil, [@processor]) - @processor.process(work) - end - end - end - - describe 'retry' do - class ArgsMutatingServerMiddleware - def call(worker, item, queue) - item['args'] = item['args'].map do |arg| - arg.to_sym if arg.is_a?(String) - end - yield - end - end - - class ArgsMutatingClientMiddleware - def call(worker, item, queue, redis_pool) - item['args'] = item['args'].map do |arg| - arg.to_s if arg.is_a?(Symbol) - end - yield - end - end - - before do - Sidekiq.server_middleware do |chain| - chain.prepend ArgsMutatingServerMiddleware - end - Sidekiq.client_middleware do |chain| - chain.prepend ArgsMutatingClientMiddleware - end - end - - after do - Sidekiq.server_middleware do |chain| - chain.remove ArgsMutatingServerMiddleware - end - Sidekiq.client_middleware do |chain| - chain.remove ArgsMutatingClientMiddleware - end - end - - describe 'middleware mutates the job args and then fails' do - it 'requeues with original arguments' do - job_data = { 'class' => MockWorker.to_s, 'args' => ['boom'] } - - retry_stub_called = false - retry_stub = lambda { |worker, msg, queue, exception| - retry_stub_called = true - assert_equal 'boom', msg['args'].first - } - - @processor.instance_variable_get('@retrier').stub(:attempt_retry, retry_stub) do - msg = Sidekiq.dump_json(job_data) - begin - @processor.process(work(msg)) - flunk "Expected exception" - rescue TestProcessorException - end - end - - assert retry_stub_called - end - end - end - - describe 'stats' do - before do - Sidekiq.redis {|c| c.flushdb } - end - - describe 'when successful' do - let(:processed_today_key) { "stat:processed:#{Time.now.utc.strftime("%Y-%m-%d")}" } - - def successful_job - msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['myarg'] }) - @mgr.expect(:processor_done, nil, [@processor]) - @processor.process(work(msg)) - end - - it 'increments processed stat' do - Sidekiq::Processor::PROCESSED.reset - successful_job - assert_equal 1, Sidekiq::Processor::PROCESSED.reset - end - end - - describe 'custom job logger class' do - class CustomJobLogger < Sidekiq::JobLogger - def call(item, queue) - yield - rescue Exception - raise - end - end - - before do - opts = {:queues => ['default'], job_logger: CustomJobLogger} - @mgr = Minitest::Mock.new - @processor = ::Sidekiq::Processor.new(@mgr, opts) - end - end - end - - describe 'stats' do - before do - Sidekiq.redis {|c| c.flushdb } - end - - def successful_job - msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['myarg'] }) - @mgr.expect(:processor_done, nil, [@processor]) - @processor.process(work(msg)) - end - - it 'increments processed stat' do - Sidekiq::Processor::PROCESSED.reset - successful_job - assert_equal 1, Sidekiq::Processor::PROCESSED.reset - end - end - - describe 'custom job logger class' do - before do - opts = {:queues => ['default'], :job_logger => CustomJobLogger} - opts[:fetch] = Sidekiq::BasicFetch.new(opts) - @processor = ::Sidekiq::Processor.new(nil, opts) - end - - it 'is called instead default Sidekiq::JobLogger' do - msg = Sidekiq.dump_json({ 'class' => MockWorker.to_s, 'args' => ['myarg'] }) - @processor.process(work(msg)) - assert_equal 1, $invokes - end - end -end diff --git a/test/test_rails.rb b/test/test_rails.rb deleted file mode 100644 index c5051ea2..00000000 --- a/test/test_rails.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/rails' -require 'sidekiq/api' - -describe 'ActiveJob' do - before do - Sidekiq.redis {|c| c.flushdb } - # need to force this since we aren't booting a Rails app - ActiveJob::Base.queue_adapter = :sidekiq - ActiveJob::Base.logger = nil - ActiveJob::Base.send(:include, ::Sidekiq::Worker::Options) unless ActiveJob::Base.respond_to?(:sidekiq_options) - end - - it 'does not allow Sidekiq::Worker in AJ::Base classes' do - ex = assert_raises ArgumentError do - Class.new(ActiveJob::Base) do - include Sidekiq::Worker - end - end - assert_includes ex.message, "Sidekiq::Worker cannot be included" - end - - it 'loads Sidekiq::Worker::Options in AJ::Base classes' do - aj = Class.new(ActiveJob::Base) do - queue_as :bar - sidekiq_options retry: 4, queue: 'foo', backtrace: 5 - sidekiq_retry_in { |count, _exception| count * 10 } - sidekiq_retries_exhausted do |msg, _exception| - Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}" - end - end - - assert_equal 4, aj.get_sidekiq_options["retry"] - - # When using ActiveJobs, you cannot set the queue with sidekiq_options, you must use - # queue_as or set(queue: ...). This is to avoid duplicate ways of doing the same thing. - instance = aj.perform_later(1, 2, 3) - q = Sidekiq::Queue.new("foo") - assert_equal 0, q.size - q = Sidekiq::Queue.new("bar") - assert_equal 1, q.size - assert_equal 24, instance.provider_job_id.size - - job = q.first - assert_equal 4, job["retry"] - assert_equal 5, job["backtrace"] - # AJ's queue_as should take precedence over Sidekiq's queue option - assert_equal "bar", job["queue"] - end -end diff --git a/test/test_redis_connection.rb b/test/test_redis_connection.rb deleted file mode 100644 index 1782ecff..00000000 --- a/test/test_redis_connection.rb +++ /dev/null @@ -1,300 +0,0 @@ -# frozen_string_literal: true - -require_relative 'helper' -require 'sidekiq/cli' - -describe Sidekiq::RedisConnection do - describe "create" do - before do - Sidekiq.options = Sidekiq::DEFAULTS.dup - @old = ENV['REDIS_URL'] - ENV['REDIS_URL'] = 'redis://localhost/15' - end - - after do - ENV['REDIS_URL'] = @old - end - - # To support both redis-rb 3.3.x #client and 4.0.x #_client - def client_for(redis) - if redis.respond_to?(:_client) - redis._client - else - redis.client - end - end - - it "creates a pooled redis connection" do - pool = Sidekiq::RedisConnection.create - assert_equal Redis, pool.checkout.class - end - - # Readers for these ivars should be available in the next release of - # `connection_pool`, until then we need to reach into the internal state to - # verify the setting. - describe "size" do - def client_connection(*args) - Sidekiq.stub(:server?, nil) do - Sidekiq::RedisConnection.create(*args) - end - end - - def server_connection(*args) - Sidekiq.stub(:server?, "constant") do - Sidekiq::RedisConnection.create(*args) - end - end - - it "uses the specified custom pool size" do - pool = client_connection(size: 42) - assert_equal 42, pool.instance_eval{ @size } - assert_equal 42, pool.instance_eval{ @available.length } - - pool = server_connection(size: 42) - assert_equal 42, pool.instance_eval{ @size } - assert_equal 42, pool.instance_eval{ @available.length } - end - - it "defaults server pool sizes based on concurrency with padding" do - _expected_padding = 5 - prev_concurrency = Sidekiq.options[:concurrency] - Sidekiq.options[:concurrency] = 6 - pool = server_connection - - assert_equal 11, pool.instance_eval{ @size } - assert_equal 11, pool.instance_eval{ @available.length } - ensure - Sidekiq.options[:concurrency] = prev_concurrency - end - - it "defaults client pool sizes to 5" do - pool = client_connection - - assert_equal 5, pool.instance_eval{ @size } - assert_equal 5, pool.instance_eval{ @available.length } - end - - it "changes client pool sizes with ENV" do - begin - ENV['RAILS_MAX_THREADS'] = '9' - pool = client_connection - - assert_equal 9, pool.instance_eval{ @size } - assert_equal 9, pool.instance_eval{ @available.length } - ensure - ENV.delete('RAILS_MAX_THREADS') - end - end - end - - it "disables client setname with nil id" do - pool = Sidekiq::RedisConnection.create(:id => nil) - assert_equal Redis, pool.checkout.class - assert_equal "redis://localhost:6379/15", pool.checkout.connection.fetch(:id) - end - - describe "network_timeout" do - it "sets a custom network_timeout if specified" do - pool = Sidekiq::RedisConnection.create(:network_timeout => 8) - redis = pool.checkout - - assert_equal 8, client_for(redis).timeout - end - - it "uses the default network_timeout if none specified" do - pool = Sidekiq::RedisConnection.create - redis = pool.checkout - - assert_equal 5, client_for(redis).timeout - end - end - - describe "namespace" do - it "uses a given :namespace set by a symbol key" do - pool = Sidekiq::RedisConnection.create(:namespace => "xxx") - assert_equal "xxx", pool.checkout.namespace - end - - it "uses a given :namespace set by a string key" do - pool = Sidekiq::RedisConnection.create("namespace" => "xxx") - assert_equal "xxx", pool.checkout.namespace - end - - it "uses given :namespace over :namespace from Sidekiq.options" do - Sidekiq.options[:namespace] = "xxx" - pool = Sidekiq::RedisConnection.create(:namespace => "yyy") - assert_equal "yyy", pool.checkout.namespace - end - end - - describe "socket path" do - it "uses a given :path" do - pool = Sidekiq::RedisConnection.create(:path => "/var/run/redis.sock") - assert_equal "unix", client_for(pool.checkout).scheme - assert_equal "/var/run/redis.sock", pool.checkout.connection.fetch(:location) - assert_equal 15, pool.checkout.connection.fetch(:db) - end - - it "uses a given :path and :db" do - pool = Sidekiq::RedisConnection.create(:path => "/var/run/redis.sock", :db => 8) - assert_equal "unix", client_for(pool.checkout).scheme - assert_equal "/var/run/redis.sock", pool.checkout.connection.fetch(:location) - assert_equal 8, pool.checkout.connection.fetch(:db) - end - end - - describe "pool_timeout" do - it "uses a given :timeout over the default of 1" do - pool = Sidekiq::RedisConnection.create(:pool_timeout => 5) - - assert_equal 5, pool.instance_eval{ @timeout } - end - - it "uses the default timeout of 1 if no override" do - pool = Sidekiq::RedisConnection.create - - assert_equal 1, pool.instance_eval{ @timeout } - end - end - - describe "driver" do - it "uses redis' ruby driver" do - pool = Sidekiq::RedisConnection.create - redis = pool.checkout - - assert_equal Redis::Connection::Ruby, redis.instance_variable_get(:@client).driver - end - - it "uses redis' default driver if there are many available" do - begin - redis_driver = Object.new - Redis::Connection.drivers << redis_driver - - pool = Sidekiq::RedisConnection.create - redis = pool.checkout - - assert_equal redis_driver, redis.instance_variable_get(:@client).driver - ensure - Redis::Connection.drivers.pop - end - end - - it "uses a given :driver" do - redis_driver = Object.new - pool = Sidekiq::RedisConnection.create(:driver => redis_driver) - redis = pool.checkout - - assert_equal redis_driver, redis.instance_variable_get(:@client).driver - end - end - - describe 'logging redis options' do - it 'redacts credentials' do - options = { - role: 'master', - master_name: 'mymaster', - sentinels: [ - { host: 'host1', port: 26379, password: 'secret'}, - { host: 'host2', port: 26379, password: 'secret'}, - { host: 'host3', port: 26379, password: 'secret'}, - ], - password: 'secret' - } - - output = capture_logging do - Sidekiq::RedisConnection.create(options) - end - - refute_includes(options.inspect, "REDACTED") - assert_includes(output, ':host=>"host1", :port=>26379, :password=>"REDACTED"') - assert_includes(output, ':host=>"host2", :port=>26379, :password=>"REDACTED"') - assert_includes(output, ':host=>"host3", :port=>26379, :password=>"REDACTED"') - assert_includes(output, ':password=>"REDACTED"') - end - - it 'prunes SSL parameters from the logging' do - options = { - ssl_params: { - cert_store: OpenSSL::X509::Store.new - } - } - - output = capture_logging do - Sidekiq::RedisConnection.create(options) - end - - assert_includes(options.inspect, "ssl_params") - refute_includes(output, "ssl_params") - end - end - end - - describe ".determine_redis_provider" do - - before do - @old_env = ENV.to_hash - end - - after do - ENV.update(@old_env) - end - - def with_env_var(var, uri, skip_provider=false) - vars = ['REDISTOGO_URL', 'REDIS_PROVIDER', 'REDIS_URL'] - [var] - vars.each do |v| - next if skip_provider - ENV[v] = nil - end - ENV[var] = uri - assert_equal uri, Sidekiq::RedisConnection.__send__(:determine_redis_provider) - ENV[var] = nil - end - - describe "with REDISTOGO_URL and a parallel REDIS_PROVIDER set" do - it "sets connection URI to the provider" do - uri = 'redis://sidekiq-redis-provider:6379/0' - provider = 'SIDEKIQ_REDIS_PROVIDER' - - ENV['REDIS_PROVIDER'] = provider - ENV[provider] = uri - ENV['REDISTOGO_URL'] = 'redis://redis-to-go:6379/0' - with_env_var provider, uri, true - - ENV[provider] = nil - end - end - - describe "with REDIS_PROVIDER set" do - it "rejects URLs in REDIS_PROVIDER" do - uri = 'redis://sidekiq-redis-provider:6379/0' - - ENV['REDIS_PROVIDER'] = uri - - assert_raises RuntimeError do - Sidekiq::RedisConnection.__send__(:determine_redis_provider) - end - - ENV['REDIS_PROVIDER'] = nil - end - - it "sets connection URI to the provider" do - uri = 'redis://sidekiq-redis-provider:6379/0' - provider = 'SIDEKIQ_REDIS_PROVIDER' - - ENV['REDIS_PROVIDER'] = provider - ENV[provider] = uri - - with_env_var provider, uri, true - - ENV[provider] = nil - end - end - - describe "with REDIS_URL set" do - it "sets connection URI to custom uri" do - with_env_var 'REDIS_URL', 'redis://redis-uri:6379/0' - end - end - - end -end diff --git a/test/test_retry.rb b/test/test_retry.rb deleted file mode 100644 index 30a1fe24..00000000 --- a/test/test_retry.rb +++ /dev/null @@ -1,373 +0,0 @@ -# encoding: utf-8 -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/scheduled' -require 'sidekiq/job_retry' -require 'sidekiq/api' - -describe Sidekiq::JobRetry do - describe 'middleware' do - class SomeWorker - include Sidekiq::Worker - end - - class BadErrorMessage < StandardError - def message - raise "Ahhh, this isn't supposed to happen" - end - end - - before do - Sidekiq.redis {|c| c.flushdb } - end - - def worker - @worker ||= SomeWorker.new - end - - def handler(options={}) - @handler ||= Sidekiq::JobRetry.new(options) - end - - def jobstr(options={}) - Sidekiq.dump_json({ 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true }.merge(options)) - end - - def job - Sidekiq::RetrySet.new.first - end - - it 'retries with a nil worker' do - assert_raises RuntimeError do - handler.global(jobstr, 'default') do - raise "boom" - end - end - assert_equal 1, Sidekiq::RetrySet.new.size - end - - it 'allows disabling retry' do - assert_raises RuntimeError do - handler.local(worker, jobstr('retry' => false), 'default') do - raise "kerblammo!" - end - end - assert_equal 0, Sidekiq::RetrySet.new.size - end - - it 'allows a numeric retry' do - assert_raises RuntimeError do - handler.local(worker, jobstr('retry' => 2), 'default') do - raise "kerblammo!" - end - end - assert_equal 1, Sidekiq::RetrySet.new.size - assert_equal 0, Sidekiq::DeadSet.new.size - end - - it 'allows 0 retry => no retry and dead queue' do - assert_raises RuntimeError do - handler.local(worker, jobstr('retry' => 0), 'default') do - raise "kerblammo!" - end - end - assert_equal 0, Sidekiq::RetrySet.new.size - assert_equal 1, Sidekiq::DeadSet.new.size - end - - it 'handles zany characters in error message, #1705' do - skip 'skipped! test requires ruby 2.1+' if RUBY_VERSION <= '2.1.0' - - assert_raises RuntimeError do - handler.local(worker, jobstr, 'default') do - raise "kerblammo! #{195.chr}" - end - end - assert_equal "kerblammo! �", job["error_message"] - end - - # In the rare event that an error message raises an error itself, - # allow the job to retry. This will likely only happen for custom - # error classes that override #message - it 'handles error message that raises an error' do - assert_raises RuntimeError do - handler.local(worker, jobstr, 'default') do - raise BadErrorMessage.new - end - end - - assert_equal 1, Sidekiq::RetrySet.new.size - refute_nil job["error_message"] - end - - it 'allows a max_retries option in initializer' do - max_retries = 7 - 1.upto(max_retries + 1) do |i| - assert_raises RuntimeError do - job = i > 1 ? jobstr('retry_count' => i - 2) : jobstr - handler(:max_retries => max_retries).local(worker, job, 'default') do - raise "kerblammo!" - end - end - end - - assert_equal max_retries, Sidekiq::RetrySet.new.size - assert_equal 1, Sidekiq::DeadSet.new.size - end - - it 'saves backtraces' do - c = nil - assert_raises RuntimeError do - handler.local(worker, jobstr('backtrace' => true), 'default') do - c = caller(0); raise "kerblammo!" - end - end - - job = Sidekiq::RetrySet.new.first - assert job.error_backtrace - assert_equal c[0], job.error_backtrace[0] - end - - it 'saves partial backtraces' do - c = nil - assert_raises RuntimeError do - handler.local(worker, jobstr('backtrace' => 3), 'default') do - c = caller(0)[0...3]; raise "kerblammo!" - end - end - - job = Sidekiq::RetrySet.new.first - assert job.error_backtrace - assert_equal c, job.error_backtrace - assert_equal 3, c.size - end - - it 'handles a new failed message' do - assert_raises RuntimeError do - handler.local(worker, jobstr, 'default') do - raise "kerblammo!" - end - end - assert_equal 'default', job["queue"] - assert_equal 'kerblammo!', job["error_message"] - assert_equal 'RuntimeError', job["error_class"] - assert_equal 0, job["retry_count"] - refute job["error_backtrace"] - assert job["failed_at"] - end - - it 'shuts down without retrying work-in-progress, which will resume' do - rs = Sidekiq::RetrySet.new - assert_equal 0, rs.size - msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true } - assert_raises Sidekiq::Shutdown do - handler.local(worker, msg, 'default') do - raise Sidekiq::Shutdown - end - end - assert_equal 0, rs.size - end - - it 'shuts down cleanly when shutdown causes exception' do - skip('Not supported in Ruby < 2.1.0') if RUBY_VERSION < '2.1.0' - - rs = Sidekiq::RetrySet.new - assert_equal 0, rs.size - msg = { 'class' => 'Bob', 'args' => [1,2,'foo'], 'retry' => true } - assert_raises Sidekiq::Shutdown do - handler.local(worker, msg, 'default') do - begin - raise Sidekiq::Shutdown - rescue Interrupt - raise "kerblammo!" - end - end - end - assert_equal 0, rs.size - end - - it 'shuts down cleanly when shutdown causes chained exceptions' do - skip('Not supported in Ruby < 2.1.0') if RUBY_VERSION < '2.1.0' - - rs = Sidekiq::RetrySet.new - assert_equal 0, rs.size - assert_raises Sidekiq::Shutdown do - handler.local(worker, jobstr, 'default') do - begin - raise Sidekiq::Shutdown - rescue Interrupt - begin - raise "kerblammo!" - rescue - raise "kablooie!" - end - end - end - end - assert_equal 0, rs.size - end - - it 'allows a retry queue' do - assert_raises RuntimeError do - handler.local(worker, jobstr("retry_queue" => 'retryx'), 'default') do - raise "kerblammo!" - end - end - assert_equal 'retryx', job["queue"] - assert_equal 'kerblammo!', job["error_message"] - assert_equal 'RuntimeError', job["error_class"] - assert_equal 0, job["retry_count"] - refute job["error_backtrace"] - assert job["failed_at"] - end - - it 'handles a recurring failed message' do - now = Time.now.to_f - msg = {"queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>now, "retry_count"=>10} - assert_raises RuntimeError do - handler.local(worker, jobstr(msg), 'default') do - raise "kerblammo!" - end - end - assert_equal 'default', job["queue"] - assert_equal 'kerblammo!', job["error_message"] - assert_equal 'RuntimeError', job["error_class"] - assert_equal 11, job["retry_count"] - assert job["failed_at"] - end - - it 'throws away old messages after too many retries (using the default)' do - q = Sidekiq::Queue.new - rs = Sidekiq::RetrySet.new - ds = Sidekiq::DeadSet.new - assert_equal 0, q.size - assert_equal 0, rs.size - assert_equal 0, ds.size - now = Time.now.to_f - msg = {"queue"=>"default", "error_message"=>"kerblammo!", "error_class"=>"RuntimeError", "failed_at"=>now, "retry_count"=>25} - assert_raises RuntimeError do - handler.local(worker, jobstr(msg), 'default') do - raise "kerblammo!" - end - end - assert_equal 0, q.size - assert_equal 0, rs.size - assert_equal 1, ds.size - end - - describe "custom retry delay" do - before do - @old_logger = Sidekiq.logger - @tmp_log_path = '/tmp/sidekiq-retries.log' - Sidekiq.logger = Logger.new(@tmp_log_path) - end - - after do - Sidekiq.logger = @old_logger - File.unlink @tmp_log_path if File.exist?(@tmp_log_path) - end - - class CustomWorkerWithoutException - include Sidekiq::Worker - - sidekiq_retry_in do |count| - count * 2 - end - end - - class SpecialError < StandardError - end - - class CustomWorkerWithException - include Sidekiq::Worker - - sidekiq_retry_in do |count, exception| - case exception - when SpecialError - nil - when ArgumentError - count * 4 - else - count * 2 - end - end - end - - class ErrorWorker - include Sidekiq::Worker - - sidekiq_retry_in do |count| - count / 0 - end - end - - it "retries with a default delay" do - refute_equal 4, handler.__send__(:delay_for, worker, 2, StandardError.new) - end - - it "retries with a custom delay and exception 1" do - assert_includes 4..35, handler.__send__(:delay_for, CustomWorkerWithException, 2, ArgumentError.new) - end - - it "retries with a custom delay and exception 2" do - assert_includes 4..35, handler.__send__(:delay_for, CustomWorkerWithException, 2, StandardError.new) - end - - it "retries with a default delay and exception in case of configured with nil" do - refute_equal 8, handler.__send__(:delay_for, CustomWorkerWithException, 2, SpecialError.new) - refute_equal 4, handler.__send__(:delay_for, CustomWorkerWithException, 2, SpecialError.new) - end - - it "retries with a custom delay without exception" do - assert_includes 4..35, handler.__send__(:delay_for, CustomWorkerWithoutException, 2, StandardError.new) - end - - it "falls back to the default retry on exception" do - refute_equal 4, handler.__send__(:delay_for, ErrorWorker, 2, StandardError.new) - assert_match(/Failure scheduling retry using the defined `sidekiq_retry_in`/, - File.read(@tmp_log_path), 'Log entry missing for sidekiq_retry_in') - end - end - - describe 'handles errors withouth cause' do - before do - @error = nil - begin - raise ::StandardError, 'Error' - rescue ::StandardError => e - @error = e - end - end - - it "does not recurse infinitely checking if it's a shutdown" do - assert(!Sidekiq::JobRetry.new.send( - :exception_caused_by_shutdown?, @error)) - end - end - - describe 'handles errors with circular causes' do - before do - @error = nil - begin - begin - raise ::StandardError, 'Error 1' - rescue ::StandardError => e1 - begin - raise ::StandardError, 'Error 2' - rescue ::StandardError - raise e1 - end - end - rescue ::StandardError => e - @error = e - end - end - - it "does not recurse infinitely checking if it's a shutdown" do - assert(!Sidekiq::JobRetry.new.send( - :exception_caused_by_shutdown?, @error)) - end - end - end - -end diff --git a/test/test_retry_exhausted.rb b/test/test_retry_exhausted.rb deleted file mode 100644 index 439eb45c..00000000 --- a/test/test_retry_exhausted.rb +++ /dev/null @@ -1,151 +0,0 @@ -# encoding: utf-8 -require_relative 'helper' -require 'sidekiq/job_retry' - -describe 'sidekiq_retries_exhausted' do - class NewWorker - include Sidekiq::Worker - - sidekiq_class_attribute :exhausted_called, :exhausted_job, :exhausted_exception - - sidekiq_retries_exhausted do |job, e| - self.exhausted_called = true - self.exhausted_job = job - self.exhausted_exception = e - end - end - - class OldWorker - include Sidekiq::Worker - - sidekiq_class_attribute :exhausted_called, :exhausted_job, :exhausted_exception - - sidekiq_retries_exhausted do |job| - self.exhausted_called = true - self.exhausted_job = job - end - end - - def cleanup - [NewWorker, OldWorker].each do |worker_class| - worker_class.exhausted_called = nil - worker_class.exhausted_job = nil - worker_class.exhausted_exception = nil - end - end - - before do - cleanup - end - - after do - cleanup - end - - def new_worker - @new_worker ||= NewWorker.new - end - - def old_worker - @old_worker ||= OldWorker.new - end - - def handler(options={}) - @handler ||= Sidekiq::JobRetry.new(options) - end - - def job(options={}) - @job ||= Sidekiq.dump_json({'class' => 'Bob', 'args' => [1, 2, 'foo']}.merge(options)) - end - - it 'does not run exhausted block when job successful on first run' do - handler.local(new_worker, job('retry' => 2), 'default') do - # successful - end - - refute NewWorker.exhausted_called - end - - it 'does not run exhausted block when job successful on last retry' do - handler.local(new_worker, job('retry_count' => 0, 'retry' => 1), 'default') do - # successful - end - - refute NewWorker.exhausted_called - end - - it 'does not run exhausted block when retries not exhausted yet' do - assert_raises RuntimeError do - handler.local(new_worker, job('retry' => 1), 'default') do - raise 'kerblammo!' - end - end - - refute NewWorker.exhausted_called - end - - it 'runs exhausted block when retries exhausted' do - assert_raises RuntimeError do - handler.local(new_worker, job('retry_count' => 0, 'retry' => 1), 'default') do - raise 'kerblammo!' - end - end - - assert NewWorker.exhausted_called - end - - - it 'passes job and exception to retries exhausted block' do - raised_error = assert_raises RuntimeError do - handler.local(new_worker, job('retry_count' => 0, 'retry' => 1), 'default') do - raise 'kerblammo!' - end - end - raised_error = raised_error.cause - - assert new_worker.exhausted_called - assert_equal raised_error.message, new_worker.exhausted_job['error_message'] - assert_equal raised_error, new_worker.exhausted_exception - end - - it 'passes job to retries exhausted block' do - raised_error = assert_raises RuntimeError do - handler.local(old_worker, job('retry_count' => 0, 'retry' => 1), 'default') do - raise 'kerblammo!' - end - end - raised_error = raised_error.cause - - assert old_worker.exhausted_called - assert_equal raised_error.message, old_worker.exhausted_job['error_message'] - assert_nil new_worker.exhausted_exception - end - - it 'allows global failure handlers' do - begin - class Foobar - include Sidekiq::Worker - end - - exhausted_job = nil - exhausted_exception = nil - Sidekiq.death_handlers.clear - Sidekiq.death_handlers << proc do |job, ex| - exhausted_job = job - exhausted_exception = ex - end - f = Foobar.new - raised_error = assert_raises RuntimeError do - handler.local(f, job('retry_count' => 0, 'retry' => 1), 'default') do - raise 'kerblammo!' - end - end - raised_error = raised_error.cause - - assert exhausted_job - assert_equal raised_error, exhausted_exception - ensure - Sidekiq.death_handlers.clear - end - end -end diff --git a/test/test_scheduled.rb b/test/test_scheduled.rb deleted file mode 100644 index 4fab6c5b..00000000 --- a/test/test_scheduled.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/scheduled' - -describe Sidekiq::Scheduled do - class ScheduledWorker - include Sidekiq::Worker - def perform(x) - end - end - - describe 'poller' do - before do - Sidekiq.redis{|c| c.flushdb} - @error_1 = { 'class' => ScheduledWorker.name, 'args' => [0], 'queue' => 'queue_1' } - @error_2 = { 'class' => ScheduledWorker.name, 'args' => [1], 'queue' => 'queue_2' } - @error_3 = { 'class' => ScheduledWorker.name, 'args' => [2], 'queue' => 'queue_3' } - @future_1 = { 'class' => ScheduledWorker.name, 'args' => [3], 'queue' => 'queue_4' } - @future_2 = { 'class' => ScheduledWorker.name, 'args' => [4], 'queue' => 'queue_5' } - @future_3 = { 'class' => ScheduledWorker.name, 'args' => [5], 'queue' => 'queue_6' } - - @retry = Sidekiq::RetrySet.new - @scheduled = Sidekiq::ScheduledSet.new - @poller = Sidekiq::Scheduled::Poller.new - end - - class MyStopper - def call(worker_class, job, queue, r) - yield if job['args'].first.odd? - end - end - - it 'executes client middleware' do - Sidekiq.client_middleware.add MyStopper - begin - @retry.schedule (Time.now - 60).to_f, @error_1 - @retry.schedule (Time.now - 60).to_f, @error_2 - @scheduled.schedule (Time.now - 60).to_f, @future_2 - @scheduled.schedule (Time.now - 60).to_f, @future_3 - - @poller.enqueue - - assert_equal 0, Sidekiq::Queue.new("queue_1").size - assert_equal 1, Sidekiq::Queue.new("queue_2").size - assert_equal 0, Sidekiq::Queue.new("queue_5").size - assert_equal 1, Sidekiq::Queue.new("queue_6").size - ensure - Sidekiq.client_middleware.remove MyStopper - end - end - - it 'should empty the retry and scheduled queues up to the current time' do - created_time = Time.new(2013, 2, 3) - enqueued_time = Time.new(2013, 2, 4) - - Time.stub(:now, created_time) do - @retry.schedule (enqueued_time - 60).to_f, @error_1.merge!('created_at' => created_time.to_f) - @retry.schedule (enqueued_time - 50).to_f, @error_2.merge!('created_at' => created_time.to_f) - @retry.schedule (enqueued_time + 60).to_f, @error_3.merge!('created_at' => created_time.to_f) - @scheduled.schedule (enqueued_time - 60).to_f, @future_1.merge!('created_at' => created_time.to_f) - @scheduled.schedule (enqueued_time - 50).to_f, @future_2.merge!('created_at' => created_time.to_f) - @scheduled.schedule (enqueued_time + 60).to_f, @future_3.merge!('created_at' => created_time.to_f) - end - - Time.stub(:now, enqueued_time) do - @poller.enqueue - - Sidekiq.redis do |conn| - %w(queue:queue_1 queue:queue_2 queue:queue_4 queue:queue_5).each do |queue_name| - assert_equal 1, conn.llen(queue_name) - job = Sidekiq.load_json(conn.lrange(queue_name, 0, -1)[0]) - assert_equal enqueued_time.to_f, job['enqueued_at'] - assert_equal created_time.to_f, job['created_at'] - end - end - - assert_equal 1, @retry.size - assert_equal 1, @scheduled.size - end - end - - it 'should not enqueue jobs when terminate has been called' do - created_time = Time.new(2013, 2, 3) - enqueued_time = Time.new(2013, 2, 4) - - Time.stub(:now, created_time) do - @retry.schedule (enqueued_time - 60).to_f, @error_1.merge!('created_at' => created_time.to_f) - @scheduled.schedule (enqueued_time - 60).to_f, @future_1.merge!('created_at' => created_time.to_f) - end - - Time.stub(:now, enqueued_time) do - @poller.terminate - @poller.enqueue - - Sidekiq.redis do |conn| - %w(queue:queue_1 queue:queue_4).each do |queue_name| - assert_equal 0, conn.llen(queue_name) - end - end - - assert_equal 1, @retry.size - assert_equal 1, @scheduled.size - end - end - - def with_sidekiq_option(name, value) - _original, Sidekiq.options[name] = Sidekiq.options[name], value - begin - yield - ensure - Sidekiq.options[name] = _original - end - end - - it 'generates random intervals that target a configured average' do - with_sidekiq_option(:poll_interval_average, 10) do - i = 500 - intervals = Array.new(i){ @poller.send(:random_poll_interval) } - - assert intervals.all?{|x| x >= 5} - assert intervals.all?{|x| x <= 15} - assert_in_delta 10, intervals.sum.to_f / i, 0.5 - end - end - - it 'calculates an average poll interval based on the number of known Sidekiq processes' do - with_sidekiq_option(:average_scheduled_poll_interval, 10) do - 3.times do |i| - Sidekiq.redis do |conn| - conn.sadd("processes", "process-#{i}") - conn.hset("process-#{i}", "info", nil) - end - end - - assert_equal 30, @poller.send(:scaled_poll_interval) - end - end - end -end diff --git a/test/test_scheduling.rb b/test/test_scheduling.rb deleted file mode 100644 index debaa57e..00000000 --- a/test/test_scheduling.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/scheduled' -require 'active_support/core_ext/integer/time' - -describe 'job scheduling' do - describe 'middleware' do - class SomeScheduledWorker - include Sidekiq::Worker - sidekiq_options :queue => :custom_queue - def perform(x) - end - end - - # Assume we can pass any class as time to perform_in - class TimeDuck - def to_f; 42.0 end - end - - it 'schedules jobs' do - ss = Sidekiq::ScheduledSet.new - ss.clear - - assert_equal 0, ss.size - - assert SomeScheduledWorker.perform_in(600, 'mike') - assert_equal 1, ss.size - - assert SomeScheduledWorker.perform_in(1.month, 'mike') - assert_equal 2, ss.size - - assert SomeScheduledWorker.perform_in(5.days.from_now, 'mike') - assert_equal 3, ss.size - - q = Sidekiq::Queue.new("custom_queue") - qs = q.size - assert SomeScheduledWorker.perform_in(-300, 'mike') - assert_equal 3, ss.size - assert_equal qs+1, q.size - - assert Sidekiq::Client.push_bulk('class' => SomeScheduledWorker, 'args' => [['mike'], ['mike']], 'at' => 600) - assert_equal 5, ss.size - - assert SomeScheduledWorker.perform_in(TimeDuck.new, 'samwise') - assert_equal 6, ss.size - end - - it 'removes the enqueued_at field when scheduling' do - ss = Sidekiq::ScheduledSet.new - ss.clear - - assert SomeScheduledWorker.perform_in(1.month, 'mike') - job = ss.first - assert job['created_at'] - refute job['enqueued_at'] - end - - it 'removes the enqueued_at field when scheduling in bulk' do - ss = Sidekiq::ScheduledSet.new - ss.clear - - assert Sidekiq::Client.push_bulk('class' => SomeScheduledWorker, 'args' => [['mike'], ['mike']], 'at' => 600) - job = ss.first - assert job['created_at'] - refute job['enqueued_at'] - end - end - -end diff --git a/test/test_sidekiq.rb b/test/test_sidekiq.rb deleted file mode 100644 index 1852a626..00000000 --- a/test/test_sidekiq.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/cli' - -describe Sidekiq do - describe 'json processing' do - it 'handles json' do - assert_equal({"foo" => "bar"}, Sidekiq.load_json("{\"foo\":\"bar\"}")) - assert_equal "{\"foo\":\"bar\"}", Sidekiq.dump_json({ "foo" => "bar" }) - end - end - - describe "redis connection" do - it "returns error without creating a connection if block is not given" do - assert_raises(ArgumentError) do - Sidekiq.redis - end - end - end - - describe "❨╯°□°❩╯︵┻━┻" do - before { $stdout = StringIO.new } - after { $stdout = STDOUT } - - it "allows angry developers to express their emotional constitution and remedies it" do - Sidekiq.❨╯°□°❩╯︵┻━┻ - assert_equal "Calm down, yo.\n", $stdout.string - end - end - - describe 'lifecycle events' do - it 'handles invalid input' do - Sidekiq.options[:lifecycle_events][:startup].clear - - e = assert_raises ArgumentError do - Sidekiq.on(:startp) - end - assert_match(/Invalid event name/, e.message) - e = assert_raises ArgumentError do - Sidekiq.on('startup') - end - assert_match(/Symbols only/, e.message) - Sidekiq.on(:startup) do - 1 + 1 - end - - assert_equal 2, Sidekiq.options[:lifecycle_events][:startup].first.call - end - end - - describe 'default_worker_options' do - it 'stringifies keys' do - @old_options = Sidekiq.default_worker_options - begin - Sidekiq.default_worker_options = { queue: 'cat'} - assert_equal 'cat', Sidekiq.default_worker_options['queue'] - ensure - Sidekiq.default_worker_options = @old_options - end - end - end - - describe 'error handling' do - it 'deals with user-specified error handlers which raise errors' do - output = capture_logging do - begin - Sidekiq.error_handlers << proc {|x, hash| - raise 'boom' - } - cli = Sidekiq::CLI.new - cli.handle_exception(RuntimeError.new("hello")) - ensure - Sidekiq.error_handlers.pop - end - end - assert_includes output, "boom" - assert_includes output, "ERROR" - end - end - - describe 'redis connection' do - it 'does not continually retry' do - assert_raises Redis::CommandError do - Sidekiq.redis do |c| - raise Redis::CommandError, "READONLY You can't write against a replica." - end - end - end - - it 'reconnects if connection is flagged as readonly' do - counts = [] - Sidekiq.redis do |c| - counts << c.info['total_connections_received'].to_i - raise Redis::CommandError, "READONLY You can't write against a replica." if counts.size == 1 - end - assert_equal 2, counts.size - assert_equal counts[0] + 1, counts[1] - end - - it 'reconnects if instance state changed' do - counts = [] - Sidekiq.redis do |c| - counts << c.info['total_connections_received'].to_i - raise Redis::CommandError, "UNBLOCKED force unblock from blocking operation, instance state changed (master -> replica?)" if counts.size == 1 - end - assert_equal 2, counts.size - assert_equal counts[0] + 1, counts[1] - end - end - - describe 'redis info' do - it 'calls the INFO command which returns at least redis_version' do - output = Sidekiq.redis_info - assert_includes output.keys, "redis_version" - end - end -end diff --git a/test/test_sidekiqmon.rb b/test/test_sidekiqmon.rb deleted file mode 100644 index c50b6392..00000000 --- a/test/test_sidekiqmon.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/monitor' - -def capture_stdout - $stdout = StringIO.new - yield - $stdout.string.chomp -ensure - $stdout = STDOUT -end - -def output(section = 'all') - capture_stdout do - Sidekiq::Monitor::Status.new.display(section) - end -end - -describe Sidekiq::Monitor do - before do - Sidekiq.redis {|c| c.flushdb} - end - - describe 'status' do - describe 'version' do - it 'displays the current Sidekiq version' do - assert_includes output, "Sidekiq #{Sidekiq::VERSION}" - end - - it 'displays the current time' do - Time.stub(:now, Time.at(0)) do - assert_includes output, Time.at(0).utc.to_s - end - end - end - - describe 'overview' do - it 'has a heading' do - assert_includes output, '---- Overview ----' - end - - it 'displays the correct output' do - mock_stats = OpenStruct.new( - processed: 420710, - failed: 12, - workers_size: 34, - enqueued: 56, - retry_size: 78, - scheduled_size: 90, - dead_size: 666 - ) - Sidekiq::Stats.stub(:new, mock_stats) do - assert_includes output, 'Processed: 420,710' - assert_includes output, 'Failed: 12' - assert_includes output, 'Busy: 34' - assert_includes output, 'Enqueued: 56' - assert_includes output, 'Retries: 78' - assert_includes output, 'Scheduled: 90' - assert_includes output, 'Dead: 666' - end - end - end - - describe 'processes' do - it 'has a heading' do - assert_includes output, '---- Processes (0) ----' - end - - it 'displays the correct output' do - mock_processes = [{ - 'identity' => 'foobar', - 'tag' => 'baz', - 'started_at' => Time.now, - 'concurrency' => 5, - 'busy' => 2, - 'queues' => %w[low medium high] - }] - Sidekiq::ProcessSet.stub(:new, mock_processes) do - assert_includes output, 'foobar [baz]' - assert_includes output, "Started: #{mock_processes.first['started_at']} (just now)" - assert_includes output, 'Threads: 5 (2 busy)' - assert_includes output, 'Queues: high, low, medium' - end - end - end - - describe 'queues' do - it 'has a heading' do - assert_includes output, '---- Queues (0) ----' - end - - it 'displays the correct output' do - mock_queues = [ - OpenStruct.new(name: 'foobar', size: 12, latency: 12.3456), - OpenStruct.new(name: 'a_long_queue_name', size: 234, latency: 567.89999) - ] - Sidekiq::Queue.stub(:all, mock_queues) do - assert_includes output, 'NAME SIZE LATENCY' - assert_includes output, 'foobar 12 12.35' - assert_includes output, 'a_long_queue_name 234 567.90' - end - end - end - end -end diff --git a/test/test_systemd.rb b/test/test_systemd.rb deleted file mode 100644 index f5a3f18d..00000000 --- a/test/test_systemd.rb +++ /dev/null @@ -1,40 +0,0 @@ -require_relative 'helper' -require "sidekiq/sd_notify" -require 'sidekiq/systemd' - -class TestSystemd < Minitest::Test - def setup - ::Dir::Tmpname.create("sidekiq_socket") do |sockaddr| - @sockaddr = sockaddr - @socket = Socket.new(:UNIX, :DGRAM, 0) - socket_ai = Addrinfo.unix(sockaddr) - @socket.bind(socket_ai) - ENV["NOTIFY_SOCKET"] = sockaddr - end - end - - def teardown - @socket.close if @socket - File.unlink(@sockaddr) if @sockaddr - @socket = nil - @sockaddr = nil - end - - def socket_message - @socket.recvfrom(10)[0] - end - - def test_notify - count = Sidekiq::SdNotify.ready - assert_equal(socket_message, "READY=1") - assert_equal(ENV["NOTIFY_SOCKET"], @sockaddr) - assert_equal(count, 7) - - count = Sidekiq::SdNotify.stopping - assert_equal(socket_message, "STOPPING=1") - assert_equal(ENV["NOTIFY_SOCKET"], @sockaddr) - assert_equal(count, 10) - - refute Sidekiq::SdNotify.watchdog? - end -end diff --git a/test/test_testing.rb b/test/test_testing.rb deleted file mode 100644 index e43aa429..00000000 --- a/test/test_testing.rb +++ /dev/null @@ -1,133 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' - -describe 'Sidekiq::Testing' do - describe 'require/load sidekiq/testing.rb' do - before do - require 'sidekiq/testing' - end - - after do - Sidekiq::Testing.disable! - end - - it 'enables fake testing' do - Sidekiq::Testing.fake! - assert Sidekiq::Testing.enabled? - assert Sidekiq::Testing.fake? - refute Sidekiq::Testing.inline? - end - - it 'enables fake testing in a block' do - Sidekiq::Testing.disable! - assert Sidekiq::Testing.disabled? - refute Sidekiq::Testing.fake? - - Sidekiq::Testing.fake! do - assert Sidekiq::Testing.enabled? - assert Sidekiq::Testing.fake? - refute Sidekiq::Testing.inline? - end - - refute Sidekiq::Testing.enabled? - refute Sidekiq::Testing.fake? - end - - it 'disables testing in a block' do - Sidekiq::Testing.fake! - assert Sidekiq::Testing.fake? - - Sidekiq::Testing.disable! do - refute Sidekiq::Testing.fake? - assert Sidekiq::Testing.disabled? - end - - assert Sidekiq::Testing.fake? - assert Sidekiq::Testing.enabled? - end - end - - describe 'require/load sidekiq/testing/inline.rb' do - before do - require 'sidekiq/testing/inline' - end - - after do - Sidekiq::Testing.disable! - end - - it 'enables inline testing' do - Sidekiq::Testing.inline! - assert Sidekiq::Testing.enabled? - assert Sidekiq::Testing.inline? - refute Sidekiq::Testing.fake? - end - - it 'enables inline testing in a block' do - Sidekiq::Testing.disable! - assert Sidekiq::Testing.disabled? - refute Sidekiq::Testing.fake? - - Sidekiq::Testing.inline! do - assert Sidekiq::Testing.enabled? - assert Sidekiq::Testing.inline? - end - - refute Sidekiq::Testing.enabled? - refute Sidekiq::Testing.inline? - refute Sidekiq::Testing.fake? - end - end - - describe 'with middleware' do - before do - require 'sidekiq/testing' - end - - after do - Sidekiq::Testing.disable! - end - - class AttributeWorker - include Sidekiq::Worker - sidekiq_class_attribute :count - self.count = 0 - attr_accessor :foo - - def perform - self.class.count += 1 if foo == :bar - end - end - - class AttributeMiddleware - def call(worker, msg, queue) - worker.foo = :bar if worker.respond_to?(:foo=) - yield - end - end - - it 'wraps the inlined worker with middleware' do - Sidekiq::Testing.server_middleware do |chain| - chain.add AttributeMiddleware - end - - begin - Sidekiq::Testing.fake! do - AttributeWorker.perform_async - assert_equal 0, AttributeWorker.count - end - - AttributeWorker.perform_one - assert_equal 1, AttributeWorker.count - - Sidekiq::Testing.inline! do - AttributeWorker.perform_async - assert_equal 2, AttributeWorker.count - end - ensure - Sidekiq::Testing.server_middleware.clear - end - end - end - -end diff --git a/test/test_testing_fake.rb b/test/test_testing_fake.rb index 36b40218..3d3baaa7 100644 --- a/test/test_testing_fake.rb +++ b/test/test_testing_fake.rb @@ -26,7 +26,7 @@ def perform(error) end before do - require 'sidekiq/testing' + require 'sidekiq/delay_extensions/testing' Sidekiq::Testing.fake! EnqueuedWorker.jobs.clear DirectWorker.jobs.clear @@ -59,13 +59,13 @@ def bar(str) end before do - Sidekiq::Extensions.enable_delay! + Sidekiq::DelayExtensions.enable_delay! end it 'stubs the delay call on mailers' do - assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size + assert_equal 0, Sidekiq::DelayExtensions::DelayedMailer.jobs.size FooMailer.delay.bar('hello!') - assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size + assert_equal 1, Sidekiq::DelayExtensions::DelayedMailer.jobs.size end class Something @@ -74,9 +74,9 @@ def self.foo(x) end it 'stubs the delay call on classes' do - assert_equal 0, Sidekiq::Extensions::DelayedClass.jobs.size + assert_equal 0, Sidekiq::DelayExtensions::DelayedClass.jobs.size Something.delay.foo(Date.today) - assert_equal 1, Sidekiq::Extensions::DelayedClass.jobs.size + assert_equal 1, Sidekiq::DelayExtensions::DelayedClass.jobs.size end class BarMailer < ActionMailer::Base @@ -86,12 +86,12 @@ def foo(str) end it 'returns enqueued jobs for specific classes' do - assert_equal 0, Sidekiq::Extensions::DelayedClass.jobs.size + assert_equal 0, Sidekiq::DelayExtensions::DelayedClass.jobs.size FooMailer.delay.bar('hello!') BarMailer.delay.foo('hello!') - assert_equal 2, Sidekiq::Extensions::DelayedMailer.jobs.size - assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs_for(FooMailer).size - assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs_for(BarMailer).size + assert_equal 2, Sidekiq::DelayExtensions::DelayedMailer.jobs.size + assert_equal 1, Sidekiq::DelayExtensions::DelayedMailer.jobs_for(FooMailer).size + assert_equal 1, Sidekiq::DelayExtensions::DelayedMailer.jobs_for(BarMailer).size end end @@ -287,7 +287,7 @@ def perform describe 'queue testing' do before do - require 'sidekiq/testing' + require 'sidekiq/delay_extensions/testing' Sidekiq::Testing.fake! end diff --git a/test/test_testing_inline.rb b/test/test_testing_inline.rb index f3cb4b11..e356fb30 100644 --- a/test/test_testing_inline.rb +++ b/test/test_testing_inline.rb @@ -21,6 +21,7 @@ def perform(time) end before do + require 'sidekiq/delay_extensions/testing' require 'sidekiq/testing/inline' Sidekiq::Testing.inline! end @@ -52,7 +53,7 @@ def self.bar(str) end before do - Sidekiq::Extensions.enable_delay! + Sidekiq::DelayExtensions.enable_delay! end it 'stubs the delay call on mailers' do diff --git a/test/test_util.rb b/test/test_util.rb deleted file mode 100644 index 9b1b9c2c..00000000 --- a/test/test_util.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/util' - -class TestUtil < Minitest::Test - class Helpers - include Sidekiq::Util - end - - def test_event_firing - before_handlers = Sidekiq.options[:lifecycle_events][:startup] - begin - Sidekiq.options[:lifecycle_events][:startup] = [proc { raise "boom" }] - h = Helpers.new - h.fire_event(:startup) - - Sidekiq.options[:lifecycle_events][:startup] = [proc { raise "boom" }] - assert_raises RuntimeError do - h.fire_event(:startup, reraise: true) - end - ensure - Sidekiq.options[:lifecycle_events][:startup] = before_handlers - end - end -end diff --git a/test/test_web.rb b/test/test_web.rb deleted file mode 100644 index 794b47f6..00000000 --- a/test/test_web.rb +++ /dev/null @@ -1,801 +0,0 @@ -# encoding: utf-8 -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/web' -require 'sidekiq/util' -require 'rack/test' - -describe Sidekiq::Web do - include Rack::Test::Methods - - def app - @app ||= Sidekiq::Web.new - end - - def job_params(job, score) - "#{score}-#{job['jid']}" - end - - before do - Sidekiq.redis {|c| c.flushdb } - app.middlewares.clear - end - - class WebWorker - include Sidekiq::Worker - - def perform(a, b) - a + b - end - end - - it 'can show text with any locales' do - rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'ru,en'} - get '/', {}, rackenv - assert_match(/Панель управления/, last_response.body) - rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'es,en'} - get '/', {}, rackenv - assert_match(/Panel de Control/, last_response.body) - rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'en-us'} - get '/', {}, rackenv - assert_match(/Dashboard/, last_response.body) - rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'zh-cn'} - get '/', {}, rackenv - assert_match(/信息板/, last_response.body) - rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'zh-tw'} - get '/', {}, rackenv - assert_match(/資訊主頁/, last_response.body) - rackenv = {'HTTP_ACCEPT_LANGUAGE' => 'nb'} - get '/', {}, rackenv - assert_match(/Oversikt/, last_response.body) - end - - it 'can provide a default, appropriate CSP for its content' do - get '/', {} - policies = last_response.headers["Content-Security-Policy"].split('; ') - assert_includes(policies, "connect-src 'self' https: http: wss: ws:") - assert_includes(policies, "style-src 'self' https: http: 'unsafe-inline'") - assert_includes(policies, "script-src 'self' https: http: 'unsafe-inline'") - assert_includes(policies, "object-src 'none'") - end - - describe 'busy' do - - it 'can display workers' do - Sidekiq.redis do |conn| - conn.incr('busy') - conn.sadd('processes', 'foo:1234') - conn.hmset('foo:1234', 'info', Sidekiq.dump_json('hostname' => 'foo', 'started_at' => Time.now.to_f, "queues" => [], 'concurrency' => 10), 'at', Time.now.to_f, 'busy', 4) - identity = 'foo:1234:workers' - hash = {:queue => 'critical', :payload => { 'class' => WebWorker.name, 'args' => [1,'abc'] }, :run_at => Time.now.to_i } - conn.hmset(identity, 1001, Sidekiq.dump_json(hash)) - end - assert_equal ['1001'], Sidekiq::Workers.new.map { |pid, tid, data| tid } - - get '/busy' - assert_equal 200, last_response.status - assert_match(/status-active/, last_response.body) - assert_match(/critical/, last_response.body) - assert_match(/WebWorker/, last_response.body) - end - - it 'can quiet a process' do - identity = 'identity' - signals_key = "#{identity}-signals" - - assert_nil Sidekiq.redis { |c| c.lpop signals_key } - post '/busy', 'quiet' => '1', 'identity' => identity - assert_equal 302, last_response.status - assert_equal 'TSTP', Sidekiq.redis { |c| c.lpop signals_key } - end - - it 'can stop a process' do - identity = 'identity' - signals_key = "#{identity}-signals" - - assert_nil Sidekiq.redis { |c| c.lpop signals_key } - post '/busy', 'stop' => '1', 'identity' => identity - assert_equal 302, last_response.status - assert_equal 'TERM', Sidekiq.redis { |c| c.lpop signals_key } - end - end - - it 'can display queues' do - assert Sidekiq::Client.push('queue' => :foo, 'class' => WebWorker, 'args' => [1, 3]) - - get '/queues' - assert_equal 200, last_response.status - assert_match(/foo/, last_response.body) - refute_match(/HardWorker/, last_response.body) - assert_match(/0.0/, last_response.body) - refute_match(/datetime/, last_response.body) - Sidekiq::Queue.new("foo").clear - - Time.stub(:now, Time.now - 65) do - assert Sidekiq::Client.push('queue' => :foo, 'class' => WebWorker, 'args' => [1, 3]) - end - - get '/queues' - assert_equal 200, last_response.status - assert_match(/foo/, last_response.body) - refute_match(/HardWorker/, last_response.body) - assert_match(/65.0/, last_response.body) - assert_match(/datetime/, last_response.body) - end - - it 'handles queue view' do - get '/queues/onmouseover=alert()' - assert_equal 404, last_response.status - - get '/queues/foo_bar:123-wow.' - assert_equal 200, last_response.status - assert_match(/foo_bar:123-wow\./, last_response.body) - - get '/queues/default' - assert_equal 200, last_response.status - end - - it 'can sort on enqueued_at column' do - Sidekiq.redis do |conn| - (1000..1005).each do |i| - conn.lpush('queue:default', Sidekiq.dump_json(args: [i], enqueued_at: Time.now.to_i + i)) - end - end - - get '/queues/default?count=3' # direction is 'desc' by default - assert_match(/1005/, last_response.body) - refute_match(/1002/, last_response.body) - - get '/queues/default?count=3&direction=asc' - assert_match(/1000/, last_response.body) - refute_match(/1003/, last_response.body) - end - - it 'can delete a queue' do - Sidekiq.redis do |conn| - conn.rpush('queue:foo', "{\"args\":[],\"enqueued_at\":1567894960}") - conn.sadd('queues', 'foo') - end - - get '/queues/foo' - assert_equal 200, last_response.status - - post '/queues/foo' - assert_equal 302, last_response.status - - Sidekiq.redis do |conn| - refute conn.smembers('queues').include?('foo') - refute conn.exists?('queue:foo') - end - end - - it 'can attempt to pause a queue' do - Sidekiq.stub(:pro?, true) do - mock = Minitest::Mock.new - mock.expect :pause!, true - - stub = lambda do |queue_name| - assert_equal 'foo', queue_name - mock - end - - Sidekiq::Queue.stub :new, stub do - post '/queues/foo', 'pause' => 'pause' - assert_equal 302, last_response.status - end - - assert_mock mock - end - end - - it 'can attempt to unpause a queue' do - Sidekiq.stub(:pro?, true) do - mock = Minitest::Mock.new - mock.expect :unpause!, true - - stub = lambda do |queue_name| - assert_equal 'foo', queue_name - mock - end - - Sidekiq::Queue.stub :new, stub do - post '/queues/foo', 'unpause' => 'unpause' - assert_equal 302, last_response.status - end - - assert_mock mock - end - end - - it 'ignores to attempt to pause a queue with pro disabled' do - mock = Minitest::Mock.new - mock.expect :clear, true - - stub = lambda do |queue_name| - assert_equal 'foo', queue_name - mock - end - - Sidekiq::Queue.stub :new, stub do - post '/queues/foo', 'pause' => 'pause' - assert_equal 302, last_response.status - end - - assert_mock mock - end - - it 'ignores to attempt to unpause a queue with pro disabled' do - mock = Minitest::Mock.new - mock.expect :clear, true - - stub = lambda do |queue_name| - assert_equal 'foo', queue_name - mock - end - - Sidekiq::Queue.stub :new, stub do - post '/queues/foo', 'unpause' => 'unpause' - assert_equal 302, last_response.status - end - - assert_mock mock - end - - it 'can delete a job' do - Sidekiq.redis do |conn| - conn.rpush('queue:foo', '{"args":[],"enqueued_at":1567894960}') - conn.rpush('queue:foo', '{"foo":"bar","args":[],"enqueued_at":1567894960}') - conn.rpush('queue:foo', '{"foo2":"bar2","args":[],"enqueued_at":1567894960}') - end - - get '/queues/foo' - assert_equal 200, last_response.status - - post '/queues/foo/delete', key_val: "{\"foo\":\"bar\"}" - assert_equal 302, last_response.status - - Sidekiq.redis do |conn| - refute conn.lrange('queue:foo', 0, -1).include?("{\"foo\":\"bar\"}") - end - end - - it 'can display retries' do - get '/retries' - assert_equal 200, last_response.status - assert_match(/found/, last_response.body) - refute_match(/HardWorker/, last_response.body) - - add_retry - - get '/retries' - assert_equal 200, last_response.status - refute_match(/found/, last_response.body) - assert_match(/HardWorker/, last_response.body) - end - - it 'can display a single retry' do - params = add_retry - get '/retries/0-shouldntexist' - assert_equal 302, last_response.status - get "/retries/#{job_params(*params)}" - assert_equal 200, last_response.status - assert_match(/HardWorker/, last_response.body) - end - - it 'handles missing retry' do - get "/retries/0-shouldntexist" - assert_equal 302, last_response.status - end - - it 'can delete a single retry' do - params = add_retry - post "/retries/#{job_params(*params)}", 'delete' => 'Delete' - assert_equal 302, last_response.status - assert_equal 'http://example.org/retries', last_response.header['Location'] - - get "/retries" - assert_equal 200, last_response.status - refute_match(/#{params.first['args'][2]}/, last_response.body) - end - - it 'can delete all retries' do - 3.times { add_retry } - - post "/retries/all/delete", 'delete' => 'Delete' - assert_equal 0, Sidekiq::RetrySet.new.size - assert_equal 302, last_response.status - assert_equal 'http://example.org/retries', last_response.header['Location'] - end - - it 'can retry a single retry now' do - params = add_retry - post "/retries/#{job_params(*params)}", 'retry' => 'Retry' - assert_equal 302, last_response.status - assert_equal 'http://example.org/retries', last_response.header['Location'] - - get '/queues/default' - assert_equal 200, last_response.status - assert_match(/#{params.first['args'][2]}/, last_response.body) - end - - it 'can kill a single retry now' do - params = add_retry - post "/retries/#{job_params(*params)}", 'kill' => 'Kill' - assert_equal 302, last_response.status - assert_equal 'http://example.org/retries', last_response.header['Location'] - - get '/morgue' - assert_equal 200, last_response.status - assert_match(/#{params.first['args'][2]}/, last_response.body) - end - - it 'can display scheduled' do - get '/scheduled' - assert_equal 200, last_response.status - assert_match(/found/, last_response.body) - refute_match(/HardWorker/, last_response.body) - - add_scheduled - - get '/scheduled' - assert_equal 200, last_response.status - refute_match(/found/, last_response.body) - assert_match(/HardWorker/, last_response.body) - end - - it 'can display a single scheduled job' do - params = add_scheduled - get '/scheduled/0-shouldntexist' - assert_equal 302, last_response.status - get "/scheduled/#{job_params(*params)}" - assert_equal 200, last_response.status - assert_match(/HardWorker/, last_response.body) - end - - it 'can display a single scheduled job tags' do - params = add_scheduled - get "/scheduled/#{job_params(*params)}" - assert_match(/tag1/, last_response.body) - assert_match(/tag2/, last_response.body) - end - - it 'handles missing scheduled job' do - get "/scheduled/0-shouldntexist" - assert_equal 302, last_response.status - end - - it 'can add to queue a single scheduled job' do - params = add_scheduled - post "/scheduled/#{job_params(*params)}", 'add_to_queue' => true - assert_equal 302, last_response.status - assert_equal 'http://example.org/scheduled', last_response.header['Location'] - - get '/queues/default' - assert_equal 200, last_response.status - assert_match(/#{params.first['args'][2]}/, last_response.body) - end - - it 'can delete a single scheduled job' do - params = add_scheduled - post "/scheduled/#{job_params(*params)}", 'delete' => 'Delete' - assert_equal 302, last_response.status - assert_equal 'http://example.org/scheduled', last_response.header['Location'] - - get "/scheduled" - assert_equal 200, last_response.status - refute_match(/#{params.first['args'][2]}/, last_response.body) - end - - it 'can delete scheduled' do - params = add_scheduled - Sidekiq.redis do |conn| - assert_equal 1, conn.zcard('schedule') - post '/scheduled', 'key' => [job_params(*params)], 'delete' => 'Delete' - assert_equal 302, last_response.status - assert_equal 'http://example.org/scheduled', last_response.header['Location'] - assert_equal 0, conn.zcard('schedule') - end - end - - it "can move scheduled to default queue" do - q = Sidekiq::Queue.new - params = add_scheduled - Sidekiq.redis do |conn| - assert_equal 1, conn.zcard('schedule') - assert_equal 0, q.size - post '/scheduled', 'key' => [job_params(*params)], 'add_to_queue' => 'AddToQueue' - assert_equal 302, last_response.status - assert_equal 'http://example.org/scheduled', last_response.header['Location'] - assert_equal 0, conn.zcard('schedule') - assert_equal 1, q.size - get '/queues/default' - assert_equal 200, last_response.status - assert_match(/#{params[0]['args'][2]}/, last_response.body) - end - end - - it 'can retry all retries' do - msg = add_retry.first - add_retry - - post "/retries/all/retry", 'retry' => 'Retry' - assert_equal 302, last_response.status - assert_equal 'http://example.org/retries', last_response.header['Location'] - assert_equal 2, Sidekiq::Queue.new("default").size - - get '/queues/default' - assert_equal 200, last_response.status - assert_match(/#{msg['args'][2]}/, last_response.body) - end - - it 'escape job args and error messages' do - # on /retries page - params = add_xss_retry - get '/retries' - assert_equal 200, last_response.status - assert_match(/FailWorker/, last_response.body) - - assert last_response.body.include?( "fail message: <a>hello</a>" ) - assert !last_response.body.include?( "fail message: hello" ) - - assert last_response.body.include?( "args\">"<a>hello</a>"<" ) - assert !last_response.body.include?( "args\">hello<" ) - - # on /workers page - Sidekiq.redis do |conn| - pro = 'foo:1234' - conn.sadd('processes', pro) - conn.hmset(pro, 'info', Sidekiq.dump_json('started_at' => Time.now.to_f, 'labels' => ['frumduz'], 'queues' =>[], 'concurrency' => 10), 'busy', 1, 'beat', Time.now.to_f) - identity = "#{pro}:workers" - hash = {:queue => 'critical', :payload => { 'class' => "FailWorker", 'args' => ["hello"] }, :run_at => Time.now.to_i } - conn.hmset(identity, 100001, Sidekiq.dump_json(hash)) - conn.incr('busy') - end - - get '/busy' - assert_equal 200, last_response.status - assert_match(/FailWorker/, last_response.body) - assert_match(/frumduz/, last_response.body) - assert last_response.body.include?( "<a>hello</a>" ) - assert !last_response.body.include?( "hello" ) - - # on /queues page - params = add_xss_retry # sorry, don't know how to easily make this show up on queues page otherwise. - post "/retries/#{job_params(*params)}", 'retry' => 'Retry' - assert_equal 302, last_response.status - - get '/queues/foo' - assert_equal 200, last_response.status - assert last_response.body.include?( "<a>hello</a>" ) - assert !last_response.body.include?( "hello" ) - end - - it 'can show user defined tab' do - begin - Sidekiq::Web.tabs['Custom Tab'] = '/custom' - - get '/' - assert_match 'Custom Tab', last_response.body - - ensure - Sidekiq::Web.tabs.delete 'Custom Tab' - end - end - - it 'can display home' do - get '/' - assert_equal 200, last_response.status - end - - describe 'custom locales' do - before do - Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "fixtures") - Sidekiq::Web.tabs['Custom Tab'] = '/custom' - Sidekiq::WebApplication.get('/custom') do - clear_caches # ugly hack since I can't figure out how to access WebHelpers outside of this context - t('translated_text') - end - end - - after do - Sidekiq::Web.tabs.delete 'Custom Tab' - Sidekiq::Web.settings.locales.pop - end - - it 'can show user defined tab with custom locales' do - get '/custom' - - assert_match(/Changed text/, last_response.body) - end - end - - describe 'dashboard/stats' do - it 'redirects to stats' do - get '/dashboard/stats' - assert_equal 302, last_response.status - assert_equal 'http://example.org/stats', last_response.header['Location'] - end - end - - describe 'stats' do - include Sidekiq::Util - - before do - Sidekiq.redis do |conn| - conn.set("stat:processed", 5) - conn.set("stat:failed", 2) - conn.sadd("queues", "default") - end - 2.times { add_retry } - 3.times { add_scheduled } - 4.times { add_worker } - end - - it 'works' do - get '/stats' - @response = Sidekiq.load_json(last_response.body) - - assert_equal 200, last_response.status - assert_includes @response.keys, "sidekiq" - assert_equal 5, @response["sidekiq"]["processed"] - assert_equal 2, @response["sidekiq"]["failed"] - assert_equal 4, @response["sidekiq"]["busy"] - assert_equal 1, @response["sidekiq"]["processes"] - assert_equal 2, @response["sidekiq"]["retries"] - assert_equal 3, @response["sidekiq"]["scheduled"] - assert_equal 0, @response["sidekiq"]["default_latency"] - assert_includes @response.keys, "redis" - assert_includes @response["redis"].keys, "redis_version" - assert_includes @response["redis"].keys, "uptime_in_days" - assert_includes @response["redis"].keys, "connected_clients" - assert_includes @response["redis"].keys, "used_memory_human" - assert_includes @response["redis"].keys, "used_memory_peak_human" - assert_includes @response.keys, "server_utc_time" - end - end - - describe 'bad JSON' do - it 'displays without error' do - s = Sidekiq::DeadSet.new - (_, score) = kill_bad - assert_equal 1, s.size - - get '/morgue' - assert_equal 200, last_response.status - assert_match(/#{score.to_i}/, last_response.body) - assert_match("something bad", last_response.body) - assert_equal 1, s.size - - post "/morgue/#{score}-", 'delete' => 'Delete' - assert_equal 302, last_response.status - assert_equal 1, s.size - end - end - - describe 'stats/queues' do - include Sidekiq::Util - - before do - Sidekiq.redis do |conn| - conn.set("stat:processed", 5) - conn.set("stat:failed", 2) - conn.sadd("queues", "default") - conn.sadd("queues", "queue2") - end - 2.times { add_retry } - 3.times { add_scheduled } - 4.times { add_worker } - - get '/stats/queues' - @response = Sidekiq.load_json(last_response.body) - end - - it 'reports the queue depth' do - assert_equal 0, @response["default"] - assert_equal 0, @response["queue2"] - end - end - - describe 'dead jobs' do - it 'shows empty index' do - get 'morgue' - assert_equal 200, last_response.status - end - - it 'shows index with jobs' do - (_, score) = add_dead - get 'morgue' - assert_equal 200, last_response.status - assert_match(/#{score}/, last_response.body) - end - - it 'can delete all dead' do - 3.times { add_dead } - - assert_equal 3, Sidekiq::DeadSet.new.size - post "/morgue/all/delete", 'delete' => 'Delete' - assert_equal 0, Sidekiq::DeadSet.new.size - assert_equal 302, last_response.status - assert_equal 'http://example.org/morgue', last_response.header['Location'] - end - - it 'can display a dead job' do - params = add_dead - get "/morgue/#{job_params(*params)}" - assert_equal 200, last_response.status - end - - it 'can retry a dead job' do - params = add_dead - post "/morgue/#{job_params(*params)}", 'retry' => 'Retry' - assert_equal 302, last_response.status - assert_equal 'http://example.org/morgue', last_response.header['Location'] - assert_equal 0, Sidekiq::DeadSet.new.size - - params = add_dead('jid-with-hyphen') - post "/morgue/#{job_params(*params)}", 'retry' => 'Retry' - assert_equal 302, last_response.status - assert_equal 0, Sidekiq::DeadSet.new.size - - get '/queues/foo' - assert_equal 200, last_response.status - assert_match(/#{params.first['args'][2]}/, last_response.body) - end - end - - def add_scheduled - score = Time.now.to_f - msg = { 'class' => 'HardWorker', - 'args' => ['bob', 1, Time.now.to_f], - 'jid' => SecureRandom.hex(12), - 'tags' => ['tag1', 'tag2'], } - Sidekiq.redis do |conn| - conn.zadd('schedule', score, Sidekiq.dump_json(msg)) - end - [msg, score] - end - - def add_retry - msg = { 'class' => 'HardWorker', - 'args' => ['bob', 1, Time.now.to_f], - 'queue' => 'default', - 'error_message' => 'Some fake message', - 'error_class' => 'RuntimeError', - 'retry_count' => 0, - 'failed_at' => Time.now.to_f, - 'jid' => SecureRandom.hex(12) } - score = Time.now.to_f - Sidekiq.redis do |conn| - conn.zadd('retry', score, Sidekiq.dump_json(msg)) - end - - [msg, score] - end - - def add_dead(jid = SecureRandom.hex(12)) - msg = { 'class' => 'HardWorker', - 'args' => ['bob', 1, Time.now.to_f], - 'queue' => 'foo', - 'error_message' => 'Some fake message', - 'error_class' => 'RuntimeError', - 'retry_count' => 0, - 'failed_at' => Time.now.utc, - 'jid' => jid } - score = Time.now.to_f - Sidekiq.redis do |conn| - conn.zadd('dead', score, Sidekiq.dump_json(msg)) - end - [msg, score] - end - - def kill_bad - job = "{ something bad }" - score = Time.now.to_f - Sidekiq.redis do |conn| - conn.zadd('dead', score, job) - end - [job, score] - end - - def add_xss_retry(job_id=SecureRandom.hex(12)) - msg = { 'class' => 'FailWorker', - 'args' => ['hello'], - 'queue' => 'foo', - 'error_message' => 'fail message: hello', - 'error_class' => 'RuntimeError', - 'retry_count' => 0, - 'failed_at' => Time.now.to_f, - 'jid' => SecureRandom.hex(12) } - score = Time.now.to_f - Sidekiq.redis do |conn| - conn.zadd('retry', score, Sidekiq.dump_json(msg)) - end - - [msg, score] - end - - def add_worker - key = "#{hostname}:#{$$}" - msg = "{\"queue\":\"default\",\"payload\":{\"retry\":true,\"queue\":\"default\",\"timeout\":20,\"backtrace\":5,\"class\":\"HardWorker\",\"args\":[\"bob\",10,5],\"jid\":\"2b5ad2b016f5e063a1c62872\"},\"run_at\":1361208995}" - Sidekiq.redis do |conn| - conn.multi do |transaction| - transaction.sadd("processes", key) - transaction.hmset(key, 'info', Sidekiq.dump_json('hostname' => 'foo', 'started_at' => Time.now.to_f, "queues" => []), 'at', Time.now.to_f, 'busy', 4) - transaction.hmset("#{key}:workers", Time.now.to_f, msg) - end - end - end - - describe 'basic auth' do - include Rack::Test::Methods - - def app - app = Sidekiq::Web.new - app.use(Rack::Auth::Basic) { |user, pass| user == "a" && pass == "b" } - app.use(Rack::Session::Cookie, secret: SecureRandom.hex(32)) - - app - end - - it 'requires basic authentication' do - get '/' - - assert_equal 401, last_response.status - refute_nil last_response.header["WWW-Authenticate"] - end - - it 'authenticates successfuly' do - basic_authorize 'a', 'b' - - get '/' - assert_equal 200, last_response.status - get '/?days=1000000' - assert_equal 401, last_response.status - end - end - - describe 'custom session' do - include Rack::Test::Methods - - def app - app = Sidekiq::Web.new - app.use Rack::Session::Cookie, secret: 'v3rys3cr31', host: 'nicehost.org' - app - end - - it 'requires uses session options' do - get '/' - - session_options = last_request.env['rack.session'].options - - assert_equal 'v3rys3cr31', session_options[:secret] - assert_equal 'nicehost.org', session_options[:host] - end - end - - describe "redirecting in before" do - include Rack::Test::Methods - - before do - Sidekiq::WebApplication.before { Thread.current[:some_setting] = :before } - Sidekiq::WebApplication.before { redirect '/' } - Sidekiq::WebApplication.after { Thread.current[:some_setting] = :after } - end - - after do - Sidekiq::WebApplication.remove_instance_variable(:@befores) - Sidekiq::WebApplication.remove_instance_variable(:@afters) - end - - def app - app = Sidekiq::Web.new - app.use Rack::Session::Cookie, secret: 'v3rys3cr31', host: 'nicehost.org' - app - end - - it "allows afters to run" do - get '/' - assert_equal :after, Thread.current[:some_setting] - end - end -end diff --git a/test/test_web_helpers.rb b/test/test_web_helpers.rb deleted file mode 100644 index cf939a50..00000000 --- a/test/test_web_helpers.rb +++ /dev/null @@ -1,127 +0,0 @@ -# frozen_string_literal: true -require_relative 'helper' -require 'sidekiq/web' - -class TestWebHelpers < Minitest::Test - class Helpers - include Sidekiq::WebHelpers - - def initialize(params={}) - @thehash = default.merge(params) - end - - def request - self - end - - def settings - self - end - - def locales - ['web/locales'] - end - - def env - @thehash - end - - def default - { - } - end - end - - def test_locale_determination - obj = Helpers.new - assert_equal 'en', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2') - assert_equal 'fr', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2') - assert_equal 'zh-cn', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'en-US,sv-SE;q=0.8,sv;q=0.6,en;q=0.4') - assert_equal 'en', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'nb-NO,nb;q=0.2') - assert_equal 'nb', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'en-us') - assert_equal 'en', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'sv-se') - assert_equal 'sv', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'pt-BR,pt;q=0.8,en-US;q=0.6,en;q=0.4') - assert_equal 'pt-br', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'pt-PT,pt;q=0.8,en-US;q=0.6,en;q=0.4') - assert_equal 'pt', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'pt-br') - assert_equal 'pt-br', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'pt-pt') - assert_equal 'pt', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'pt') - assert_equal 'pt', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'en-us; *') - assert_equal 'en', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.8') - assert_equal 'en', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'en-GB,en-US;q=0.8,en;q=0.6') - assert_equal 'en', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'ru,en') - assert_equal 'ru', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => 'lt') - assert_equal 'lt', obj.locale - - obj = Helpers.new('HTTP_ACCEPT_LANGUAGE' => '*') - assert_equal 'en', obj.locale - end - - def test_available_locales - obj = Helpers.new - expected = %w( - ar cs da de el en es fa fr he hi it ja - ko lt nb nl pl pt pt-br ru sv ta uk ur - vi zh-cn zh-tw - ) - assert_equal expected, obj.available_locales.sort - end - - def test_display_illegal_args - o = Helpers.new - s = o.display_args([1,2,3]) - assert_equal "1, 2, 3", s - s = o.display_args(["", 12]) - assert_equal ""<html>", 12", s - s = o.display_args("") - assert_equal "Invalid job payload, args must be an Array, not String", s - s = o.display_args(nil) - assert_equal "Invalid job payload, args is nil", s - end - - def test_to_query_string_escapes_bad_query_input - obj = Helpers.new - assert_equal "page=B%3CH", obj.to_query_string("page" => "B "H>B" } - end - end - assert_equal "direction=H%3EB&page=B%3CH", obj.qparams("page" => "B 12 - end - - def setup - Sidekiq.redis {|c| c.flushdb } - end - - it "provides basic ActiveJob compatibilility" do - q = Sidekiq::ScheduledSet.new - assert_equal 0, q.size - jid = SetWorker.set(wait_until: 1.hour.from_now).perform_async(123) - assert jid - assert_equal 1, q.size - jid = SetWorker.set(wait: 1.hour).perform_async(123) - assert jid - assert_equal 2, q.size - - q = Sidekiq::Queue.new("foo") - assert_equal 0, q.size - SetWorker.perform_async - assert_equal 1, q.size - - SetWorker.set(queue: 'xyz').perform_async - assert_equal 1, Sidekiq::Queue.new("xyz").size - end - - it 'can be memoized' do - q = Sidekiq::Queue.new('bar') - assert_equal 0, q.size - set = SetWorker.set(queue: :bar, foo: 'qaaz') - set.perform_async(1) - set.perform_async(1) - set.perform_async(1) - set.perform_async(1) - assert_equal 4, q.size - assert_equal 4, q.map{|j| j['jid'] }.uniq.size - set.perform_in(10, 1) - end - - it 'allows option overrides' do - q = Sidekiq::Queue.new('bar') - assert_equal 0, q.size - assert SetWorker.set(queue: :bar).perform_async(1) - job = q.first - assert_equal 'bar', job['queue'] - assert_equal 12, job['retry'] - end - - it 'handles symbols and strings' do - q = Sidekiq::Queue.new('bar') - assert_equal 0, q.size - assert SetWorker.set('queue' => 'bar', :retry => 11).perform_async(1) - job = q.first - assert_equal 'bar', job['queue'] - assert_equal 11, job['retry'] - - q.clear - assert SetWorker.perform_async(1) - assert_equal 0, q.size - - q = Sidekiq::Queue.new('foo') - job = q.first - assert_equal 'foo', job['queue'] - assert_equal 12, job['retry'] - end - - it 'allows multiple calls' do - SetWorker.set(queue: :foo).set(bar: 'xyz').perform_async - - q = Sidekiq::Queue.new('foo') - job = q.first - assert_equal 'foo', job['queue'] - assert_equal 'xyz', job['bar'] - end - - it 'works with .perform_bulk' do - q = Sidekiq::Queue.new('bar') - assert_equal 0, q.size - - set = SetWorker.set(queue: 'bar') - jids = set.perform_bulk((1..1_001).to_a.map { |x| Array(x) }) - - assert_equal 1_001, q.size - assert_equal 1_001, jids.size - end - - describe '.perform_bulk and lazy enumerators' do - it 'evaluates lazy enumerators' do - q = Sidekiq::Queue.new('bar') - assert_equal 0, q.size - - set = SetWorker.set('queue' => 'bar') - lazy_args = (1..1_001).to_a.map { |x| Array(x) }.lazy - jids = set.perform_bulk(lazy_args) - - assert_equal 1_001, q.size - assert_equal 1_001, jids.size - end - end - end - - describe '#perform_inline' do - $my_recorder = [] - - class MyCustomWorker - include Sidekiq::Worker - - def perform(recorder) - $my_recorder << ['work_performed'] - end - end - - class MyCustomMiddleware - def initialize(name, recorder) - @name = name - @recorder = recorder - end - - def call(*args) - @recorder << "#{@name}-before" - response = yield - @recorder << "#{@name}-after" - return response - end - end - - it 'executes middleware & runs job inline' do - server_chain = Sidekiq::Middleware::Chain.new - server_chain.add MyCustomMiddleware, "1-server", $my_recorder - client_chain = Sidekiq::Middleware::Chain.new - client_chain.add MyCustomMiddleware, "1-client", $my_recorder - Sidekiq.stub(:server_middleware, server_chain) do - Sidekiq.stub(:client_middleware, client_chain) do - MyCustomWorker.perform_inline($my_recorder) - assert_equal $my_recorder.flatten, %w(1-client-before 1-client-after 1-server-before work_performed 1-server-after) - end - end - end - end -end diff --git a/web/assets/images/apple-touch-icon.png b/web/assets/images/apple-touch-icon.png deleted file mode 100644 index 588ac508..00000000 Binary files a/web/assets/images/apple-touch-icon.png and /dev/null differ diff --git a/web/assets/images/favicon.ico b/web/assets/images/favicon.ico deleted file mode 100644 index f515e0fd..00000000 Binary files a/web/assets/images/favicon.ico and /dev/null differ diff --git a/web/assets/images/logo.png b/web/assets/images/logo.png deleted file mode 100755 index b06e15ce..00000000 Binary files a/web/assets/images/logo.png and /dev/null differ diff --git a/web/assets/images/status.png b/web/assets/images/status.png deleted file mode 100755 index 2f55920f..00000000 Binary files a/web/assets/images/status.png and /dev/null differ diff --git a/web/assets/javascripts/application.js b/web/assets/javascripts/application.js deleted file mode 100644 index a8e1d6c6..00000000 --- a/web/assets/javascripts/application.js +++ /dev/null @@ -1,111 +0,0 @@ -/* timeago.js - https://github.com/hustcc/timeago.js */ -!function(e,t){"object"==typeof module&&module.exports?module.exports=t(e):e.timeago=t(e)}("undefined"!=typeof window?window:this,function(){function e(e){return e instanceof Date?e:isNaN(e)?/^\d+$/.test(e)?new Date(t(e,10)):(e=(e||"").trim().replace(/\.\d+/,"").replace(/-/,"/").replace(/-/,"/").replace(/T/," ").replace(/Z/," UTC").replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"),new Date(e)):new Date(t(e))}function t(e){return parseInt(e)}function n(e,n,r){n=d[n]?n:d[r]?r:"en";var i=0;for(agoin=e<0?1:0,e=Math.abs(e);e>=l[i]&&i(0===i?9:1)&&(i+=1),d[n](e,i)[agoin].replace("%s",e)}function r(t,n){return n=n?e(n):new Date,(n-e(t))/1e3}function i(e){for(var t=1,n=0,r=e;e>=l[n]&&n1&&(n+="s"),[e+" "+n+" ago","in "+e+" "+n]},zh_CN:function(e,t){if(0===t)return["刚刚","片刻后"];var n=s[parseInt(t/2)];return[e+n+"前",e+n+"后"]}},l=[60,60,24,7,365/7/12,12],p=6,_="datetime";return a.register=function(e,t){d[e]=t},a}); -!function(s){function n(a){if(e[a])return e[a].exports;var r=e[a]={exports:{},id:a,loaded:!1};return s[a].call(r.exports,r,r.exports,n),r.loaded=!0,r.exports}var e={};return n.m=s,n.c=e,n.p="",n(0)}([function(s,n,e){for(var a=e(1),r=null,t=a.length-1;t>=0;t--)r=a[t],"en"!=r&&"zh_CN"!=r&&timeago.register(r,e(2)("./"+r))},function(s,n){s.exports=["ar","be","bg","ca","da","de","el","en","en_short","es","eu","fr","hu","in_BG","in_HI","in_ID","it","ja","ko","ml","nb_NO","nl","nn_NO","pl","pt_BR","ru","sv","ta","th","uk","vi","zh_CN","zh_TW"]},function(s,n,e){function a(s){return e(r(s))}function r(s){return t[s]||function(){throw new Error("Cannot find module '"+s+"'.")}()}var t={"./ar":3,"./ar.js":3,"./be":4,"./be.js":4,"./bg":5,"./bg.js":5,"./ca":6,"./ca.js":6,"./da":7,"./da.js":7,"./de":8,"./de.js":8,"./el":9,"./el.js":9,"./en":10,"./en.js":10,"./en_short":11,"./en_short.js":11,"./es":12,"./es.js":12,"./eu":13,"./eu.js":13,"./fr":14,"./fr.js":14,"./hu":15,"./hu.js":15,"./in_BG":16,"./in_BG.js":16,"./in_HI":17,"./in_HI.js":17,"./in_ID":18,"./in_ID.js":18,"./it":19,"./it.js":19,"./ja":20,"./ja.js":20,"./ko":21,"./ko.js":21,"./locales":1,"./locales.js":1,"./ml":22,"./ml.js":22,"./nb_NO":23,"./nb_NO.js":23,"./nl":24,"./nl.js":24,"./nn_NO":25,"./nn_NO.js":25,"./pl":26,"./pl.js":26,"./pt_BR":27,"./pt_BR.js":27,"./ru":28,"./ru.js":28,"./sv":29,"./sv.js":29,"./ta":30,"./ta.js":30,"./th":31,"./th.js":31,"./uk":32,"./uk.js":32,"./vi":33,"./vi.js":33,"./zh_CN":34,"./zh_CN.js":34,"./zh_TW":35,"./zh_TW.js":35};a.keys=function(){return Object.keys(t)},a.resolve=r,s.exports=a,a.id=2},function(s,n){function e(s,n){return 1===n?a[s][0]:2==n?a[s][1]:n>=3&&n<=10?a[s][2]:a[s][3]}s.exports=function(s,n){if(0===n)return["منذ لحظات","بعد لحظات"];var a;switch(n){case 1:a=0;break;case 2:case 3:a=1;break;case 4:case 5:a=2;break;case 6:case 7:a=3;break;case 8:case 9:a=4;break;case 10:case 11:a=5;break;case 12:case 13:a=6}var r=e(a,s);return["منذ "+r,"بعد "+r]};var a=[["ثانية","ثانيتين","%s ثوان","%s ثانية"],["دقيقة","دقيقتين","%s دقائق","%s دقيقة"],["ساعة","ساعتين","%s ساعات","%s ساعة"],["يوم","يومين","%s أيام","%s يوماً"],["أسبوع","أسبوعين","%s أسابيع","%s أسبوعاً"],["شهر","شهرين","%s أشهر","%s شهراً"],["عام","عامين","%s أعوام","%s عاماً"]]},function(s,n){function e(s,n,e,a,r){var t=r%10,u=a;return 1===r?u=s:1===t&&r>20?u=n:t>1&&t<5&&(r>20||r<10)&&(u=e),u}var a=e.bind(null,"секунду","%s секунду","%s секунды","%s секунд"),r=e.bind(null,"хвіліну","%s хвіліну","%s хвіліны","%s хвілін"),t=e.bind(null,"гадзіну","%s гадзіну","%s гадзіны","%s гадзін"),u=e.bind(null,"дзень","%s дзень","%s дні","%s дзён"),i=e.bind(null,"тыдзень","%s тыдзень","%s тыдні","%s тыдняў"),o=e.bind(null,"месяц","%s месяц","%s месяцы","%s месяцаў"),d=e.bind(null,"год","%s год","%s гады","%s гадоў");s.exports=function(s,n){switch(n){case 0:return["толькі што","праз некалькі секунд"];case 1:return[a(s)+" таму","праз "+a(s)];case 2:case 3:return[r(s)+" таму","праз "+r(s)];case 4:case 5:return[t(s)+" таму","праз "+t(s)];case 6:case 7:return[u(s)+" таму","праз "+u(s)];case 8:case 9:return[i(s)+" таму","праз "+i(s)];case 10:case 11:return[o(s)+" таму","праз "+o(s)];case 12:case 13:return[d(s)+" таму","праз "+d(s)];default:return["",""]}}},function(s,n){s.exports=function(s,n){return[["току що","съвсем скоро"],["преди %s секунди","след %s секунди"],["преди 1 минута","след 1 минута"],["преди %s минути","след %s минути"],["преди 1 час","след 1 час"],["преди %s часа","след %s часа"],["преди 1 ден","след 1 ден"],["преди %s дни","след %s дни"],["преди 1 седмица","след 1 седмица"],["преди %s седмици","след %s седмици"],["преди 1 месец","след 1 месец"],["преди %s месеца","след %s месеца"],["преди 1 година","след 1 година"],["преди %s години","след %s години"]][n]}},function(s,n){s.exports=function(s,n){return[["fa un moment","d'aquí un moment"],["fa %s segons","d'aquí %s segons"],["fa 1 minut","d'aquí 1 minut"],["fa %s minuts","d'aquí %s minuts"],["fa 1 hora","d'aquí 1 hora"],["fa %s hores","d'aquí %s hores"],["fa 1 dia","d'aquí 1 dia"],["fa %s dies","d'aquí %s dies"],["fa 1 setmana","d'aquí 1 setmana"],["fa %s setmanes","d'aquí %s setmanes"],["fa 1 mes","d'aquí 1 mes"],["fa %s mesos","d'aquí %s mesos"],["fa 1 any","d'aquí 1 any"],["fa %s anys","d'aquí %s anys"]][n]}},function(s,n){s.exports=function(s,n){return[["for et øjeblik siden","om et øjeblik"],["for %s sekunder siden","om %s sekunder"],["for 1 minut siden","om 1 minut"],["for %s minutter siden","om %s minutter"],["for 1 time siden","om 1 time"],["for %s timer siden","om %s timer"],["for 1 dag siden","om 1 dag"],["for %s dage siden","om %s dage"],["for 1 uge siden","om 1 uge"],["for %s uger siden","om %s uger"],["for 1 måned siden","om 1 måned"],["for %s måneder siden","om %s måneder"],["for 1 år siden","om 1 år"],["for %s år siden","om %s år"]][n]}},function(s,n){s.exports=function(s,n){return[["gerade eben","vor einer Weile"],["vor %s Sekunden","in %s Sekunden"],["vor 1 Minute","in 1 Minute"],["vor %s Minuten","in %s Minuten"],["vor 1 Stunde","in 1 Stunde"],["vor %s Stunden","in %s Stunden"],["vor 1 Tag","in 1 Tag"],["vor %s Tagen","in %s Tagen"],["vor 1 Woche","in 1 Woche"],["vor %s Wochen","in %s Wochen"],["vor 1 Monat","in 1 Monat"],["vor %s Monaten","in %s Monaten"],["vor 1 Jahr","in 1 Jahr"],["vor %s Jahren","in %s Jahren"]][n]}},function(s,n){s.exports=function(s,n){return[["μόλις τώρα","σε λίγο"],["%s δευτερόλεπτα πριν","σε %s δευτερόλεπτα"],["1 λεπτό πριν","σε 1 λεπτό"],["%s λεπτά πριν","σε %s λεπτά"],["1 ώρα πριν","σε 1 ώρα"],["%s ώρες πριν","σε %s ώρες"],["1 μέρα πριν","σε 1 μέρα"],["%s μέρες πριν","σε %s μέρες"],["1 εβδομάδα πριν","σε 1 εβδομάδα"],["%s εβδομάδες πριν","σε %s εβδομάδες"],["1 μήνα πριν","σε 1 μήνα"],["%s μήνες πριν","σε %s μήνες"],["1 χρόνο πριν","σε 1 χρόνο"],["%s χρόνια πριν","σε %s χρόνια"]][n]}},function(s,n){s.exports=function(s,n){return[["just now","a while"],["%s seconds ago","in %s seconds"],["1 minute ago","in 1 minute"],["%s minutes ago","in %s minutes"],["1 hour ago","in 1 hour"],["%s hours ago","in %s hours"],["1 day ago","in 1 day"],["%s days ago","in %s days"],["1 week ago","in 1 week"],["%s weeks ago","in %s weeks"],["1 month ago","in 1 month"],["%s months ago","in %s months"],["1 year ago","in 1 year"],["%s years ago","in %s years"]][n]}},function(s,n){s.exports=function(s,n){return[["just now","a while"],["%ss ago","in %ss"],["1m ago","in 1m"],["%sm ago","in %sm"],["1h ago","in 1h"],["%sh ago","in %sh"],["1d ago","in 1d"],["%sd ago","in %sd"],["1w ago","in 1w"],["%sw ago","in %sw"],["1mo ago","in 1mo"],["%smo ago","in %smo"],["1yr ago","in 1yr"],["%syr ago","in %syr"]][n]}},function(s,n){s.exports=function(s,n){return[["justo ahora","en un rato"],["hace %s segundos","en %s segundos"],["hace 1 minuto","en 1 minuto"],["hace %s minutos","en %s minutos"],["hace 1 hora","en 1 hora"],["hace %s horas","en %s horas"],["hace 1 día","en 1 día"],["hace %s días","en %s días"],["hace 1 semana","en 1 semana"],["hace %s semanas","en %s semanas"],["hace 1 mes","en 1 mes"],["hace %s meses","en %s meses"],["hace 1 año","en 1 año"],["hace %s años","en %s años"]][n]}},function(s,n){s.exports=function(s,n){return[["orain","denbora bat barru"],["duela %s segundu","%s segundu barru"],["duela minutu 1","minutu 1 barru"],["duela %s minutu","%s minutu barru"],["duela ordu 1","ordu 1 barru"],["duela %s ordu","%s ordu barru"],["duela egun 1","egun 1 barru"],["duela %s egun","%s egun barru"],["duela aste 1","aste 1 barru"],["duela %s aste","%s aste barru"],["duela hillabete 1","hillabete 1 barru"],["duela %s hillabete","%s hillabete barru"],["duela urte 1","urte 1 barru"],["duela %s urte","%s urte barru"]][n]}},function(s,n){s.exports=function(s,n){return[["à l'instant","dans un instant"],["il y a %s secondes","dans %s secondes"],["il y a 1 minute","dans 1 minute"],["il y a %s minutes","dans %s minutes"],["il y a 1 heure","dans 1 heure"],["il y a %s heures","dans %s heures"],["il y a 1 jour","dans 1 jour"],["il y a %s jours","dans %s jours"],["il y a 1 semaine","dans 1 semaine"],["il y a %s semaines","dans %s semaines"],["il y a 1 mois","dans 1 mois"],["il y a %s mois","dans %s mois"],["il y a 1 an","dans 1 an"],["il y a %s ans","dans %s ans"]][n]}},function(s,n){s.exports=function(s,n){return[["éppen most","éppen most"],["%s másodperce","%s másodpercen belül"],["1 perce","1 percen belül"],["%s perce","%s percen belül"],["1 órája","1 órán belül"],["%s órája","%s órán belül"],["1 napja","1 napon belül"],["%s napja","%s napon belül"],["1 hete","1 héten belül"],["%s hete","%s héten belül"],["1 hónapja","1 hónapon belül"],["%s hónapja","%s hónapon belül"],["1 éve","1 éven belül"],["%s éve","%s éven belül"]][n]}},function(s,n){s.exports=function(s,n){return[["এইমাত্র","একটা সময়"],["%s সেকেন্ড আগে","%s এর সেকেন্ডের মধ্যে"],["1 মিনিট আগে","1 মিনিটে"],["%s এর মিনিট আগে","%s এর মিনিটের মধ্যে"],["1 ঘন্টা আগে","1 ঘন্টা"],["%s ঘণ্টা আগে","%s এর ঘন্টার মধ্যে"],["1 দিন আগে","1 দিনের মধ্যে"],["%s এর দিন আগে","%s এর দিন"],["1 সপ্তাহ আগে","1 সপ্তাহের মধ্যে"],["%s এর সপ্তাহ আগে","%s সপ্তাহের মধ্যে"],["1 মাস আগে","1 মাসে"],["%s মাস আগে","%s মাসে"],["1 বছর আগে","1 বছরের মধ্যে"],["%s বছর আগে","%s বছরে"]][n]}},function(s,n){s.exports=function(s,n){return[["अभी","कुछ समय"],["%s सेकंड पहले","%s सेकंड में"],["1 मिनट पहले","1 मिनट में"],["%s मिनट पहले","%s मिनट में"],["1 घंटे पहले","1 घंटे में"],["%s घंटे पहले","%s घंटे में"],["1 दिन पहले","1 दिन में"],["%s दिन पहले","%s दिनों में"],["1 सप्ताह पहले","1 सप्ताह में"],["%s हफ्ते पहले","%s हफ्तों में"],["1 महीने पहले","1 महीने में"],["%s महीने पहले","%s महीनों में"],["1 साल पहले","1 साल में"],["%s साल पहले","%s साल में"]][n]}},function(s,n){s.exports=function(s,n){return[["baru saja","sebentar"],["%s detik yang lalu","dalam %s detik"],["1 menit yang lalu","dalam 1 menit"],["%s menit yang lalu","dalam %s menit"],["1 jam yang lalu","dalam 1 jam"],["%s jam yang lalu","dalam %s jam"],["1 hari yang lalu","dalam 1 hari"],["%s hari yang lalu","dalam %s hari"],["1 minggu yang lalu","dalam 1 minggu"],["%s minggu yang lalu","dalam %s minggu"],["1 bulan yang lalu","dalam 1 bulan"],["%s bulan yang lalu","dalam %s bulan"],["1 tahun yang lalu","dalam 1 tahun"],["%s tahun yang lalu","dalam %s tahun"]][n]}},function(s,n){s.exports=function(s,n){return[["poco fa","tra poco"],["%s secondi fa","%s secondi da ora"],["un minuto fa","un minuto da ora"],["%s minuti fa","%s minuti da ora"],["un'ora fa","un'ora da ora"],["%s ore fa","%s ore da ora"],["un giorno fa","un giorno da ora"],["%s giorni fa","%s giorni da ora"],["una settimana fa","una settimana da ora"],["%s settimane fa","%s settimane da ora"],["un mese fa","un mese da ora"],["%s mesi fa","%s mesi da ora"],["un anno fa","un anno da ora"],["%s anni fa","%s anni da ora"]][n]}},function(s,n){s.exports=function(s,n){return[["すこし前","すぐに"],["%s秒前","%s秒以内"],["1分前","1分以内"],["%s分前","%s分以内"],["1時間前","1時間以内"],["%s時間前","%s時間以内"],["1日前","1日以内"],["%s日前","%s日以内"],["1週間前","1週間以内"],["%s週間前","%s週間以内"],["1ヶ月前","1ヶ月以内"],["%sヶ月前","%sヶ月以内"],["1年前","1年以内"],["%s年前","%s年以内"]][n]}},function(s,n){s.exports=function(s,n){return[["방금","곧"],["%s초 전","%s초 후"],["1분 전","1분 후"],["%s분 전","%s분 후"],["1시간 전","1시간 후"],["%s시간 전","%s시간 후"],["1일 전","1일 후"],["%s일 전","%s일 후"],["1주일 전","1주일 후"],["%s주일 전","%s주일 후"],["1개월 전","1개월 후"],["%s개월 전","%s개월 후"],["1년 전","1년 후"],["%s년 전","%s년 후"]][n]}},function(s,n){s.exports=function(s,n){return[["ഇപ്പോള്‍","കുറച്ചു മുന്‍പ്"],["%s സെക്കന്റ്‌കള്‍ക്ക് മുന്‍പ്","%s സെക്കന്റില്‍"],["1 മിനിറ്റിനു മുന്‍പ്","1 മിനിറ്റില്‍"],["%s മിനിറ്റുകള്‍ക്ക് മുന്‍പ","%s മിനിറ്റില്‍"],["1 മണിക്കൂറിനു മുന്‍പ്","1 മണിക്കൂറില്‍"],["%s മണിക്കൂറുകള്‍ക്കു മുന്‍പ്","%s മണിക്കൂറില്‍"],["1 ഒരു ദിവസം മുന്‍പ്","1 ദിവസത്തില്‍"],["%s ദിവസങ്ങള്‍ക് മുന്‍പ്","%s ദിവസങ്ങള്‍ക്കുള്ളില്‍"],["1 ആഴ്ച മുന്‍പ്","1 ആഴ്ചയില്‍"],["%s ആഴ്ചകള്‍ക്ക് മുന്‍പ്","%s ആഴ്ചകള്‍ക്കുള്ളില്‍"],["1 മാസത്തിനു മുന്‍പ്","1 മാസത്തിനുള്ളില്‍"],["%s മാസങ്ങള്‍ക്ക് മുന്‍പ്","%s മാസങ്ങള്‍ക്കുള്ളില്‍"],["1 വര്‍ഷത്തിനു മുന്‍പ്","1 വര്‍ഷത്തിനുള്ളില്‍"],["%s വര്‍ഷങ്ങള്‍ക്കു മുന്‍പ്","%s വര്‍ഷങ്ങള്‍ക്കുല്ല്ളില്‍"]][n]}},function(s,n){s.exports=function(s,n){return[["akkurat nå","om litt"],["%s sekunder siden","om %s sekunder"],["1 minutt siden","om 1 minutt"],["%s minutter siden","om %s minutter"],["1 time siden","om 1 time"],["%s timer siden","om %s timer"],["1 dag siden","om 1 dag"],["%s dager siden","om %s dager"],["1 uke siden","om 1 uke"],["%s uker siden","om %s uker"],["1 måned siden","om 1 måned"],["%s måneder siden","om %s måneder"],["1 år siden","om 1 år"],["%s år siden","om %s år"]][n]}},function(s,n){s.exports=function(s,n){return[["recent","binnenkort"],["%s seconden geleden","binnen %s seconden"],["1 minuut geleden","binnen 1 minuut"],["%s minuten geleden","binnen %s minuten"],["1 uur geleden","binnen 1 uur"],["%s uren geleden","binnen %s uren"],["1 dag geleden","binnen 1 dag"],["%s dagen geleden","binnen %s dagen"],["1 week geleden","binnen 1 week"],["%s weken geleden","binnen %s weken"],["1 maand geleden","binnen 1 maand"],["%s maanden geleden","binnen %s maanden"],["1 jaar geleden","binnen 1 jaar"],["%s jaren geleden","binnen %s jaren"]][n]}},function(s,n){s.exports=function(s,n){return[["nett no","om litt"],["%s sekund sidan","om %s sekund"],["1 minutt sidan","om 1 minutt"],["%s minutt sidan","om %s minutt"],["1 time sidan","om 1 time"],["%s timar sidan","om %s timar"],["1 dag sidan","om 1 dag"],["%s dagar sidan","om %s dagar"],["1 veke sidan","om 1 veke"],["%s veker sidan","om %s veker"],["1 månad sidan","om 1 månad"],["%s månadar sidan","om %s månadar"],["1 år sidan","om 1 år"],["%s år sidan","om %s år"]][n]}},function(s,n){s.exports=function(s,n){var e=[["w tej chwili","za chwilę"],["%s sekund temu","za %s sekund"],["1 minutę temu","za 1 minutę"],["%s minut temu","za %s minut"],["1 godzina temu","za 1 godzinę"],["%s godzin temu","za %s godzin"],["1 dzień temu","za 1 dzień"],["%s dni temu","za %s dni"],["1 tydzień temu","za 1 tydzień"],["%s tygodni temu","za %s tygodni"],["1 miesiąc temu","za 1 miesiąc"],["%s miesiące temu","za %s miesiące"],["1 rok temu","za 1 rok"],["%s lata temu","za %s lata"]],a=s.toString();return 1==n&&(2==a.length&&"1"==a[0]&&"0"!=a[1]||[2,3,4].indexOf(s%10)!=-1||[2,3,4].indexOf(s)!=-1)?["%s sekundy temu","za %s sekundy"]:3!=n||[2,3,4].indexOf(s%10)==-1&&[2,3,4].indexOf(s)==-1?5!=n||[2,3,4].indexOf(s%10)==-1&&[2,3,4].indexOf(s)==-1?9==n&&[2,3,4].indexOf(s)!=-1?["%s tygodnie temu","za %s tygodnie"]:11==n&&(s%10==0||2==a.length&&"1"==a[0]||[1,5,6,7,8,9].indexOf(s%10)!=-1)?["%s miesięcy temu","za %s miesięcy"]:13==n&&(s%10==0||2==a.length&&"1"==a[0]||[1,5,6,7,8,9].indexOf(s%10)!=-1)?["%s lat temu","za %s lat"]:e[n]:["%s godziny temu","za %s godziny"]:["%s minuty temu","za %s minuty"]}},function(s,n){s.exports=function(s,n){return[["agora mesmo","daqui um pouco"],["há %s segundos","em %s segundos"],["há um minuto","em um minuto"],["há %s minutos","em %s minutos"],["há uma hora","em uma hora"],["há %s horas","em %s horas"],["há um dia","em um dia"],["há %s dias","em %s dias"],["há uma semana","em uma semana"],["há %s semanas","em %s semanas"],["há um mês","em um mês"],["há %s meses","em %s meses"],["há um ano","em um ano"],["há %s anos","em %s anos"]][n]}},function(s,n){function e(s,n,e,a,r){var t=r%10,u=a;return 1===r?u=s:1===t&&r>20?u=n:t>1&&t<5&&(r>20||r<10)&&(u=e),u}var a=e.bind(null,"секунду","%s секунду","%s секунды","%s секунд"),r=e.bind(null,"минуту","%s минуту","%s минуты","%s минут"),t=e.bind(null,"час","%s час","%s часа","%s часов"),u=e.bind(null,"день","%s день","%s дня","%s дней"),i=e.bind(null,"неделю","%s неделю","%s недели","%s недель"),o=e.bind(null,"месяц","%s месяц","%s месяца","%s месяцев"),d=e.bind(null,"год","%s год","%s года","%s лет");s.exports=function(s,n){switch(n){case 0:return["только что","через несколько секунд"];case 1:return[a(s)+" назад","через "+a(s)];case 2:case 3:return[r(s)+" назад","через "+r(s)];case 4:case 5:return[t(s)+" назад","через "+t(s)];case 6:return["вчера","завтра"];case 7:return[u(s)+" назад","через "+u(s)];case 8:case 9:return[i(s)+" назад","через "+i(s)];case 10:case 11:return[o(s)+" назад","через "+o(s)];case 12:case 13:return[d(s)+" назад","через "+d(s)];default:return["",""]}}},function(s,n){s.exports=function(s,n){return[["just nu","om en stund"],["%s sekunder sedan","om %s seconder"],["1 minut sedan","om 1 minut"],["%s minuter sedan","om %s minuter"],["1 timme sedan","om 1 timme"],["%s timmar sedan","om %s timmar"],["1 dag sedan","om 1 day"],["%s dagar sedan","om %s days"],["1 vecka sedan","om 1 vecka"],["%s veckor sedan","om %s veckor"],["1 månad sedan","om 1 månad"],["%s månader sedan","om %s månader"],["1 år sedan","om 1 år"],["%s år sedan","om %s år"]][n]}},function(s,n){s.exports=function(s,n){return[["இப்போது","சற்று நேரம் முன்பு"],["%s நொடிக்கு முன்","%s நொடிகளில்"],["1 நிமிடத்திற்க்கு முன்","1 நிமிடத்தில்"],["%s நிமிடத்திற்க்கு முன்","%s நிமிடங்களில்"],["1 மணி நேரத்திற்கு முன்","1 மணி நேரத்திற்குள்"],["%s மணி நேரத்திற்கு முன்","%s மணி நேரத்திற்குள்"],["1 நாளுக்கு முன்","1 நாளில்"],["%s நாட்களுக்கு முன்","%s நாட்களில்"],["1 வாரத்திற்கு முன்","1 வாரத்தில்"],["%s வாரங்களுக்கு முன்","%s வாரங்களில்"],["1 மாதத்திற்கு முன்","1 மாதத்தில்"],["%s மாதங்களுக்கு முன்","%s மாதங்களில்"],["1 வருடத்திற்கு முன்","1 வருடத்தில்"],["%s வருடங்களுக்கு முன்","%s வருடங்களில்"]][n]}},function(s,n){s.exports=function(s,n){return[["เมื่อสักครู่นี้","อีกสักครู่"],["%s วินาทีที่แล้ว","ใน %s วินาที"],["1 นาทีที่แล้ว","ใน 1 นาที"],["%s นาทีที่แล้ว","ใน %s นาที"],["1 ชั่วโมงที่แล้ว","ใน 1 ชั่วโมง"],["%s ชั่วโมงที่แล้ว","ใน %s ชั่วโมง"],["1 วันที่แล้ว","ใน 1 วัน"],["%s วันที่แล้ว","ใน %s วัน"],["1 อาทิตย์ที่แล้ว","ใน 1 อาทิตย์"],["%s อาทิตย์ที่แล้ว","ใน %s อาทิตย์"],["1 เดือนที่แล้ว","ใน 1 เดือน"],["%s เดือนที่แล้ว","ใน %s เดือน"],["1 ปีที่แล้ว","ใน 1 ปี"],["%s ปีที่แล้ว","ใน %s ปี"]][n]}},function(s,n){function e(s,n,e,a,r){var t=r%10,u=a;return 1===r?u=s:1===t&&r>20?u=n:t>1&&t<5&&(r>20||r<10)&&(u=e),u}var a=e.bind(null,"секунду","%s секунду","%s секунди","%s секунд"),r=e.bind(null,"хвилину","%s хвилину","%s хвилини","%s хвилин"),t=e.bind(null,"годину","%s годину","%s години","%s годин"),u=e.bind(null,"день","%s день","%s дні","%s днів"),i=e.bind(null,"тиждень","%s тиждень","%s тиждні","%s тижднів"),o=e.bind(null,"місяць","%s місяць","%s місяці","%s місяців"),d=e.bind(null,"рік","%s рік","%s роки","%s років");s.exports=function(s,n){switch(n){case 0:return["щойно","через декілька секунд"];case 1:return[a(s)+" тому","через "+a(s)];case 2:case 3:return[r(s)+" тому","через "+r(s)];case 4:case 5:return[t(s)+" тому","через "+t(s)];case 6:case 7:return[u(s)+" тому","через "+u(s)];case 8:case 9:return[i(s)+" тому","через "+i(s)];case 10:case 11:return[o(s)+" тому","через "+o(s)];case 12:case 13:return[d(s)+" тому","через "+d(s)];default:return["",""]}}},function(s,n){s.exports=function(s,n){return[["vừa xong","một lúc"],["%s giây trước","trong %s giây"],["1 phút trước","trong 1 phút"],["%s phút trước","trong %s phút"],["1 giờ trước","trong 1 giờ"],["%s giờ trước","trong %s giờ"],["1 ngày trước","trong 1 ngày"],["%s ngày trước","trong %s ngày"],["1 tuần trước","trong 1 tuần"],["%s tuần trước","trong %s tuần"],["1 tháng trước","trong 1 tháng"],["%s tháng trước","trong %s tháng"],["1 năm trước","trong 1 năm"],["%s năm trước","trong %s năm"]][n]}},function(s,n){s.exports=function(s,n){return[["刚刚","片刻后"],["%s秒前","%s秒后"],["1分钟前","1分钟后"],["%s分钟前","%s分钟后"],["1小时前","1小时后"],["%s小时前","%s小时后"],["1天前","1天后"],["%s天前","%s天后"],["1周前","1周后"],["%s周前","%s周后"],["1月前","1月后"],["%s月前","%s月后"],["1年前","1年后"],["%s年前","%s年后"]][n]}},function(s,n){s.exports=function(s,n){return[["剛剛","片刻後"],["%s秒前","%s秒後"],["1分鐘前","1分鐘後"],["%s分鐘前","%s分鐘後"],["1小時前","1小時後"],["%s小時前","%s小時後"],["1天前","1天後"],["%s天前","%s天後"],["1周前","1周後"],["%s周前","%s周後"],["1月前","1月後"],["%s月前","%s月後"],["1年前","1年後"],["%s年前","%s年後"]][n]}}]); - -var livePollTimer = null; - -var ready = (callback) => { - if (document.readyState != "loading") callback(); - else document.addEventListener("DOMContentLoaded", callback); -} - -ready(() => { - document.querySelectorAll(".check_all").forEach(node => { - node.addEventListener("click", event => { - node.closest('table').querySelectorAll('input[type=checkbox]').forEach(inp => { inp.checked = !!node.checked; }); - }) - }); - - document.querySelectorAll("input[data-confirm]").forEach(node => { - node.addEventListener("click", event => { - if (!window.confirm(node.getAttribute("data-confirm"))) { - event.preventDefault(); - event.stopPropagation(); - } - }) - }) - - document.querySelectorAll("[data-toggle]").forEach(node => { - node.addEventListener("click", event => { - var targName = node.getAttribute("data-toggle"); - var full = document.getElementById(targName + "_full"); - if (full.style.display == "block") { - full.style.display = 'none'; - } else { - full.style.display = 'block'; - } - }) - }) - - updateFuzzyTimes(); - - var buttons = document.querySelectorAll(".live-poll"); - if (buttons.length > 0) { - buttons.forEach(node => { - node.addEventListener("click", event => { - if (localStorage.sidekiqLivePoll == "enabled") { - localStorage.sidekiqLivePoll = "disabled"; - clearTimeout(livePollTimer); - livePollTimer = null; - } else { - localStorage.sidekiqLivePoll = "enabled"; - livePollCallback(); - } - - updateLivePollButton(); - }) - }); - - updateLivePollButton(); - if (localStorage.sidekiqLivePoll == "enabled") { - scheduleLivePoll(); - } - } -}) - -function updateFuzzyTimes() { - var locale = document.body.getAttribute("data-locale"); - var parts = locale.split('-'); - if (typeof parts[1] !== 'undefined') { - parts[1] = parts[1].toUpperCase(); - locale = parts.join('_'); - } - - var t = timeago() - t.render(document.querySelectorAll('time'), locale); - t.cancel(); -} - -function updateLivePollButton() { - if (localStorage.sidekiqLivePoll == "enabled") { - document.querySelectorAll('.live-poll-stop').forEach(box => { box.style.display = "inline-block" }) - document.querySelectorAll('.live-poll-start').forEach(box => { box.style.display = "none" }) - } else { - document.querySelectorAll('.live-poll-start').forEach(box => { box.style.display = "inline-block" }) - document.querySelectorAll('.live-poll-stop').forEach(box => { box.style.display = "none" }) - } -} - -function livePollCallback() { - clearTimeout(livePollTimer); - - fetch(window.location.href).then(resp => resp.text()).then(replacePage).finally(scheduleLivePoll) -} - -function scheduleLivePoll() { - let ti = parseInt(localStorage.sidekiqTimeInterval) || 5000; - livePollTimer = setTimeout(livePollCallback, ti); -} - -function replacePage(text) { - var parser = new DOMParser(); - var doc = parser.parseFromString(text, "text/html"); - - var page = doc.querySelector('#page') - document.querySelector("#page").replaceWith(page) - - var header_status = doc.querySelector('.status') - document.querySelector('.status').replaceWith(header_status) - - updateFuzzyTimes(); -} diff --git a/web/assets/javascripts/dashboard.js b/web/assets/javascripts/dashboard.js deleted file mode 100644 index 79262af6..00000000 --- a/web/assets/javascripts/dashboard.js +++ /dev/null @@ -1,296 +0,0 @@ -// D3 3.5.16 -!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function u(n){return!isNaN(n)}function i(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)<0?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)>0?u=i:r=i+1}return r}}}function a(n){return n.length}function o(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function s(n){return(n+="")===xa||n[0]===ba?ba+n:n}function f(n){return(n+="")[0]===ba?n.slice(1):n}function h(n){return s(n)in this._}function g(n){return(n=s(n))in this._&&delete this._[n]}function p(){var n=[];for(var t in this._)n.push(f(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function m(){this._=Object.create(null)}function y(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=_a.length;r>e;++e){var u=_a[e]+t;if(u in n)return u}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,u=-1,i=r.length;++ue;e++)for(var u,i=n[e],a=0,o=i.length;o>a;a++)(u=i[a])&&t(u,a,e);return n}function Z(n){return Sa(n,La),n}function V(n){var t,e;return function(r,u,i){var a,o=n[i].update,l=o.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(a=o[t])&&++t0&&(n=n.slice(0,o));var c=qa.get(n);return c&&(n=c,l=B),o?t?u:r:t?b:i}function $(n,t){return function(e){var r=oa.event;oa.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{oa.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Ra,u="click"+r,i=oa.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ta&&(Ta="onselectstart"in e?!1:x(e.style,"userSelect")),Ta){var a=n(e).style,o=a[Ta];a[Ta]="none"}return function(n){if(i.on(r,null),Ta&&(a[Ta]=o),n){var t=function(){i.on(u,null)};i.on(u,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var u=r.createSVGPoint();if(0>Da){var i=t(n);if(i.scrollX||i.scrollY){r=oa.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var a=r[0][0].getScreenCTM();Da=!(a.f||a.e),r.remove()}}return Da?(u.x=e.pageX,u.y=e.pageY):(u.x=e.clientX,u.y=e.clientY),u=u.matrixTransform(n.getScreenCTM().inverse()),[u.x,u.y]}var o=n.getBoundingClientRect();return[e.clientX-o.left-n.clientLeft,e.clientY-o.top-n.clientTop]}function G(){return oa.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?ja:Math.acos(n)}function tn(n){return n>1?Oa:-1>n?-Oa:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function an(n){return(n=Math.sin(n/2))*n}function on(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(a-i)*n/60:180>n?a:240>n?i+(a-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,a;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,a=.5>=e?e*(1+t):e+t-e*t,i=2*e-a,new yn(u(n+120),u(n),u(n-120))}function sn(n,t,e){return this instanceof sn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof sn?new sn(n.h,n.c,n.l):n instanceof hn?pn(n.l,n.a,n.b):pn((n=Sn((n=oa.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new sn(n,t,e)}function fn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Ia)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof sn?fn(n.h,n.c,n.l):Sn((n=yn(n)).r,n.g,n.b):new hn(n,t,e)}function gn(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=vn(u)*Qa,r=vn(r)*no,i=vn(i)*to,new yn(mn(3.2404542*u-1.5371385*r-.4985314*i),mn(-.969266*u+1.8760108*r+.041556*i),mn(.0556434*u-.2040259*r+1.0572252*i))}function pn(n,t,e){return n>0?new sn(Math.atan2(e,t)*Ya,Math.sqrt(t*t+e*e),n):new sn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function mn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function yn(n,t,e){return this instanceof yn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof yn?new yn(n.r,n.g,n.b):_n(""+n,yn,cn):new yn(n,t,e)}function Mn(n){return new yn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,u,i,a=0,o=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Nn(u[0]),Nn(u[1]),Nn(u[2]))}return(i=uo.get(n))?t(i.r,i.g,i.b):(null==n||"#"!==n.charAt(0)||isNaN(i=parseInt(n.slice(1),16))||(4===n.length?(a=(3840&i)>>4,a=a>>4|a,o=240&i,o=o>>4|o,l=15&i,l=l<<4|l):7===n.length&&(a=(16711680&i)>>16,o=(65280&i)>>8,l=255&i)),t(a,o,l))}function wn(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),a=Math.max(n,t,e),o=a-i,l=(a+i)/2;return o?(u=.5>l?o/(a+i):o/(2-a-i),r=n==a?(t-e)/o+(e>t?6:0):t==a?(e-n)/o+2:(n-t)/o+4,r*=60):(r=NaN,u=l>0&&1>l?0:r),new ln(r,u,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/Qa),u=dn((.2126729*n+.7151522*t+.072175*e)/no),i=dn((.0193339*n+.119192*t+.9503041*e)/to);return hn(116*u-16,500*(r-u),200*(u-i))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function u(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(i,l)}catch(r){return void a.error.call(i,r)}a.load.call(i,n)}else a.error.call(i,l)}var i={},a=oa.dispatch("beforesend","progress","load","error"),o={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=u:l.onreadystatechange=function(){l.readyState>3&&u()},l.onprogress=function(n){var t=oa.event;oa.event=n;try{a.progress.call(i,l)}finally{oa.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?o[n]:(null==t?delete o[n]:o[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(c=n,i):c},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(ca(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),l.open(e,n,!0),null==t||"accept"in o||(o.accept=t+",*/*"),l.setRequestHeader)for(var s in o)l.setRequestHeader(s,o[s]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),a.beforesend.call(i,l),l.send(null==r?null:r),i},i.abort=function(){return l.abort(),i},oa.rebind(i,a,"on"),null==r?i:i.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,n:null};return ao?ao.n=i:io=i,ao=i,oo||(lo=clearTimeout(lo),oo=1,co(Tn)),i}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(lo),lo=setTimeout(Tn,t)),oo=0):(oo=1,co(Tn))}function Rn(){for(var n=Date.now(),t=io;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=io,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r&&e?function(n,t){for(var u=n.length,i=[],a=0,o=r[0],l=0;u>0&&o>0&&(l+o+1>t&&(o=Math.max(1,t-l)),i.push(n.substring(u-=o,u+o)),!((l+=o+1)>t));)o=r[a=(a+1)%r.length];return i.reverse().join(e)}:y;return function(n){var e=fo.exec(n),r=e[1]||" ",a=e[2]||">",o=e[3]||"-",l=e[4]||"",c=e[5],s=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1,y=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===a)&&(c=r="0",a="="),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+g.toLowerCase());case"c":y=!1;case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===l&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=ho.get(g)||Fn;var M=c&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===o?"":o;if(0>p){var l=oa.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=p;n=g(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=y?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&f&&(x=i(x,1/0));var S=v.length+x.length+b.length+(M?0:u.length),k=s>S?new Array(S=s-S+1).join(r):"";return M&&(x=i(k+x,k.length?s-b.length:1/0)),u+=v,n=x+b,("<"===a?u+n+k:">"===a?k+u+n:"^"===a?k.substring(0,S>>=1)+u+n+k.substring(S):u+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new po(e-1)),1),e}function i(n,e){return t(n=new po(+n),e),n}function a(n,r,i){var a=u(n),o=[];if(i>1)for(;r>a;)e(a)%i||o.push(new Date(+a)),t(a,1);else for(;r>a;)o.push(new Date(+a)),t(a,1);return o}function o(n,t,e){try{po=Hn;var r=new Hn;return r._=n,a(r,t,e)}finally{po=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=a;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(u),l.offset=In(i),l.range=o,n}function In(n){return function(t,e){try{po=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{po=Date}}}function Yn(n){function t(n){function t(t){for(var e,u,i,a=[],o=-1,l=0;++oo;){if(r>=c)return-1;if(u=t.charCodeAt(o++),37===u){if(a=t.charAt(o++),i=C[a in mo?t.charAt(o++):a],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function s(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{po=Hn;var t=new po;return t._=n,r(t)}finally{po=Date}}var r=t(n);return e.parse=function(n){try{po=Hn;var t=r.parse(n);return t&&t._}finally{po=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=oa.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(m),k=Xn(m),N=Vn(y),E=Xn(y);p.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+go.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(go.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(go.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:ot,"%":function(){return"%"}},C={a:r,A:u,b:i,B:a,c:o,d:tt,e:tt,H:rt,I:rt,j:et,L:at,m:nt,M:ut,p:s,S:it,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function Vn(n){return new RegExp("^(?:"+n.map(oa.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function ut(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function it(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function at(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ot(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=Ma(t)/60|0,u=Ma(t)%60;return e+Zn(r,"0",2)+Zn(u,"0",2)}function lt(n,t,e){Mo.lastIndex=0;var r=Mo.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,o=a*e,l=Math.cos(t),c=Math.sin(t),s=i*c,f=u*l+s*Math.cos(o),h=s*a*Math.sin(o);ko.add(Math.atan2(h,f)),r=n,u=l,i=c}var t,e,r,u,i;No.point=function(a,o){No.point=n,r=(t=a)*Ia,u=Math.cos(o=(e=o)*Ia/2+ja/4),i=Math.sin(o)},No.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function mt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function yt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return Ma(n[0]-t[0])o;++o)u.point((e=n[o])[0],e[1]);return void u.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,i.push(l),a.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,i.push(l),a.push(c)}}),a.sort(t),qt(i),qt(a),i.length){for(var o=0,l=e,c=a.length;c>o;++o)a[o].e=l=!l;for(var s,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;s=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var o=0,c=s.length;c>o;++o)u.point((f=s[o])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){s=g.p.z;for(var o=s.length-1;o>=0;--o)u.point((f=s[o])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,s=g.z,p=!p}while(!g.v);u.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r0){for(b||(i.polygonStart(),b=!0),i.lineStart();++a1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Dt))}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:a,lineStart:l,lineEnd:c,polygonStart:function(){y.point=s,y.lineStart=f,y.lineEnd=h,g=[],p=[]},polygonEnd:function(){y.point=a,y.lineStart=l,y.lineEnd=c,g=oa.merge(g);var n=Ot(m,p);g.length?(b||(i.polygonStart(),b=!0),Lt(g,Ut,n,e,i)):n&&(b||(i.polygonStart(),b=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),b&&(i.polygonEnd(),b=!1),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},M=Pt(),x=t(M),b=!1;return y}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Oa-Pa:Oa-n[1])-((t=t.x)[0]<0?t[1]-Oa-Pa:Oa-t[1])}function jt(n){var t,e=NaN,r=NaN,u=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(i,a){var o=i>0?ja:-ja,l=Ma(i-e);Ma(l-ja)0?Oa:-Oa),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(o,r),n.point(i,r),t=0):u!==o&&l>=ja&&(Ma(e-u)Pa?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*a)):(t+r)/2}function Ht(n,t,e,r){var u;if(null==n)u=e*Oa,r.point(-ja,u),r.point(0,u),r.point(ja,u),r.point(ja,0),r.point(ja,-u),r.point(0,-u),r.point(-ja,-u),r.point(-ja,0),r.point(-ja,u);else if(Ma(n[0]-t[0])>Pa){var i=n[0]o;++o){var c=t[o],s=c.length;if(s)for(var f=c[0],h=f[0],g=f[1]/2+ja/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===s&&(d=0),n=c[d];var m=n[0],y=n[1]/2+ja/4,M=Math.sin(y),x=Math.cos(y),b=m-h,_=b>=0?1:-1,w=_*b,S=w>ja,k=p*M;if(ko.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),i+=S?b+_*Fa:b,S^h>=e^m>=e){var N=yt(dt(f),dt(n));bt(N);var E=yt(u,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(a+=S^b>=0?1:-1)}if(!d++)break;h=m,p=M,v=x,f=n}}return(-Pa>i||Pa>i&&0>ko)^1&a}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,l,c,s;return{lineStart:function(){c=l=!1,s=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=a?v?0:u(f,h):v?u(f+(0>f?ja:-ja),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(g=r(e,p),(wt(e,g)||wt(p,g))&&(p[0]+=Pa,p[1]+=Pa,v=t(p[0],p[1]))),v!==l)s=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(o&&e&&a^v){var m;d&i||!(m=r(p,e,!0))||(s=0,a?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&wt(e,p)||n.point(p[0],p[1]),e=p,l=v,i=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return s|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),u=dt(t),a=[1,0,0],o=yt(r,u),l=mt(o,o),c=o[0],s=l-c*c;if(!s)return!e&&n;var f=i*l/s,h=-i*c/s,g=yt(a,o),p=xt(a,f),v=xt(o,h);Mt(p,v);var d=g,m=mt(p,d),y=mt(d,d),M=m*m-y*(mt(p,p)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-m-x)/y);if(Mt(b,p),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=Ma(E-ja)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(Ma(b[0]-w)ja^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-m+x)/y);return Mt(z,p),[b,_t(z)]}}}function u(t,e){var r=a?n:ja-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),a=i>0,o=Ma(i)>Pa,l=ve(n,6*Ia);return Rt(t,e,l,a?[0,-n]:[-ja,n-ja])}function Yt(n,t,e,r){return function(u){var i,a=u.a,o=u.b,l=a.x,c=a.y,s=o.x,f=o.y,h=0,g=1,p=s-l,v=f-c;if(i=n-l,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-l,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-c,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-c,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:l+h*p,y:c+h*v}),1>g&&(u.b={x:l+g*p,y:c+g*v}),u}}}}}}function Zt(n,t,e,r){function u(r,u){return Ma(r[0]-n)0?0:3:Ma(r[0]-e)0?2:1:Ma(r[1]-t)0?1:0:u>0?3:2}function i(n,t){return a(n.x,t.x)}function a(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(o){function l(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,a=1,o=d[u],l=o.length,c=o[0];l>a;++a)i=o[a],c[1]<=r?i[1]>r&&Q(c,i,n)>0&&++t:i[1]<=r&&Q(c,i,n)<0&&--t,c=i;return 0!==t}function c(i,o,l,c){var s=0,f=0;if(null==i||(s=u(i,l))!==(f=u(o,l))||a(i,o)<0^l>0){do c.point(0===s||3===s?n:e,s>1?r:t);while((s=(s+l+4)%4)!==f)}else c.point(o[0],o[1])}function s(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){s(n,t)&&o.point(n,t)}function h(){C.point=p,d&&d.push(m=[]),S=!0,w=!1,b=_=NaN}function g(){v&&(p(y,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=f,w&&o.lineEnd()}function p(n,t){n=Math.max(-Ho,Math.min(Ho,n)),t=Math.max(-Ho,Math.min(Ho,t));var e=s(n,t);if(d&&m.push([n,t]),S)y=n,M=t,x=e,S=!1,e&&(o.lineStart(),o.point(n,t));else if(e&&w)o.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(o.lineStart(),o.point(r.a.x,r.a.y)),o.point(r.b.x,r.b.y),e||o.lineEnd(),k=!1):e&&(o.lineStart(),o.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,m,y,M,x,b,_,w,S,k,N=o,E=Pt(),A=Yt(n,t,e,r),C={point:f,lineStart:h,lineEnd:g,polygonStart:function(){o=E,v=[],d=[],k=!0},polygonEnd:function(){o=N,v=oa.merge(v);var t=l([n,r]),e=k&&t,u=v.length;(e||u)&&(o.polygonStart(),e&&(o.lineStart(),c(null,null,1,o),o.lineEnd()),u&&Lt(v,i,t,c,o),o.polygonEnd()),v=d=m=null}};return C}}function Vt(n){var t=0,e=ja/3,r=oe(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*ja/180,e=n[1]*ja/180):[t/ja*180,e/ja*180]},u}function Xt(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),a-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),a=Math.sqrt(i)/u;return e.invert=function(n,t){var e=a-t;return[Math.atan2(n,e)/u,tn((i-(n*n+e*e)*u*u)/(2*u))]},e}function $t(){function n(n,t){Io+=u*n-r*t,r=n,u=t}var t,e,r,u;$o.point=function(i,a){$o.point=n,t=r=i,e=u=a},$o.lineEnd=function(){n(t,e)}}function Bt(n,t){Yo>n&&(Yo=n),n>Vo&&(Vo=n),Zo>t&&(Zo=t),t>Xo&&(Xo=t)}function Wt(){function n(n,t){a.push("M",n,",",t,i)}function t(n,t){a.push("M",n,",",t),o.point=e}function e(n,t){a.push("L",n,",",t)}function r(){o.point=n}function u(){a.push("Z")}var i=Jt(4.5),a=[],o={point:n,lineStart:function(){o.point=t},lineEnd:r,polygonStart:function(){o.lineEnd=u},polygonEnd:function(){o.lineEnd=r,o.point=n},pointRadius:function(n){return i=Jt(n),o},result:function(){if(a.length){var n=a.join("");return a=[],n}}};return o}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Co+=n,zo+=t,++Lo}function Kt(){function n(n,r){var u=n-t,i=r-e,a=Math.sqrt(u*u+i*i);qo+=a*(t+n)/2,To+=a*(e+r)/2,Ro+=a,Gt(t=n,e=r)}var t,e;Wo.point=function(r,u){Wo.point=n,Gt(t=r,e=u)}}function Qt(){Wo.point=Gt}function ne(){function n(n,t){var e=n-r,i=t-u,a=Math.sqrt(e*e+i*i);qo+=a*(r+n)/2,To+=a*(u+t)/2,Ro+=a,a=u*n-r*t,Do+=a*(r+n),Po+=a*(u+t),Uo+=3*a,Gt(r=n,u=t)}var t,e,r,u;Wo.point=function(i,a){Wo.point=n,Gt(t=r=i,e=u=a)},Wo.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+a,e),n.arc(t,e,a,0,Fa)}function e(t,e){n.moveTo(t,e),o.point=r}function r(t,e){n.lineTo(t,e)}function u(){o.point=t}function i(){n.closePath()}var a=4.5,o={point:t,lineStart:function(){o.point=e},lineEnd:u,polygonStart:function(){o.lineEnd=i},polygonEnd:function(){o.lineEnd=u,o.point=t},pointRadius:function(n){return a=n,o},result:b};return o}function ee(n){function t(n){return(o?r:e)(n)}function e(t){return ie(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=i,t.lineStart()}function i(e,r){var i=dt([e,r]),a=n(e,r);u(M,x,y,b,_,w,M=a[0],x=a[1],y=e,b=i[0],_=i[1],w=i[2],o,t),t.point(M,x)}function a(){S.point=e,t.lineEnd()}function l(){ -r(),S.point=c,S.lineEnd=s}function c(n,t){i(f=n,h=t),g=M,p=x,v=b,d=_,m=w,S.point=i}function s(){u(M,x,y,b,_,w,g,p,f,v,d,m,o,t),S.lineEnd=a,a()}var f,h,g,p,v,d,m,y,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:a,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,o,l,c,s,f,h,g,p,v,d,m){var y=s-t,M=f-e,x=y*y+M*M;if(x>4*i&&d--){var b=o+g,_=l+p,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=Ma(Ma(w)-1)i||Ma((y*z+M*L)/x-.5)>.3||a>o*g+l*p+c*v)&&(u(t,e,r,o,l,c,A,C,N,b/=S,_/=S,w,d,m),m.point(A,C),u(A,C,N,b,_,w,s,f,h,g,p,v,d,m))}}var i=.5,a=Math.cos(30*Ia),o=16;return t.precision=function(n){return arguments.length?(o=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function re(n){var t=ee(function(t,e){return n([t*Ya,e*Ya])});return function(n){return le(t(n))}}function ue(n){this.stream=n}function ie(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function ae(n){return oe(function(){return n})()}function oe(n){function t(n){return n=o(n[0]*Ia,n[1]*Ia),[n[0]*h+l,c-n[1]*h]}function e(n){return n=o.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Ya,n[1]*Ya]}function r(){o=Ct(a=fe(m,M,x),i);var n=i(v,d);return l=g-n[0]*h,c=p+n[1]*h,u()}function u(){return s&&(s.valid=!1,s=null),t}var i,a,o,l,c,s,f=ee(function(n,t){return n=i(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,M=0,x=0,b=Fo,_=y,w=null,S=null;return t.stream=function(n){return s&&(s.valid=!1),s=le(b(a,f(_(n)))),s.valid=!0,s},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Fo):It((w=+n)*Ia),u()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):y,u()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Ia,d=n[1]%360*Ia,r()):[v*Ya,d*Ya]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Ia,M=n[1]%360*Ia,x=n.length>2?n[2]%360*Ia:0,r()):[m*Ya,M*Ya,x*Ya]},oa.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function le(n){return ie(n,function(t,e){n.point(t*Ia,e*Ia)})}function ce(n,t){return[n,t]}function se(n,t){return[n>ja?n-Fa:-ja>n?n+Fa:n,t]}function fe(n,t,e){return n?t||e?Ct(ge(n),pe(t,e)):ge(n):t||e?pe(t,e):se}function he(n){return function(t,e){return t+=n,[t>ja?t-Fa:-ja>t?t+Fa:t,e]}}function ge(n){var t=he(n);return t.invert=he(-n),t}function pe(n,t){function e(n,t){var e=Math.cos(t),o=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),s=c*r+o*u;return[Math.atan2(l*i-s*a,o*r-c*u),tn(s*i+l*a)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),a=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),o=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),s=c*i-l*a;return[Math.atan2(l*i+c*a,o*r+s*u),tn(s*r-o*u)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,a,o){var l=a*t;null!=u?(u=de(e,u),i=de(e,i),(a>0?i>u:u>i)&&(u+=a*Fa)):(u=n+a*Fa,i=n-.5*l);for(var c,s=u;a>0?s>i:i>s;s-=l)o.point((c=_t([e,-r*Math.cos(s),-r*Math.sin(s)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Pa)%(2*Math.PI)}function me(n,t,e){var r=oa.range(n,t-Pa,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function ye(n,t,e){var r=oa.range(n,t-Pa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),a=Math.cos(r),o=Math.sin(r),l=u*Math.cos(n),c=u*Math.sin(n),s=a*Math.cos(e),f=a*Math.sin(e),h=2*Math.asin(Math.sqrt(an(r-t)+u*a*an(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*l+t*s,u=e*c+t*f,a=e*i+t*o;return[Math.atan2(u,r)*Ya,Math.atan2(a,Math.sqrt(r*r+u*u))*Ya]}:function(){return[n*Ya,t*Ya]};return p.distance=h,p}function _e(){function n(n,u){var i=Math.sin(u*=Ia),a=Math.cos(u),o=Ma((n*=Ia)-t),l=Math.cos(o);Jo+=Math.atan2(Math.sqrt((o=a*Math.sin(o))*o+(o=r*i-e*a*l)*o),e*i+r*a*l),t=n,e=i,r=a}var t,e,r;Go.point=function(u,i){t=u*Ia,e=Math.sin(i*=Ia),r=Math.cos(i),Go.point=n},Go.lineEnd=function(){Go.point=Go.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),a=Math.cos(u);return[Math.atan2(n*i,r*a),Math.asin(r&&e*i/r)]},e}function Se(n,t){function e(n,t){a>0?-Oa+Pa>t&&(t=-Oa+Pa):t>Oa-Pa&&(t=Oa-Pa);var e=a/Math.pow(u(t),i);return[e*Math.sin(i*n),a-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(ja/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),a=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=a-t,r=K(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(a/r,1/i))-Oa]},e):Ne}function ke(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return Ma(u)u;u++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var u=n[0],i=e[0],a=t[0]-u,o=r[0]-i,l=n[1],c=e[1],s=t[1]-l,f=r[1]-c,h=(o*(l-c)-f*(u-i))/(f*a-o*s);return[u+h*a,l+h*s]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=cl.pop()||new Pe;return t.site=n,t}function je(n){Be(n),al.remove(n),cl.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,a=n.N,o=[n];je(n);for(var l=i;l.circle&&Ma(e-l.circle.x)s;++s)c=o[s],l=o[s-1],nr(c.edge,l.site,c.site,u);l=o[0],c=o[f-1],c.edge=Ke(l.site,c.site,null,u),$e(l),$e(c)}function He(n){for(var t,e,r,u,i=n.x,a=n.y,o=al._;o;)if(r=Oe(o,a)-i,r>Pa)o=o.L;else{if(u=i-Ie(o,a),!(u>Pa)){r>-Pa?(t=o.P,e=o):u>-Pa?(t=o,e=o.N):t=e=o;break}if(!o.R){t=o;break}o=o.R}var l=Ue(n);if(al.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),al.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,s=c.x,f=c.y,h=n.x-s,g=n.y-f,p=e.site,v=p.x-s,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,M=v*v+d*d,x={x:(d*y-g*M)/m+s,y:(h*M-v*y)/m+f};nr(e.edge,c,p,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,p,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var a=n.P;if(!a)return-(1/0);e=a.site;var o=e.x,l=e.y,c=l-t;if(!c)return o;var s=o-r,f=1/i-1/c,h=s/c;return f?(-h+Math.sqrt(h*h-2*f*(s*s/(-2*c)-l+c/2+u-i/2)))/f+r:(r+o)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,u,i,a,o,l,c,s,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=il,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(o=i.edges,l=o.length,a=0;l>a;)s=o[a].end(),r=s.x,u=s.y,c=o[++a%l].start(),t=c.x,e=c.y,(Ma(r-t)>Pa||Ma(u-e)>Pa)&&(o.splice(a,0,new tr(Qe(i.site,s,Ma(r-f)Pa?{x:f,y:Ma(t-f)Pa?{x:Ma(e-p)Pa?{x:h,y:Ma(t-h)Pa?{x:Ma(e-g)=-Ua)){var g=l*l+c*c,p=s*s+f*f,v=(f*g-c*p)/h,d=(l*p-s*g)/h,f=d+o,m=sl.pop()||new Xe;m.arc=n,m.site=u,m.x=v+a,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,M=ll._;M;)if(m.yd||d>=o)return;if(h>p){if(i){if(i.y>=c)return}else i={x:d,y:l};e={x:d,y:c}}else{if(i){if(i.yr||r>1)if(h>p){if(i){if(i.y>=c)return}else i={x:(l-u)/r,y:l};e={x:(c-u)/r,y:c}}else{if(i){if(i.yg){if(i){if(i.x>=o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}else{if(i){if(i.xi||f>a||r>h||u>g)){if(p=n.point){var p,v=t-n.x,d=e-n.y,m=v*v+d*d;if(l>m){var y=Math.sqrt(l=m);r=t-y,u=e-y,i=t+y,a=e+y,o=p}}for(var M=n.nodes,x=.5*(s+h),b=.5*(f+g),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,s,f,x,b);break;case 1:c(n,x,f,h,b);break;case 2:c(n,s,b,x,g);break;case 3:c(n,x,b,h,g)}}}(n,r,u,i,a),o}function vr(n,t){n=oa.rgb(n),t=oa.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,a=t.g-r,o=t.b-u;return function(n){return"#"+bn(Math.round(e+i*n))+bn(Math.round(r+a*n))+bn(Math.round(u+o*n))}}function dr(n,t){var e,r={},u={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function mr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function yr(n,t){var e,r,u,i=hl.lastIndex=gl.lastIndex=0,a=-1,o=[],l=[];for(n+="",t+="";(e=hl.exec(n))&&(r=gl.exec(t));)(u=r.index)>i&&(u=t.slice(i,u),o[a]?o[a]+=u:o[++a]=u),(e=e[0])===(r=r[0])?o[a]?o[a]+=r:o[++a]=r:(o[++a]=null,l.push({i:a,x:mr(e,r)})),i=gl.lastIndex;return ir;++r)o[(e=l[r]).i]=e.x(n);return o.join("")})}function Mr(n,t){for(var e,r=oa.interpolators.length;--r>=0&&!(e=oa.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],u=[],i=n.length,a=t.length,o=Math.min(n.length,t.length);for(e=0;o>e;++e)r.push(Mr(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;a>e;++e)u[e]=t[e];return function(n){for(e=0;o>e;++e)u[e]=r[e](n);return u}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Oa)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Fa*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Fa/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=oa.hcl(n),t=oa.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,a=t.c-r,o=t.l-u;return isNaN(a)&&(a=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return fn(e+i*n,r+a*n,u+o*n)+""}}function Dr(n,t){n=oa.hsl(n),t=oa.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,a=t.s-r,o=t.l-u;return isNaN(a)&&(a=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return cn(e+i*n,r+a*n,u+o*n)+""}}function Pr(n,t){n=oa.lab(n),t=oa.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,a=t.a-r,o=t.b-u;return function(n){return gn(e+i*n,r+a*n,u+o*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),u=Fr(t,e),i=Hr(Or(e,t,-u))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:mr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:mr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var u=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:u-4,x:mr(n[0],t[0])},{i:u-2,x:mr(n[1],t[1])})}else(1!==t[0]||1!==t[1])&&e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=oa.transform(n),t=oa.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,u=-1,i=r.length;++u=0;)e.push(u[r])}function au(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(i=n.children)&&(u=i.length))for(var u,i,a=-1;++ae;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function mu(n){return n.reduce(yu,0)}function yu(n,t){return n+t[1]}function Mu(n,t){return xu(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xu(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function bu(n){return[oa.min(n),oa.max(n)]}function _u(n,t){return n.value-t.value}function wu(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Su(n,t){n._pack_next=t,t._pack_prev=n}function ku(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function Nu(n){function t(n){s=Math.min(n.x-n.r,s),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(c=e.length)){var e,r,u,i,a,o,l,c,s=1/0,f=-(1/0),h=1/0,g=-(1/0);if(e.forEach(Eu),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(u=e[1],u.x=u.r,u.y=0,t(u),c>2))for(i=e[2],zu(r,u,i),t(i),wu(r,i),r._pack_prev=i,wu(i,u),u=r._pack_next,a=3;c>a;a++){zu(r,u,i=e[a]);var p=0,v=1,d=1;for(o=u._pack_next;o!==u;o=o._pack_next,v++)if(ku(o,i)){p=1;break}if(1==p)for(l=r._pack_prev;l!==o._pack_prev&&!ku(l,i);l=l._pack_prev,d++);p?(d>v||v==d&&u.ra;a++)i=e[a],i.x-=m,i.y-=y,M=Math.max(M,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=M,e.forEach(Au)}}function Eu(n){n._pack_next=n._pack_prev=n}function Au(n){delete n._pack_next,delete n._pack_prev}function Cu(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,a=u.length;++i=0;)t=u[i],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pu(n,t,e){return n.a.parent===t.parent?n.a:e}function Uu(n){return 1+oa.max(n,function(n){return n.y})}function ju(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fu(n){var t=n.children;return t&&t.length?Fu(t[0]):n}function Hu(n){var t,e=n.children;return e&&(t=e.length)?Hu(e[t-1]):n}function Ou(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Iu(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Yu(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zu(n){return n.rangeExtent?n.rangeExtent():Yu(n.range())}function Vu(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Xu(n,t){var e,r=0,u=n.length-1,i=n[r],a=n[u];return i>a&&(e=r,r=u,u=e,e=i,i=a,a=e),n[r]=t.floor(i),n[u]=t.ceil(a),n}function $u(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Sl}function Bu(n,t,e,r){var u=[],i=[],a=0,o=Math.min(n.length,t.length)-1;for(n[o]2?Bu:Vu,l=r?Wr:Br;return a=u(n,t,l,e),o=u(t,n,l,Mr),i}function i(n){return a(n)}var a,o;return i.invert=function(n){return o(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Ur)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Qu(n,t)},i.tickFormat=function(t,e){return ni(n,t,e)},i.nice=function(t){return Gu(n,t),u()},i.copy=function(){return Wu(n,t,e,r)},u()}function Ju(n,t){return oa.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gu(n,t){return Xu(n,$u(Ku(n,t)[2])),Xu(n,$u(Ku(n,t)[2])),n}function Ku(n,t){null==t&&(t=10);var e=Yu(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Qu(n,t){return oa.range.apply(oa,Ku(n,t))}function ni(n,t,e){var r=Ku(n,t);if(e){var u=fo.exec(e);if(u.shift(),"s"===u[8]){var i=oa.formatPrefix(Math.max(Ma(r[0]),Ma(r[1])));return u[7]||(u[7]="."+ti(i.scale(r[2]))),u[8]="f",e=oa.format(u.join("")),function(n){return e(i.scale(n))+i.symbol}}u[7]||(u[7]="."+ei(u[8],r)),e=u.join("")}else e=",."+ti(r[2])+"f";return oa.format(e)}function ti(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function ei(n,t){var e=ti(t[2]);return n in kl?Math.abs(e-ti(Math.max(Ma(t[0]),Ma(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ri(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function a(t){return n(u(t))}return a.invert=function(t){return i(n.invert(t))},a.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),a):r},a.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),a):t},a.nice=function(){var t=Xu(r.map(u),e?Math:El);return n.domain(t),r=t.map(i),a},a.ticks=function(){var n=Yu(r),a=[],o=n[0],l=n[1],c=Math.floor(u(o)),s=Math.ceil(u(l)),f=t%1?2:t;if(isFinite(s-c)){if(e){for(;s>c;c++)for(var h=1;f>h;h++)a.push(i(c)*h);a.push(i(c))}else for(a.push(i(c));c++0;h--)a.push(i(c)*h);for(c=0;a[c]l;s--);a=a.slice(c,s)}return a},a.tickFormat=function(n,e){if(!arguments.length)return Nl;arguments.length<2?e=Nl:"function"!=typeof e&&(e=oa.format(e));var r=Math.max(1,t*n/a.ticks().length);return function(n){var a=n/i(Math.round(u(n)));return t-.5>a*t&&(a*=t),r>=a?e(n):""}},a.copy=function(){return ri(n.copy(),t,e,r)},Ju(a,n)}function ui(n,t,e){function r(t){return n(u(t))}var u=ii(t),i=ii(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Qu(e,n)},r.tickFormat=function(n,t){return ni(e,n,t)},r.nice=function(n){return r.domain(Gu(e,n))},r.exponent=function(a){return arguments.length?(u=ii(t=a),i=ii(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return ui(n.copy(),t,e)},Ju(r,n)}function ii(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ai(n,t){function e(e){return i[((u.get(e)||("range"===t.t?u.set(e,n.push(e)):NaN))-1)%i.length]}function r(t,e){return oa.range(n.length).map(function(n){return t+e*n})}var u,i,a;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new c;for(var i,a=-1,o=r.length;++ae?[NaN,NaN]:[e>0?o[e-1]:n[0],et?NaN:t/i+n,[t,t+1/i]},r.copy=function(){return li(n,t,e)},u()}function ci(n,t){function e(e){return e>=e?t[oa.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return ci(n,t)},e}function si(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qu(n,t)},t.tickFormat=function(t,e){return ni(n,t,e)},t.copy=function(){return si(n)},t}function fi(){return 0}function hi(n){return n.innerRadius}function gi(n){return n.outerRadius}function pi(n){return n.startAngle}function vi(n){return n.endAngle}function di(n){return n&&n.padAngle}function mi(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function yi(n,t,e,r,u){var i=n[0]-t[0],a=n[1]-t[1],o=(u?r:-r)/Math.sqrt(i*i+a*a),l=o*a,c=-o*i,s=n[0]+l,f=n[1]+c,h=t[0]+l,g=t[1]+c,p=(s+h)/2,v=(f+g)/2,d=h-s,m=g-f,y=d*d+m*m,M=e-r,x=s*g-h*f,b=(0>m?-1:1)*Math.sqrt(Math.max(0,M*M*y-x*x)),_=(x*m-d*b)/y,w=(-x*d-m*b)/y,S=(x*m+d*b)/y,k=(-x*d+m*b)/y,N=_-p,E=w-v,A=S-p,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mi(n){function t(t){function a(){c.push("M",i(n(s),o))}for(var l,c=[],s=[],f=-1,h=t.length,g=En(e),p=En(r);++f1?n.join("L"):n+"Z"}function bi(n){return n.join("L")+"Z"}function _i(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1&&u.push("H",r[0]),u.join("")}function wi(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1){o=t[1],i=n[l],l++,r+="C"+(u[0]+a[0])+","+(u[1]+a[1])+","+(i[0]-o[0])+","+(i[1]-o[1])+","+i[0]+","+i[1];for(var c=2;c9&&(u=3*t/Math.sqrt(u),a[o]=u*e,a[o+1]=u*r));for(o=-1;++o<=l;)u=(n[Math.min(l,o+1)][0]-n[Math.max(0,o-1)][0])/(6*(1+a[o]*a[o])),i.push([u||0,a[o]*u||0]);return i}function Fi(n){return n.length<3?xi(n):n[0]+Ai(n,ji(n))}function Hi(n){for(var t,e,r,u=-1,i=n.length;++u=t?a(n-t):void(s.c=a)}function a(e){var u=p.active,i=p[u];i&&(i.timer.c=null,i.timer.t=NaN,--p.count,delete p[u],i.event&&i.event.interrupt.call(n,n.__data__,i.index));for(var a in p)if(r>+a){var c=p[a];c.timer.c=null,c.timer.t=NaN,--p.count,delete p[a]}s.c=o,qn(function(){return s.c&&o(e||1)&&(s.c=null,s.t=NaN),1},0,l),p.active=r,v.event&&v.event.start.call(n,n.__data__,t),g=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&g.push(r)}),h=v.ease,f=v.duration}function o(u){for(var i=u/f,a=h(i),o=g.length;o>0;)g[--o].call(n,a);return i>=1?(v.event&&v.event.end.call(n,n.__data__,t),--p.count?delete p[r]:delete n[e],1):void 0}var l,s,f,h,g,p=n[e]||(n[e]={active:0,count:0}),v=p[r];v||(l=u.time,s=qn(i,0,l),v=p[r]={tween:new c,time:l,timer:s,delay:u.delay,duration:u.duration,ease:u.ease,index:t},u=null,++p.count)}function na(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function ta(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function ea(n){return n.toISOString()}function ra(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=oa.bisect(Kl,u);return i==Kl.length?[t.year,Ku(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/Kl[i-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=ua(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=ua(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yu(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],ua(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ra(n.copy(),t,e)},Ju(r,n)}function ua(n){return new Date(n)}function ia(n){return JSON.parse(n.responseText)}function aa(n){var t=sa.createRange();return t.selectNode(sa.body),t.createContextualFragment(n.responseText)}var oa={version:"3.5.16"},la=[].slice,ca=function(n){return la.call(n)},sa=this.document;if(sa)try{ca(sa.documentElement.childNodes)[0].nodeType}catch(fa){ca=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),sa)try{sa.createElement("DIV").style.setProperty("opacity",0,"")}catch(ha){var ga=this.Element.prototype,pa=ga.setAttribute,va=ga.setAttributeNS,da=this.CSSStyleDeclaration.prototype,ma=da.setProperty;ga.setAttribute=function(n,t){pa.call(this,n,t+"")},ga.setAttributeNS=function(n,t,e){va.call(this,n,t,e+"")},da.setProperty=function(n,t,e){ma.call(this,n,t+"",e)}}oa.ascending=e,oa.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},oa.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=r){e=r;break}for(;++ur&&(e=r)}else{for(;++u=r){e=r;break}for(;++ur&&(e=r)}return e},oa.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=r){e=r;break}for(;++ue&&(e=r)}else{for(;++u=r){e=r;break}for(;++ue&&(e=r)}return e},oa.extent=function(n,t){var e,r,u,i=-1,a=n.length;if(1===arguments.length){for(;++i=r){e=u=r;break}for(;++ir&&(e=r),r>u&&(u=r))}else{for(;++i=r){e=u=r;break}for(;++ir&&(e=r),r>u&&(u=r))}return[e,u]},oa.sum=function(n,t){var e,r=0,i=n.length,a=-1;if(1===arguments.length)for(;++a1?l/(s-1):void 0},oa.deviation=function(){var n=oa.variance.apply(this,arguments);return n?Math.sqrt(n):n};var ya=i(e);oa.bisectLeft=ya.left,oa.bisect=oa.bisectRight=ya.right,oa.bisector=function(n){return i(1===n.length?function(t,r){return e(n(t),r)}:n)},oa.shuffle=function(n,t,e){(i=arguments.length)<3&&(e=n.length,2>i&&(t=0));for(var r,u,i=e-t;i;)u=Math.random()*i--|0,r=n[i+t],n[i+t]=n[u+t],n[u+t]=r;return n},oa.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},oa.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},oa.transpose=function(n){if(!(u=n.length))return[];for(var t=-1,e=oa.min(n,a),r=new Array(e);++t=0;)for(r=n[u],t=r.length;--t>=0;)e[--a]=r[t];return e};var Ma=Math.abs;oa.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,u=[],i=o(Ma(e)),a=-1;if(n*=i,t*=i,e*=i,0>e)for(;(r=n+e*++a)>t;)u.push(r/i);else for(;(r=n+e*++a)=i.length)return r?r.call(u,a):e?a.sort(e):a;for(var l,s,f,h,g=-1,p=a.length,v=i[o++],d=new c;++g=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(oa.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},oa.set=function(n){var t=new m;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(m,{has:h,add:function(n){return this._[s(n+="")]=!0,n},remove:g,values:p,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,f(t))}}),oa.behavior={},oa.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},oa.event=null,oa.requote=function(n){return n.replace(wa,"\\$&")};var wa=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,Sa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ka=function(n,t){return t.querySelector(n)},Na=function(n,t){return t.querySelectorAll(n)},Ea=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ea=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(ka=function(n,t){return Sizzle(n,t)[0]||null},Na=Sizzle,Ea=Sizzle.matchesSelector),oa.selection=function(){return oa.select(sa.documentElement)};var Aa=oa.selection.prototype=[];Aa.select=function(n){var t,e,r,u,i=[];n=A(n);for(var a=-1,o=this.length;++a=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),za.hasOwnProperty(e)?{space:za[e],local:n}:n}},Aa.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=oa.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Aa.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,u=-1;if(t=e.classList){for(;++uu){if("string"!=typeof n){2>u&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>u){var i=this.node();return t(i).getComputedStyle(i,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Aa.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Aa.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Aa.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Aa.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Aa.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Aa.remove=function(){return this.each(F)},Aa.data=function(n,t){function e(n,e){var r,u,i,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new c,y=new Array(a);for(r=-1;++rr;++r)p[r]=H(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,o.push(p),l.push(g),s.push(v)}var r,u,i=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++ii;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var o=0,l=e.length;l>o;o++)(r=e[o])&&n.call(r,r.__data__,o,i)&&t.push(r)}return E(u)},Aa.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},Aa.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},Aa.size=function(){var n=0;return Y(this,function(){++n}),n};var La=[];oa.selection.enter=Z,oa.selection.enter.prototype=La,La.append=Aa.append,La.empty=Aa.empty,La.node=Aa.node,La.call=Aa.call,La.size=Aa.size,La.select=function(n){for(var t,e,r,u,i,a=[],o=-1,l=this.length;++or){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var qa=oa.map({mouseenter:"mouseover",mouseleave:"mouseout"});sa&&qa.forEach(function(n){"on"+n in sa&&qa.remove(n)});var Ta,Ra=0;oa.mouse=function(n){return J(n,k())};var Da=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;oa.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,u=0,i=t.length;i>u;++u)if((r=t[u]).identifier===e)return J(n,r)},oa.behavior.drag=function(){function n(){this.on("mousedown.drag",i).on("touchstart.drag",a)}function e(n,t,e,i,a){return function(){function o(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],p|=n|e,M=r,g({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(m.on(i+d,null).on(a+d,null),y(p),g({type:"dragend"}))}var c,s=this,f=oa.event.target.correspondingElement||oa.event.target,h=s.parentNode,g=r.of(s,arguments),p=0,v=n(),d=".drag"+(null==v?"":"-"+v),m=oa.select(e(f)).on(i+d,o).on(a+d,l),y=W(f),M=t(h,v);u?(c=u.apply(s,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],g({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),u=null,i=e(b,oa.mouse,t,"mousemove","mouseup"),a=e(G,oa.touch,y,"touchmove","touchend");return n.origin=function(t){return arguments.length?(u=t,n):u},oa.rebind(n,r,"on")},oa.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?ca(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Pa=1e-6,Ua=Pa*Pa,ja=Math.PI,Fa=2*ja,Ha=Fa-Pa,Oa=ja/2,Ia=ja/180,Ya=180/ja,Za=Math.SQRT2,Va=2,Xa=4;oa.interpolateZoom=function(n,t){var e,r,u=n[0],i=n[1],a=n[2],o=t[0],l=t[1],c=t[2],s=o-u,f=l-i,h=s*s+f*f;if(Ua>h)r=Math.log(c/a)/Za,e=function(n){return[u+n*s,i+n*f,a*Math.exp(Za*n*r)]};else{var g=Math.sqrt(h),p=(c*c-a*a+Xa*h)/(2*a*Va*g),v=(c*c-a*a-Xa*h)/(2*c*Va*g),d=Math.log(Math.sqrt(p*p+1)-p),m=Math.log(Math.sqrt(v*v+1)-v);r=(m-d)/Za,e=function(n){var t=n*r,e=rn(d),o=a/(Va*g)*(e*un(Za*t+d)-en(d));return[u+o*s,i+o*f,a*e/rn(Za*t+d)]}}return e.duration=1e3*r,e},oa.behavior.zoom=function(){function n(n){n.on(L,f).on(Ba+".zoom",g).on("dblclick.zoom",p).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function u(n){k.k=Math.max(A[0],Math.min(A[1],n))}function i(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function a(t,e,r,a){t.__chart__={x:k.x,y:k.y,k:k.k},u(Math.pow(2,a)),i(d=e,r),t=oa.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function o(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){o(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function s(n){--z||(n({type:"zoomend"}),d=null)}function f(){function n(){o=1,i(oa.mouse(u),h),c(a)}function r(){f.on(q,null).on(T,null),g(o),s(a)}var u=this,a=D.of(u,arguments),o=0,f=oa.select(t(u)).on(q,n).on(T,r),h=e(oa.mouse(u)),g=W(u);Il.call(u),l(a)}function h(){function n(){var n=oa.touches(p);return g=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=oa.event.target;oa.select(t).on(x,r).on(b,o),_.push(t);for(var e=oa.event.changedTouches,u=0,i=e.length;i>u;++u)d[e[u].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var s=l[0];a(p,s,d[s.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var s=l[0],f=l[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function r(){var n,t,e,r,a=oa.touches(p);Il.call(p);for(var o=0,l=a.length;l>o;++o,r=null)if(e=a[o],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var s=(s=e[0]-n[0])*s+(s=e[1]-n[1])*s,f=m&&Math.sqrt(s/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],u(f*g)}M=null,i(n,t),c(v)}function o(){if(oa.event.touches.length){for(var t=oa.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var u in d)return void n()}oa.selectAll(_).on(y,null),w.on(L,f).on(R,h),N(),s(v)}var g,p=this,v=D.of(p,arguments),d={},m=0,y=".zoom-"+oa.event.changedTouches[0].identifier,x="touchmove"+y,b="touchend"+y,_=[],w=oa.select(p),N=W(p);t(),l(v),w.on(L,null).on(R,t)}function g(){var n=D.of(this,arguments);y?clearTimeout(y):(Il.call(this),v=e(d=m||oa.mouse(this)),l(n)),y=setTimeout(function(){y=null,s(n)},50),S(),u(Math.pow(2,.002*$a())*k.k),i(d,v),c(n)}function p(){var n=oa.mouse(this),t=Math.log(k.k)/Math.LN2;a(this,n,e(n),oa.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,m,y,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Wa,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return Ba||(Ba="onwheel"in sa?($a=function(){return-oa.event.deltaY*(oa.event.deltaMode?120:1)},"wheel"):"onmousewheel"in sa?($a=function(){return oa.event.wheelDelta},"mousewheel"):($a=function(){return-oa.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Hl?oa.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],u=d?d[0]:e/2,i=d?d[1]:r/2,a=oa.interpolateZoom([(u-k.x)/k.k,(i-k.y)/k.k,e/k.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=a(t),o=e/r[2];this.__chart__=k={x:u-r[0]*o,y:i-r[1]*o,k:o},c(n)}}).each("interrupt.zoom",function(){s(n)}).each("end.zoom",function(){s(n)}):(this.__chart__=k,l(n),c(n),s(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},o(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},u(+t),o(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Wa:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(m=t&&[+t[0],+t[1]],n):m},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},oa.rebind(n,D,"on")};var $a,Ba,Wa=[0,1/0];oa.color=on,on.prototype.toString=function(){return this.rgb()+""},oa.hsl=ln;var Ja=ln.prototype=new on;Ja.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Ja.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Ja.rgb=function(){return cn(this.h,this.s,this.l)},oa.hcl=sn;var Ga=sn.prototype=new on;Ga.brighter=function(n){return new sn(this.h,this.c,Math.min(100,this.l+Ka*(arguments.length?n:1)))},Ga.darker=function(n){return new sn(this.h,this.c,Math.max(0,this.l-Ka*(arguments.length?n:1)))},Ga.rgb=function(){return fn(this.h,this.c,this.l).rgb()},oa.lab=hn;var Ka=18,Qa=.95047,no=1,to=1.08883,eo=hn.prototype=new on;eo.brighter=function(n){return new hn(Math.min(100,this.l+Ka*(arguments.length?n:1)),this.a,this.b)},eo.darker=function(n){return new hn(Math.max(0,this.l-Ka*(arguments.length?n:1)),this.a,this.b)},eo.rgb=function(){return gn(this.l,this.a,this.b)},oa.rgb=yn;var ro=yn.prototype=new on;ro.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),new yn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new yn(u,u,u)},ro.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new yn(n*this.r,n*this.g,n*this.b)},ro.hsl=function(){return wn(this.r,this.g,this.b)},ro.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var uo=oa.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});uo.forEach(function(n,t){uo.set(n,Mn(t))}),oa.functor=En,oa.xhr=An(y),oa.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var a=Cn(n,t,null==e?r:u(e),i);return a.row=function(n){return arguments.length?a.response(null==(e=n)?r:u(n)):e},a}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(a).join(n)}function a(n){return o.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var o=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(s>=c)return a;if(u)return u=!1,i;var t=s;if(34===n.charCodeAt(t)){for(var e=t;e++s;){var r=n.charCodeAt(s++),o=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(s)&&(++s,++o);else if(r!==l)continue;return n.slice(t,s-o)}return n.slice(t)}for(var r,u,i={},a={},o=[],c=n.length,s=0,f=0;(r=e())!==a;){for(var h=[];r!==i&&r!==a;)h.push(r),r=e();t&&null==(h=t(h,f++))||o.push(h)}return o},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new m,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(a).join(n)].concat(t.map(function(t){return u.map(function(n){return a(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},oa.csv=oa.dsv(",","text/csv"),oa.tsv=oa.dsv(" ","text/tab-separated-values");var io,ao,oo,lo,co=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};oa.timer=function(){qn.apply(this,arguments)},oa.timer.flush=function(){Rn(),Dn()},oa.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var so=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);oa.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=oa.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),so[8+e/3]};var fo=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,ho=oa.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=oa.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),go=oa.time={},po=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){vo.setUTCDate.apply(this._,arguments)},setDay:function(){vo.setUTCDay.apply(this._,arguments)},setFullYear:function(){vo.setUTCFullYear.apply(this._,arguments)},setHours:function(){vo.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){vo.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){vo.setUTCMinutes.apply(this._,arguments)},setMonth:function(){vo.setUTCMonth.apply(this._,arguments)},setSeconds:function(){vo.setUTCSeconds.apply(this._,arguments)},setTime:function(){vo.setTime.apply(this._,arguments)}};var vo=Date.prototype;go.year=On(function(n){return n=go.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),go.years=go.year.range,go.years.utc=go.year.utc.range,go.day=On(function(n){var t=new po(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),go.days=go.day.range,go.days.utc=go.day.utc.range,go.dayOfYear=function(n){var t=go.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=go[n]=On(function(n){return(n=go.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=go.year(n).getDay();return Math.floor((go.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});go[n+"s"]=e.range,go[n+"s"].utc=e.utc.range,go[n+"OfYear"]=function(n){var e=go.year(n).getDay();return Math.floor((go.dayOfYear(n)+(e+t)%7)/7)}}),go.week=go.sunday,go.weeks=go.sunday.range,go.weeks.utc=go.sunday.utc.range,go.weekOfYear=go.sundayOfYear;var mo={"-":"",_:" ",0:"0"},yo=/^\s*\d+/,Mo=/^%/;oa.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var xo=oa.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], -shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});oa.format=xo.numberFormat,oa.geo={},st.prototype={s:0,t:0,add:function(n){ft(n,this.t,bo),ft(bo.s,this.s,this),this.s?this.t+=bo.t:this.s=bo.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var bo=new st;oa.geo.stream=function(n,t){n&&_o.hasOwnProperty(n.type)?_o[n.type](n,t):ht(n,t)};var _o={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++rn?4*ja+n:n,No.lineStart=No.lineEnd=No.point=b}};oa.geo.bounds=function(){function n(n,t){M.push(x=[s=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=dt([t*Ia,e*Ia]);if(m){var u=yt(m,r),i=[u[1],-u[0],0],a=yt(i,u);bt(a),a=_t(a);var l=t-p,c=l>0?1:-1,v=a[0]*Ya*c,d=Ma(l)>180;if(d^(v>c*p&&c*t>v)){var y=a[1]*Ya;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>c*p&&c*t>v)){var y=-a[1]*Ya;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?o(s,t)>o(s,h)&&(h=t):o(t,h)>o(s,h)&&(s=t):h>=s?(s>t&&(s=t),t>h&&(h=t)):t>p?o(s,t)>o(s,h)&&(h=t):o(t,h)>o(s,h)&&(s=t)}else n(t,e);m=r,p=t}function e(){b.point=t}function r(){x[0]=s,x[1]=h,b.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=Ma(r)>180?r+(r>0?360:-360):r}else v=n,d=e;No.point(n,e),t(n,e)}function i(){No.lineStart()}function a(){u(v,d),No.lineEnd(),Ma(y)>Pa&&(s=-(h=180)),x[0]=s,x[1]=h,m=null}function o(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nko?(s=-(h=180),f=-(g=90)):y>Pa?g=90:-Pa>y&&(f=-90),x[0]=s,x[1]=h}};return function(n){g=h=-(s=f=1/0),M=[],oa.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,u=M[0],i=[u];t>r;++r)e=M[r],c(e[0],u)||c(e[1],u)?(o(u[0],e[1])>o(u[0],u[1])&&(u[1]=e[1]),o(e[0],u[1])>o(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var a,e,p=-(1/0),t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(a=o(u[1],e[0]))>p&&(p=a,s=e[0],h=u[1])}return M=x=null,s===1/0||f===1/0?[[NaN,NaN],[NaN,NaN]]:[[s,f],[h,g]]}}(),oa.geo.centroid=function(n){Eo=Ao=Co=zo=Lo=qo=To=Ro=Do=Po=Uo=0,oa.geo.stream(n,jo);var t=Do,e=Po,r=Uo,u=t*t+e*e+r*r;return Ua>u&&(t=qo,e=To,r=Ro,Pa>Ao&&(t=Co,e=zo,r=Lo),u=t*t+e*e+r*r,Ua>u)?[NaN,NaN]:[Math.atan2(e,t)*Ya,tn(r/Math.sqrt(u))*Ya]};var Eo,Ao,Co,zo,Lo,qo,To,Ro,Do,Po,Uo,jo={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){jo.lineStart=At},polygonEnd:function(){jo.lineStart=Nt}},Fo=Rt(zt,jt,Ht,[-ja,-ja/2]),Ho=1e9;oa.geo.clipExtent=function(){var n,t,e,r,u,i,a={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(o){return arguments.length?(i=Zt(n=+o[0][0],t=+o[0][1],e=+o[1][0],r=+o[1][1]),u&&(u.valid=!1,u=null),a):[[n,t],[e,r]]}};return a.extent([[0,0],[960,500]])},(oa.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,oa.geo.albers=function(){return oa.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},oa.geo.albersUsa=function(){function n(n){var i=n[0],a=n[1];return t=null,e(i,a),t||(r(i,a),t)||u(i,a),t}var t,e,r,u,i=oa.geo.albers(),a=oa.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),o=oa.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?a:u>=.166&&.234>u&&r>=-.214&&-.115>r?o:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=a.stream(n),r=o.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),a.precision(t),o.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),a.scale(.35*t),o.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var c=i.scale(),s=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[s-.455*c,f-.238*c],[s+.455*c,f+.238*c]]).stream(l).point,r=a.translate([s-.307*c,f+.201*c]).clipExtent([[s-.425*c+Pa,f+.12*c+Pa],[s-.214*c-Pa,f+.234*c-Pa]]).stream(l).point,u=o.translate([s-.205*c,f+.212*c]).clipExtent([[s-.214*c+Pa,f+.166*c+Pa],[s-.115*c-Pa,f+.234*c-Pa]]).stream(l).point,n},n.scale(1070)};var Oo,Io,Yo,Zo,Vo,Xo,$o={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Io=0,$o.lineStart=$t},polygonEnd:function(){$o.lineStart=$o.lineEnd=$o.point=b,Oo+=Ma(Io/2)}},Bo={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Wo={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Wo.lineStart=ne},polygonEnd:function(){Wo.point=Gt,Wo.lineStart=Kt,Wo.lineEnd=Qt}};oa.geo.path=function(){function n(n){return n&&("function"==typeof o&&i.pointRadius(+o.apply(this,arguments)),a&&a.valid||(a=u(i)),oa.geo.stream(n,a)),i.result()}function t(){return a=null,n}var e,r,u,i,a,o=4.5;return n.area=function(n){return Oo=0,oa.geo.stream(n,u($o)),Oo},n.centroid=function(n){return Co=zo=Lo=qo=To=Ro=Do=Po=Uo=0,oa.geo.stream(n,u(Wo)),Uo?[Do/Uo,Po/Uo]:Ro?[qo/Ro,To/Ro]:Lo?[Co/Lo,zo/Lo]:[NaN,NaN]},n.bounds=function(n){return Vo=Xo=-(Yo=Zo=1/0),oa.geo.stream(n,u(Bo)),[[Yo,Zo],[Vo,Xo]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||re(n):y,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Wt:new te(n),"function"!=typeof o&&i.pointRadius(o),t()):r},n.pointRadius=function(t){return arguments.length?(o="function"==typeof t?t:(i.pointRadius(+t),+t),n):o},n.projection(oa.geo.albersUsa()).context(null)},oa.geo.transform=function(n){return{stream:function(t){var e=new ue(t);for(var r in n)e[r]=n[r];return e}}},ue.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},oa.geo.projection=ae,oa.geo.projectionMutator=oe,(oa.geo.equirectangular=function(){return ae(ce)}).raw=ce.invert=ce,oa.geo.rotation=function(n){function t(t){return t=n(t[0]*Ia,t[1]*Ia),t[0]*=Ya,t[1]*=Ya,t}return n=fe(n[0]%360*Ia,n[1]*Ia,n.length>2?n[2]*Ia:0),t.invert=function(t){return t=n.invert(t[0]*Ia,t[1]*Ia),t[0]*=Ya,t[1]*=Ya,t},t},se.invert=ce,oa.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=fe(-n[0]*Ia,-n[1]*Ia,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Ya,n[1]*=Ya}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Ia,u*Ia),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Ia,(u=+r)*Ia),n):u},n.angle(90)},oa.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Ia,u=n[1]*Ia,i=t[1]*Ia,a=Math.sin(r),o=Math.cos(r),l=Math.sin(u),c=Math.cos(u),s=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*a)*e+(e=c*s-l*f*o)*e),l*s+c*f*o)},oa.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return oa.range(Math.ceil(i/d)*d,u,d).map(h).concat(oa.range(Math.ceil(c/m)*m,l,m).map(g)).concat(oa.range(Math.ceil(r/p)*p,e,p).filter(function(n){return Ma(n%d)>Pa}).map(s)).concat(oa.range(Math.ceil(o/v)*v,a,v).filter(function(n){return Ma(n%m)>Pa}).map(f))}var e,r,u,i,a,o,l,c,s,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(l).slice(1),h(u).reverse().slice(1),g(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],c=+t[0][1],l=+t[1][1],i>u&&(t=i,i=u,u=t),c>l&&(t=c,c=l,l=t),n.precision(y)):[[i,c],[u,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],o=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),o>a&&(t=o,o=a,a=t),n.precision(y)):[[r,o],[e,a]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,s=me(o,a,90),f=ye(r,e,y),h=me(c,l,90),g=ye(i,u,y),n):y},n.majorExtent([[-180,-90+Pa],[180,90-Pa]]).minorExtent([[-180,-80-Pa],[180,80+Pa]])},oa.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=Me,u=xe;return n.distance=function(){return oa.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},oa.geo.interpolate=function(n,t){return be(n[0]*Ia,n[1]*Ia,t[0]*Ia,t[1]*Ia)},oa.geo.length=function(n){return Jo=0,oa.geo.stream(n,Go),Jo};var Jo,Go={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Ko=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(oa.geo.azimuthalEqualArea=function(){return ae(Ko)}).raw=Ko;var Qo=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},y);(oa.geo.azimuthalEquidistant=function(){return ae(Qo)}).raw=Qo,(oa.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(oa.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var nl=we(function(n){return 1/n},Math.atan);(oa.geo.gnomonic=function(){return ae(nl)}).raw=nl,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Oa]},(oa.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var tl=we(function(){return 1},Math.asin);(oa.geo.orthographic=function(){return ae(tl)}).raw=tl;var el=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(oa.geo.stereographic=function(){return ae(el)}).raw=el,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Oa]},(oa.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,oa.geom={},oa.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=En(e),i=En(r),a=n.length,o=[],l=[];for(t=0;a>t;t++)o.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(o.sort(qe),t=0;a>t;t++)l.push([o[t][0],-o[t][1]]);var c=Le(o),s=Le(l),f=s[0]===c[0],h=s[s.length-1]===c[c.length-1],g=[];for(t=c.length-1;t>=0;--t)g.push(n[o[c[t]][2]]);for(t=+f;t=r&&c.x<=i&&c.y>=u&&c.y<=a?[[r,a],[i,a],[i,u],[r,u]]:[];s.point=n[o]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Pa)*Pa,y:Math.round(a(n,t)/Pa)*Pa,i:t}})}var r=Ce,u=ze,i=r,a=u,o=fl;return n?t(n):(t.links=function(n){return or(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return or(e(n)).cells.forEach(function(e,r){for(var u,i,a=e.site,o=e.edges.sort(Ve),l=-1,c=o.length,s=o[c-1].edge,f=s.l===a?s.r:s.l;++l=c,h=r>=s,g=h<<1|f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=hr()),f?u=c:o=c,h?a=s:l=s,i(n,t,e,r,u,a,o,l)}var s,f,h,g,p,v,d,m,y,M=En(o),x=En(l);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,a)for(g=0;p>g;++g)s=n[g],s.xm&&(m=s.x),s.y>y&&(y=s.y),f.push(s.x),h.push(s.y);else for(g=0;p>g;++g){var b=+M(s=n[g],g),_=+x(s,g);v>b&&(v=b),d>_&&(d=_),b>m&&(m=b),_>y&&(y=_),f.push(b),h.push(_)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=hr();if(k.add=function(n){i(k,n,+M(n,++g),+x(n,g),v,d,m,y)},k.visit=function(n){gr(n,k,v,d,m,y)},k.find=function(n){return pr(k,n[0],n[1],v,d,m,y)},g=-1,null==t){for(;++g=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||pl,r=dl.get(r)||y,br(r(e.apply(null,la.call(arguments,1))))},oa.interpolateHcl=Rr,oa.interpolateHsl=Dr,oa.interpolateLab=Pr,oa.interpolateRound=Ur,oa.transform=function(n){var t=sa.createElementNS(oa.ns.prefix.svg,"g");return(oa.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:ml)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var ml={a:1,b:0,c:0,d:1,e:0,f:0};oa.interpolateTransform=$r,oa.layout={},oa.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++eo*o/m){if(v>l){var c=t.charge/l;n.px-=i*c,n.py-=a*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=i*c,n.py-=a*c}}return!t.charge}}function t(n){n.px=oa.event.x,n.py=oa.event.y,l.resume()}var e,r,u,i,a,o,l={},c=oa.dispatch("start","tick","end"),s=[1,1],f=.9,h=yl,g=Ml,p=-30,v=xl,d=.1,m=.64,M=[],x=[];return l.tick=function(){if((u*=.99)<.005)return e=null,c.end({type:"end",alpha:u=0}),!0;var t,r,l,h,g,v,m,y,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,g=l.target,y=g.x-h.x,b=g.y-h.y,(v=y*y+b*b)&&(v=u*a[r]*((v=Math.sqrt(v))-i[r])/v,y*=v,b*=v,g.x-=y*(m=h.weight+g.weight?h.weight/(h.weight+g.weight):.5),g.y-=b*m,h.x+=y*(m=1-m),h.y+=b*m);if((m=u*d)&&(y=s[0]/2,b=s[1]/2,r=-1,m))for(;++r<_;)l=M[r],l.x+=(y-l.x)*m,l.y+=(b-l.y)*m;if(p)for(ru(t=oa.geom.quadtree(M),u,o),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*f,l.y-=(l.py-(l.py=l.y))*f);c.tick({type:"tick",alpha:u})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(s=n,l):s},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.friction=function(n){return arguments.length?(f=+n,l):f},l.charge=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(m=n*n,l):Math.sqrt(m)},l.alpha=function(n){return arguments.length?(n=+n,u?n>0?u=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:u=0})):n>0&&(c.start({type:"start",alpha:u=n}),e=qn(l.tick)),l):u},l.start=function(){function n(n,r){if(!e){for(e=new Array(u),l=0;u>l;++l)e[l]=[];for(l=0;c>l;++l){var i=x[l];e[i.source.index].push(i.target),e[i.target.index].push(i.source)}}for(var a,o=e[t],l=-1,s=o.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;u>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",f)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(i=[],"function"==typeof h)for(t=0;c>t;++t)i[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)i[t]=h;if(a=[],"function"==typeof g)for(t=0;c>t;++t)a[t]=+g.call(this,x[t],t);else for(t=0;c>t;++t)a[t]=g;if(o=[],"function"==typeof p)for(t=0;u>t;++t)o[t]=+p.call(this,M[t],t);else for(t=0;u>t;++t)o[t]=p;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=oa.behavior.drag().origin(y).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",nu)),arguments.length?void this.on("mouseover.force",tu).on("mouseout.force",eu).call(r):r},oa.rebind(l,c,"on")};var yl=20,Ml=1,xl=1/0;oa.layout.hierarchy=function(){function n(u){var i,a=[u],o=[];for(u.depth=0;null!=(i=a.pop());)if(o.push(i),(c=e.call(n,i,i.depth))&&(l=c.length)){for(var l,c,s;--l>=0;)a.push(s=c[l]),s.parent=i,s.depth=i.depth+1;r&&(i.value=0),i.children=c}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return au(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),o}var t=cu,e=ou,r=lu;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(iu(t,function(n){n.children&&(n.value=0)}),au(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},oa.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(a=i.length)){var a,o,l,c=-1;for(r=t.value?r/t.value:0;++cf?-1:1),p=oa.sum(c),v=p?(f-l*g)/p:0,d=oa.range(l),m=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(a[n],a[t])}),d.forEach(function(n){m[n]={data:a[n],value:o=c[n],startAngle:s,endAngle:s+=o*v+g,padAngle:h}}),m}var t=Number,e=bl,r=0,u=Fa,i=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n.padAngle=function(t){return arguments.length?(i=t,n):i},n};var bl={};oa.layout.stack=function(){function n(o,l){if(!(h=o.length))return o;var c=o.map(function(e,r){return t.call(n,e,r)}),s=c.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),a.call(n,t,e)]})}),f=e.call(n,s,l);c=oa.permute(c,f),s=oa.permute(s,f);var h,g,p,v,d=r.call(n,s,l),m=c[0].length;for(p=0;m>p;++p)for(u.call(n,c[0][p],v=d[p],s[0][p][1]),g=1;h>g;++g)u.call(n,c[g][p],v+=s[g-1][p][1],s[g][p][1]);return o}var t=y,e=pu,r=vu,u=gu,i=fu,a=hu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||pu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vu,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(u=t,n):u},n};var _l=oa.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(du),i=n.map(mu),a=oa.range(r).sort(function(n,t){return u[n]-u[t]}),o=0,l=0,c=[],s=[];for(t=0;r>t;++t)e=a[t],l>o?(o+=i[e],c.push(e)):(l+=i[e],s.push(e));return s.reverse().concat(c)},reverse:function(n){return oa.range(n.length).reverse()},"default":pu}),wl=oa.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,a=[],o=0,l=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;i>e;++e)l[e]=(o-a[e])/2;return l},wiggle:function(n){var t,e,r,u,i,a,o,l,c,s=n.length,f=n[0],h=f.length,g=[];for(g[0]=l=c=0,e=1;h>e;++e){for(t=0,u=0;s>t;++t)u+=n[t][e][1];for(t=0,i=0,o=f[e][0]-f[e-1][0];s>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;i+=a*n[t][e][1]}g[e]=l-=u?i/u*o:0,c>l&&(c=l)}for(e=0;h>e;++e)g[e]-=c;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,a=1/u,o=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=a}for(e=0;i>e;++e)o[e]=0;return o},zero:vu});oa.layout.histogram=function(){function n(n,i){for(var a,o,l=[],c=n.map(e,this),s=r.call(this,c,i),f=u.call(this,s,c,i),i=-1,h=c.length,g=f.length-1,p=t?1:1/h;++i0)for(i=-1;++i=s[0]&&o<=s[1]&&(a=l[oa.bisect(f,o,1,g)-1],a.y+=p,a.push(n[i]));return l}var t=!0,e=Number,r=bu,u=Mu;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return xu(n,t)}:En(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},oa.layout.pack=function(){function n(n,i){var a=e.call(this,n,i),o=a[0],l=u[0],c=u[1],s=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(o.x=o.y=0,au(o,function(n){n.r=+s(n.value)}),au(o,Nu),r){var f=r*(t?1:Math.max(2*o.r/l,2*o.r/c))/2;au(o,function(n){n.r+=f}),au(o,Nu),au(o,function(n){n.r-=f})}return Cu(o,l/2,c/2,t?1:1/Math.max(2*o.r/l,2*o.r/c)),a}var t,e=oa.layout.hierarchy().sort(_u),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},uu(n,e)},oa.layout.tree=function(){function n(n,u){var s=a.call(this,n,u),f=s[0],h=t(f);if(au(h,e),h.parent.m=-h.z,iu(h,r),c)iu(f,i);else{var g=f,p=f,v=f;iu(f,function(n){n.xp.x&&(p=n),n.depth>v.depth&&(v=n)});var d=o(g,p)/2-g.x,m=l[0]/(p.x+o(p,g)/2+d),y=l[1]/(v.depth||1);iu(f,function(n){n.x=(n.x+d)*m,n.y=n.depth*y})}return s}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var u,i=t.children,a=0,o=i.length;o>a;++a)r.push((i[a]=u={_:i[a],parent:t,children:(u=i[a].children)&&u.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:a}).a=u);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Du(n);var i=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+o(n._,r._),n.m=n.z-i):n.z=i}else r&&(n.z=r.z+o(n._,r._));n.parent.A=u(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function u(n,t,e){if(t){for(var r,u=n,i=n,a=t,l=u.parent.children[0],c=u.m,s=i.m,f=a.m,h=l.m;a=Tu(a),u=qu(u),a&&u;)l=qu(l),i=Tu(i),i.a=n,r=a.z+f-u.z-c+o(a._,u._),r>0&&(Ru(Pu(a,n,e),n,r),c+=r,s+=r),f+=a.m,c+=u.m,h+=l.m,s+=i.m;a&&!Tu(i)&&(i.t=a,i.m+=f-s),u&&!qu(l)&&(l.t=u,l.m+=c-h,e=n)}return e}function i(n){n.x*=l[0],n.y=n.depth*l[1]}var a=oa.layout.hierarchy().sort(null).value(null),o=Lu,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(o=t,n):o},n.size=function(t){return arguments.length?(c=null==(l=t)?i:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:i,n):c?l:null},uu(n,a)},oa.layout.cluster=function(){function n(n,i){var a,o=t.call(this,n,i),l=o[0],c=0;au(l,function(n){var t=n.children;t&&t.length?(n.x=ju(t),n.y=Uu(t)):(n.x=a?c+=e(n,a):0,n.y=0,a=n)});var s=Fu(l),f=Hu(l),h=s.x-e(s,f)/2,g=f.x+e(f,s)/2;return au(l,u?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),o}var t=oa.layout.hierarchy().sort(null).value(null),e=Lu,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},uu(n,t)},oa.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++ut?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var a,o,l,c=f(e),s=[],h=i.slice(),p=1/0,v="slice"===g?c.dx:"dice"===g?c.dy:"slice-dice"===g?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),s.area=0;(l=h.length)>0;)s.push(a=h[l-1]),s.area+=a.area,"squarify"!==g||(o=r(s,v))<=p?(h.pop(),p=o):(s.area-=s.pop().area,u(s,v,c,!1),v=Math.min(c.dx,c.dy),s.length=s.area=0,p=1/0);s.length&&(u(s,v,c,!0),s.length=s.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,a=f(t),o=r.slice(),l=[];for(n(o,a.dx*a.dy/t.value),l.area=0;i=o.pop();)l.push(i),l.area+=i.area,null!=i.z&&(u(l,i.z?a.dx:a.dy,a,!o.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,a=-1,o=n.length;++ae&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,a=n.length,o=e.x,c=e.y,s=t?l(n.area/t):0; -if(t==e.dx){for((r||s>e.dy)&&(s=e.dy);++ie.dx)&&(s=e.dx);++ie&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=oa.random.normal.apply(oa,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=oa.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},oa.scale={};var Sl={floor:y,ceil:y};oa.scale.linear=function(){return Wu([0,1],[0,1],Mr,!1)};var kl={s:1,g:1,p:1,r:1,e:1};oa.scale.log=function(){return ri(oa.scale.linear().domain([0,1]),10,!0,[1,10])};var Nl=oa.format(".0e"),El={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};oa.scale.pow=function(){return ui(oa.scale.linear(),1,[0,1])},oa.scale.sqrt=function(){return oa.scale.pow().exponent(.5)},oa.scale.ordinal=function(){return ai([],{t:"range",a:[[]]})},oa.scale.category10=function(){return oa.scale.ordinal().range(Al)},oa.scale.category20=function(){return oa.scale.ordinal().range(Cl)},oa.scale.category20b=function(){return oa.scale.ordinal().range(zl)},oa.scale.category20c=function(){return oa.scale.ordinal().range(Ll)};var Al=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Cl=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),zl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),Ll=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);oa.scale.quantile=function(){return oi([],[])},oa.scale.quantize=function(){return li(0,1,[0,1])},oa.scale.threshold=function(){return ci([.5],[0,1])},oa.scale.identity=function(){return si([0,1])},oa.svg={},oa.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),s=a.apply(this,arguments)-Oa,f=o.apply(this,arguments)-Oa,h=Math.abs(f-s),g=s>f?0:1;if(n>c&&(p=c,c=n,n=p),h>=Ha)return t(c,g)+(n?t(n,1-g):"")+"Z";var p,v,d,m,y,M,x,b,_,w,S,k,N=0,E=0,A=[];if((m=(+l.apply(this,arguments)||0)/2)&&(d=i===ql?Math.sqrt(n*n+c*c):+i.apply(this,arguments),g||(E*=-1),c&&(E=tn(d/c*Math.sin(m))),n&&(N=tn(d/n*Math.sin(m)))),c){y=c*Math.cos(s+E),M=c*Math.sin(s+E),x=c*Math.cos(f-E),b=c*Math.sin(f-E);var C=Math.abs(f-s-2*E)<=ja?0:1;if(E&&mi(y,M,x,b)===g^C){var z=(s+f)/2;y=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else y=M=0;if(n){_=n*Math.cos(f-N),w=n*Math.sin(f-N),S=n*Math.cos(s+N),k=n*Math.sin(s+N);var L=Math.abs(s-f+2*N)<=ja?0:1;if(N&&mi(_,w,S,k)===1-g^L){var q=(s+f)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Pa&&(p=Math.min(Math.abs(c-n)/2,+u.apply(this,arguments)))>.001){v=c>n^g?0:1;var T=p,R=p;if(ja>h){var D=null==S?[_,w]:null==x?[y,M]:Re([y,M],[S,k],[x,b],[_,w]),P=y-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(p,(n-O)/(H-1)),T=Math.min(p,(c-O)/(H+1))}if(null!=x){var I=yi(null==S?[_,w]:[S,k],[y,M],c,T,g),Y=yi([x,b],[_,w],c,T,g);p===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-g^mi(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",g," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",y,",",M);if(null!=S){var Z=yi([y,M],[S,k],n,-R,g),V=yi([_,w],null==x?[y,M]:[x,b],n,-R,g);p===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",g^mi(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-g," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",y,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",g," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-g," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hi,r=gi,u=fi,i=ql,a=pi,o=vi,l=di;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(u=En(t),n):u},n.padRadius=function(t){return arguments.length?(i=t==ql?ql:En(t),n):i},n.startAngle=function(t){return arguments.length?(a=En(t),n):a},n.endAngle=function(t){return arguments.length?(o=En(t),n):o},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+a.apply(this,arguments)+ +o.apply(this,arguments))/2-Oa;return[Math.cos(t)*n,Math.sin(t)*n]},n};var ql="auto";oa.svg.line=function(){return Mi(y)};var Tl=oa.map({linear:xi,"linear-closed":bi,step:_i,"step-before":wi,"step-after":Si,basis:zi,"basis-open":Li,"basis-closed":qi,bundle:Ti,cardinal:Ei,"cardinal-open":ki,"cardinal-closed":Ni,monotone:Fi});Tl.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Rl=[0,2/3,1/3,0],Dl=[0,1/3,2/3,0],Pl=[0,1/6,2/3,1/6];oa.svg.line.radial=function(){var n=Mi(Hi);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wi.reverse=Si,Si.reverse=wi,oa.svg.area=function(){return Oi(y)},oa.svg.area.radial=function(){var n=Oi(Hi);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},oa.svg.chord=function(){function n(n,o){var l=t(this,i,n,o),c=t(this,a,n,o);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?u(l.r,l.p1,l.r,l.p0):u(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+u(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=o.call(n,u,r),a=l.call(n,u,r)-Oa,s=c.call(n,u,r)-Oa;return{r:i,a0:a,a1:s,p0:[i*Math.cos(a),i*Math.sin(a)],p1:[i*Math.cos(s),i*Math.sin(s)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>ja)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=Me,a=xe,o=Ii,l=pi,c=vi;return n.radius=function(t){return arguments.length?(o=En(t),n):o},n.source=function(t){return arguments.length?(i=En(t),n):i},n.target=function(t){return arguments.length?(a=En(t),n):a},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},oa.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),a=e.call(this,n,u),o=(i.y+a.y)/2,l=[i,{x:i.x,y:o},{x:a.x,y:o},a];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yi;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},oa.svg.diagonal.radial=function(){var n=oa.svg.diagonal(),t=Yi,e=n.projection;return n.projection=function(n){return arguments.length?e(Zi(t=n)):t},n},oa.svg.symbol=function(){function n(n,r){return(Ul.get(t.call(this,n,r))||$i)(e.call(this,n,r))}var t=Xi,e=Vi;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Ul=oa.map({circle:$i,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Fl)),e=t*Fl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});oa.svg.symbolTypes=Ul.keys();var jl=Math.sqrt(3),Fl=Math.tan(30*Ia);Aa.transition=function(n){for(var t,e,r=Hl||++Zl,u=Ki(n),i=[],a=Ol||{time:Date.now(),ease:Nr,delay:0,duration:250},o=-1,l=this.length;++oi;i++){u.push(t=[]);for(var e=this[i],o=0,l=e.length;l>o;o++)(r=e[o])&&n.call(r,r.__data__,o,i)&&t.push(r)}return Wi(u,this.namespace,this.id)},Yl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(u){u[r][e].tween.set(n,t)})},Yl.attr=function(n,t){function e(){this.removeAttribute(o)}function r(){this.removeAttributeNS(o.space,o.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(o);return e!==n&&(t=a(e,n),function(n){this.setAttribute(o,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(o.space,o.local);return e!==n&&(t=a(e,n),function(n){this.setAttributeNS(o.space,o.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var a="transform"==n?$r:Mr,o=oa.ns.qualify(n);return Ji(this,"attr."+n,t,o.local?i:u)},Yl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=oa.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Yl.style=function(n,e,r){function u(){this.style.removeProperty(n)}function i(e){return null==e?u:(e+="",function(){var u,i=t(this).getComputedStyle(this,null).getPropertyValue(n);return i!==e&&(u=Mr(i,e),function(t){this.style.setProperty(n,u(t),r)})})}var a=arguments.length;if(3>a){if("string"!=typeof n){2>a&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ji(this,"style."+n,e,i)},Yl.styleTween=function(n,e,r){function u(u,i){var a=e.call(this,u,i,t(this).getComputedStyle(this,null).getPropertyValue(n));return a&&function(t){this.style.setProperty(n,a(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,u)},Yl.text=function(n){return Ji(this,"text",n,Gi)},Yl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Yl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=oa.ease.apply(oa,arguments)),Y(this,function(r){r[e][t].ease=n}))},Yl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,u,i){r[e][t].delay=+n.call(r,r.__data__,u,i)}:(n=+n,function(r){r[e][t].delay=n}))},Yl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,u,i){r[e][t].duration=Math.max(1,n.call(r,r.__data__,u,i))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Yl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var u=Ol,i=Hl;try{Hl=e,Y(this,function(t,u,i){Ol=t[r][e],n.call(t,t.__data__,u,i)})}finally{Ol=u,Hl=i}}else Y(this,function(u){var i=u[r][e];(i.event||(i.event=oa.dispatch("start","end","interrupt"))).on(n,t)});return this},Yl.transition=function(){for(var n,t,e,r,u=this.id,i=++Zl,a=this.namespace,o=[],l=0,c=this.length;c>l;l++){o.push(n=[]);for(var t=this[l],s=0,f=t.length;f>s;s++)(e=t[s])&&(r=e[a][u],Qi(e,s,a,i,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wi(o,a,i)},oa.svg.axis=function(){function n(n){n.each(function(){var n,c=oa.select(this),s=this.__chart__||e,f=this.__chart__=e.copy(),h=null==l?f.ticks?f.ticks.apply(f,o):f.domain():l,g=null==t?f.tickFormat?f.tickFormat.apply(f,o):y:t,p=c.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Pa),d=oa.transition(p.exit()).style("opacity",Pa).remove(),m=oa.transition(p.order()).style("opacity",1),M=Math.max(u,0)+a,x=Zu(f),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),oa.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=m.select("line"),C=p.select("text").text(g),z=v.select("text"),L=m.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=na,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*i+"V0H"+x[1]+"V"+q*i)):(n=ta,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*i+","+x[0]+"H0V"+x[1]+"H"+q*i)),E.attr(N,q*u),z.attr(k,q*M),A.attr(S,0).attr(N,q*u),L.attr(w,0).attr(k,q*M),f.rangeBand){var T=f,R=T.rangeBand()/2;s=f=function(n){return T(n)+R}}else s.rangeBand?s=f:d.call(n,f,s);v.call(n,s,f),m.call(n,f,f)})}var t,e=oa.scale.linear(),r=Vl,u=6,i=6,a=3,o=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Xl?t+"":Vl,n):r},n.ticks=function(){return arguments.length?(o=ca(arguments),n):o},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(a=+t,n):a},n.tickSubdivide=function(){return arguments.length&&n},n};var Vl="bottom",Xl={top:1,right:1,bottom:1,left:1};oa.svg.brush=function(){function n(t){t.each(function(){var t=oa.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",i).on("touchstart.brush",i),a=t.selectAll(".background").data([0]);a.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var o=t.selectAll(".resize").data(v,y);o.exit().remove(),o.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return $l[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),o.style("display",n.empty()?"none":null);var l,f=oa.transition(t),h=oa.transition(a);c&&(l=Zu(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(f)),s&&(l=Zu(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),u(f)),e(f)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+f[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",f[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",f[1]-f[0])}function u(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function i(){function i(){32==oa.event.keyCode&&(C||(M=null,L[0]-=f[1],L[1]-=h[1],C=2),S())}function v(){32==oa.event.keyCode&&2==C&&(L[0]+=f[1],L[1]+=h[1],C=0,S())}function d(){var n=oa.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(oa.event.altKey?(M||(M=[(f[0]+f[1])/2,(h[0]+h[1])/2]),L[0]=f[+(n[0]s?(u=r,r=s):u=s),v[0]!=r||v[1]!=u?(e?o=null:a=null,v[0]=r,v[1]=u,!0):void 0}function y(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),oa.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=oa.select(oa.event.target),w=l.of(b,arguments),k=oa.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&s,C=_.classed("extent"),z=W(b),L=oa.mouse(b),q=oa.select(t(b)).on("keydown.brush",i).on("keyup.brush",v);if(oa.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",y):q.on("mousemove.brush",d).on("mouseup.brush",y),k.interrupt().selectAll("*").interrupt(),C)L[0]=f[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[f[1-T]-L[0],h[1-R]-L[1]],L[0]=f[T],L[1]=h[R]}else oa.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),oa.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var a,o,l=N(n,"brushstart","brush","brushend"),c=null,s=null,f=[0,0],h=[0,0],g=!0,p=!0,v=Bl[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:f,y:h,i:a,j:o},e=this.__chart__||t;this.__chart__=t,Hl?oa.select(this).transition().each("start.brush",function(){a=e.i,o=e.j,f=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(f,t.x),r=xr(h,t.y);return a=o=null,function(u){f=t.x=e(u),h=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){a=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=Bl[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,v=Bl[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(g=!!t[0],p=!!t[1]):c?g=!!t:s&&(p=!!t),n):c&&s?[g,p]:c?g:s?p:null},n.extent=function(t){var e,r,u,i,l;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),a=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),(e!=f[0]||r!=f[1])&&(f=[e,r])),s&&(u=t[0],i=t[1],c&&(u=u[1],i=i[1]),o=[u,i],s.invert&&(u=s(u),i=s(i)),u>i&&(l=u,u=i,i=l),(u!=h[0]||i!=h[1])&&(h=[u,i])),n):(c&&(a?(e=a[0],r=a[1]):(e=f[0],r=f[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),s&&(o?(u=o[0],i=o[1]):(u=h[0],i=h[1],s.invert&&(u=s.invert(u),i=s.invert(i)),u>i&&(l=u,u=i,i=l))),c&&s?[[e,u],[r,i]]:c?[e,r]:s&&[u,i])},n.clear=function(){return n.empty()||(f=[0,0],h=[0,0],a=o=null),n},n.empty=function(){return!!c&&f[0]==f[1]||!!s&&h[0]==h[1]},oa.rebind(n,l,"on")};var $l={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Bl=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Wl=go.format=xo.timeFormat,Jl=Wl.utc,Gl=Jl("%Y-%m-%dT%H:%M:%S.%LZ");Wl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?ea:Gl,ea.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},ea.toString=Gl.toString,go.second=On(function(n){return new po(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),go.seconds=go.second.range,go.seconds.utc=go.second.utc.range,go.minute=On(function(n){return new po(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),go.minutes=go.minute.range,go.minutes.utc=go.minute.utc.range,go.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new po(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),go.hours=go.hour.range,go.hours.utc=go.hour.utc.range,go.month=On(function(n){return n=go.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),go.months=go.month.range,go.months.utc=go.month.utc.range;var Kl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Ql=[[go.second,1],[go.second,5],[go.second,15],[go.second,30],[go.minute,1],[go.minute,5],[go.minute,15],[go.minute,30],[go.hour,1],[go.hour,3],[go.hour,6],[go.hour,12],[go.day,1],[go.day,2],[go.week,1],[go.month,1],[go.month,3],[go.year,1]],nc=Wl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),tc={range:function(n,t,e){return oa.range(Math.ceil(n/e)*e,+t,e).map(ua)},floor:y,ceil:y};Ql.year=go.year,go.scale=function(){return ra(oa.scale.linear(),Ql,nc)};var ec=Ql.map(function(n){return[n[0].utc,n[1]]}),rc=Jl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);ec.year=go.year.utc,go.scale.utc=function(){return ra(oa.scale.linear(),ec,rc)},oa.text=An(function(n){return n.responseText}),oa.json=function(n,t){return Cn(n,"application/json",ia,t)},oa.html=function(n,t){return Cn(n,"text/html",aa,t)},oa.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=oa,define(oa)):"object"==typeof module&&module.exports?module.exports=oa:this.d3=oa}(); - -// Match width of graphs with summary bar -var responsiveWidth = function() { - return document.getElementsByClassName('summary_bar')[0].clientWidth - 30; -}; - -// Rickshaw 1.6.0 -(function(root,factory){if(typeof define==="function"&&define.amd){define(["d3"],function(d3){return root.Rickshaw=factory(d3)})}else if(typeof exports==="object"){module.exports=factory(require("d3"))}else{root.Rickshaw=factory(d3)}})(this,function(d3){var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i0){var x=s.data[0].x;var y=s.data[0].y;if(typeof x!="number"||typeof y!="number"&&y!==null){throw"x and y properties of points should be numbers instead of "+typeof x+" and "+typeof y}}if(s.data.length>=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.onConfigure=function(callback){this.configureCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){this.config=this.config||{};if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);Rickshaw.keys(this.config).forEach(function(k){this[k]=this.config[k]},this);if("stack"in args)args.unstack=!args.stack;var renderer=args.renderer||this.renderer&&this.renderer.name||"stack";this.setRenderer(renderer,args);this.configureCallbacks.forEach(function(callback){callback(args)})};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getUTCMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getUTCMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="month"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),date.getUTCMonth())/1e3;if(floor==time)return time;year=date.getUTCFullYear();var month=date.getUTCMonth();if(month==11){month=0;year=year+1}else{month+=1}return Date.UTC(year,month)/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),0)/1e3;if(floor==time)return time;year=date.getUTCFullYear()+1;return Date.UTC(year,0)/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year,offset;if(unit.name=="day"){var nearFuture=new Date((time+unit.seconds-1)*1e3);var rounded=new Date(0);rounded.setFullYear(nearFuture.getFullYear());rounded.setMonth(nearFuture.getMonth());rounded.setDate(nearFuture.getDate());rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);return rounded.getTime()/1e3}if(unit.name=="month"){date=new Date(time*1e3);floor=new Date(date.getFullYear(),date.getMonth()).getTime()/1e3;if(floor==time)return time;year=date.getFullYear();var month=date.getMonth();if(month==11){month=0;year=year+1}else{month+=1}return new Date(year,month).getTime()/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=new Date(date.getUTCFullYear(),0).getTime()/1e3;if(floor==time)return time;year=date.getFullYear()+1;return new Date(year,0).getTime()/1e3}offset=new Date(time*1e3).getTimezoneOffset()*60;return Math.ceil((time-offset)/unit.seconds)*unit.seconds+offset}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this._renderWidth!==undefined&&this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var bar_offset=this.graph.renderer.name=="bar"&&Math.ceil(this.graph.width*.95/this.graph.series[0].data.length/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+(berth+bar_offset)+","+yOffset+")"}else{transform="translate("+(berth+bar_offset)+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-x-value",this.textContent)});this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this._renderHeight!==undefined&&this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){ -var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-y-value",this.textContent)})}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var domain=this.scale.domain();var renderDomain=this.graph.renderer.domain().y;var extents=[Math.min.apply(Math,domain),Math.max.apply(Math,domain)];var extentMap=d3.scale.linear().domain([0,1]).range(extents);var adjExtents=[extentMap(renderDomain[0]),extentMap(renderDomain[1])];var adjustment=d3.scale.linear().domain(extents).range(adjExtents);var adjustedScale=this.scale.copy().domain(domain.map(adjustment)).range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line){if(l===line){if(self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.indexOf(line.series);line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.jQuery=="undefined"){throw"couldn't find jQuery at window.jQuery"}if(typeof window.jQuery.ui=="undefined"){throw"couldn't find jQuery UI at window.jQuery.ui"}jQuery(function(){jQuery(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];jQuery(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});jQuery(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}self.graph.update()}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)0){alignables.forEach(function(el){el.classList.remove("left");el.classList.add("right")});var rightAlignError=this._calcLayoutError(alignables);if(rightAlignError>leftAlignError){alignables.forEach(function(el){el.classList.remove("right");el.classList.add("left")})}}if(typeof this.onRender=="function"){this.onRender(args)}},_calcLayoutError:function(alignables){var parentRect=this.element.parentNode.getBoundingClientRect();var error=0;var alignRight=alignables.forEach(function(el){var rect=el.getBoundingClientRect();if(!rect.width){return}if(rect.right>parentRect.right){error+=rect.right-parentRect.right}if(rect.left=self.previewWidth){frameAfterDrag[0]-=frameAfterDrag[1]-self.previewWidth;frameAfterDrag[1]=self.previewWidth}}self.graphs.forEach(function(graph){var domainScale=d3.scale.linear().interpolate(d3.interpolateNumber).domain([0,self.previewWidth]).range(graph.dataDomain());var windowAfterDrag=[domainScale(frameAfterDrag[0]),domainScale(frameAfterDrag[1])];self.slideCallbacks.forEach(function(callback){callback(graph,windowAfterDrag[0],windowAfterDrag[1])});if(frameAfterDrag[0]===0){windowAfterDrag[0]=undefined}if(frameAfterDrag[1]===self.previewWidth){windowAfterDrag[1]=undefined}graph.window.xMin=windowAfterDrag[0];graph.window.xMax=windowAfterDrag[1];graph.update()})}function onMousedown(){drag.target=d3.event.target;drag.start=self._getClientXFromEvent(d3.event,drag);self.frameBeforeDrag=self.currentFrame.slice();d3.event.preventDefault?d3.event.preventDefault():d3.event.returnValue=false;d3.select(document).on("mousemove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("mouseup.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchmove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("touchend.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",onMouseup)}function onMousedownLeftHandle(datum,index){drag.left=true;onMousedown()}function onMousedownRightHandle(datum,index){drag.right=true;onMousedown()}function onMousedownMiddleHandle(datum,index){drag.left=true;drag.right=true;drag.rigid=true;onMousedown()}function onMouseup(datum,index){d3.select(document).on("mousemove.rickshaw_range_slider_preview",null);d3.select(document).on("mouseup.rickshaw_range_slider_preview",null);d3.select(document).on("touchmove.rickshaw_range_slider_preview",null);d3.select(document).on("touchend.rickshaw_range_slider_preview",null);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",null);delete self.frameBeforeDrag;drag.left=false;drag.right=false;drag.rigid=false}element.select("rect.left_handle").on("mousedown",onMousedownLeftHandle);element.select("rect.right_handle").on("mousedown",onMousedownRightHandle);element.select("rect.middle_handle").on("mousedown",onMousedownMiddleHandle);element.select("rect.left_handle").on("touchstart",onMousedownLeftHandle);element.select("rect.right_handle").on("touchstart",onMousedownRightHandle);element.select("rect.middle_handle").on("touchstart",onMousedownMiddleHandle)},_getClientXFromEvent:function(event,drag){switch(event.type){case"touchstart":case"touchmove":var touchList=event.changedTouches;var touch=null;for(var touchIndex=0;touchIndexyMax)yMax=y});if(!series.length)return;if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var pathNodes=vis.selectAll("path.path").data(data).enter().append("svg:path").classed("path",true).attr("d",this.seriesPathFactory());if(this.stroke){var strokeNodes=vis.selectAll("path.stroke").data(data).enter().append("svg:path").classed("stroke",true).attr("d",this.seriesStrokeFactory())}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=pathNodes[0][i];if(this.stroke)series.stroke=strokeNodes[0][i];this._styleSeries(series);i++},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";var strokeWidth=series.strokeWidth?series.strokeWidth:this.strokeWidth;var opacity=series.opacity?series.opacity:this.opacity;series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",strokeWidth);series.path.setAttribute("opacity",opacity);if(series.className){d3.select(series.path).classed(series.className,true)}if(series.className&&this.stroke){d3.select(series.stroke).classed(series.className,true)}},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false,opacity:1});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x.magnitude(frequentInterval.magnitude)*(1-this.gapSize);return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series); -var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("opacity",series.opacity).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}});return Rickshaw}); - -Sidekiq = {}; - -var nf = new Intl.NumberFormat(); -var poller; -var realtimeGraph = function(updatePath) { - var timeInterval = parseInt(localStorage.sidekiqTimeInterval) || 5000; - var graphElement = document.getElementById("realtime"); - - var graph = new Rickshaw.Graph( { - element: graphElement, - width: responsiveWidth(), - height: 200, - renderer: 'line', - interpolation: 'linear', - - series: new Rickshaw.Series.FixedDuration([{ name: graphElement.dataset.failedLabel, color: '#af0014' }, { name: graphElement.dataset.processedLabel, color: '#006f68' }], undefined, { - timeInterval: timeInterval, - maxDataPoints: 100, - }) - }); - - var y_axis = new Rickshaw.Graph.Axis.Y( { - graph: graph, - tickFormat: Rickshaw.Fixtures.Number.formatKMBT, - ticksTreatment: 'glow' - }); - - graph.render(); - - var legend = document.getElementById('realtime-legend'); - var Hover = Rickshaw.Class.create(Rickshaw.Graph.HoverDetail, { - render: function(args) { - legend.innerHTML = ""; - - var timestamp = document.createElement('div'); - timestamp.className = 'timestamp'; - timestamp.innerHTML = args.formattedXValue; - legend.appendChild(timestamp); - - args.detail.sort(function(a, b) { return a.order - b.order }).forEach( function(d) { - var line = document.createElement('div'); - line.className = 'line'; - - var swatch = document.createElement('div'); - swatch.className = 'swatch'; - swatch.style.backgroundColor = d.series.color; - - var label = document.createElement('div'); - label.className = 'tag'; - label.innerHTML = d.name + ": " + nf.format(Math.floor(d.formattedYValue)); - - line.appendChild(swatch); - line.appendChild(label); - legend.appendChild(line); - - var dot = document.createElement('div'); - dot.className = 'dot'; - dot.style.top = graph.y(d.value.y0 + d.value.y) + 'px'; - dot.style.borderColor = d.series.color; - - this.element.appendChild(dot); - dot.className = 'dot active'; - this.show(); - }, this ); - } - }); - var hover = new Hover( { graph: graph } ); - - var i = 0; - poller = setInterval(function() { - var url = document.getElementById("history").getAttribute("data-update-url"); - - fetch(url).then(response => response.json()).then(data => { - if (i === 0) { - var processed = data.sidekiq.processed; - var failed = data.sidekiq.failed; - } else { - var processed = data.sidekiq.processed - Sidekiq.processed; - var failed = data.sidekiq.failed - Sidekiq.failed; - } - - dataPoint = {}; - dataPoint[graphElement.dataset.failedLabel] = failed; - dataPoint[graphElement.dataset.processedLabel] = processed; - - graph.series.addData(dataPoint); - graph.render(); - - Sidekiq.processed = data.sidekiq.processed; - Sidekiq.failed = data.sidekiq.failed; - - updateStatsSummary(data.sidekiq); - updateRedisStats(data.redis); - updateFooterUTCTime(data.server_utc_time) - - pulseBeacon(); - }); - - i++; - }, timeInterval); -} - -var historyGraph = function() { - var h = document.getElementById("history"); - processed = createSeries(h.getAttribute("data-processed")); - failed = createSeries(h.getAttribute("data-failed")); - - var graphElement = h; - var graph = new Rickshaw.Graph( { - element: graphElement, - width: responsiveWidth(), - height: 200, - renderer: 'line', - interpolation: 'linear', - series: [ - { - color: "#af0014", - data: failed, - name: graphElement.dataset.failedLabel - }, { - color: "#006f68", - data: processed, - name: graphElement.dataset.processedLabel - } - ] - } ); - var x_axis = new Rickshaw.Graph.Axis.Time( { graph: graph } ); - var y_axis = new Rickshaw.Graph.Axis.Y({ - graph: graph, - tickFormat: Rickshaw.Fixtures.Number.formatKMBT, - ticksTreatment: 'glow' - }); - - graph.render(); - - var legend = document.getElementById('history-legend'); - var Hover = Rickshaw.Class.create(Rickshaw.Graph.HoverDetail, { - render: function(args) { - legend.innerHTML = ""; - - var timestamp = document.createElement('div'); - timestamp.className = 'timestamp'; - timestamp.innerHTML = args.formattedXValue; - legend.appendChild(timestamp); - - args.detail.sort(function(a, b) { return a.order - b.order }).forEach( function(d) { - var line = document.createElement('div'); - line.className = 'line'; - - var swatch = document.createElement('div'); - swatch.className = 'swatch'; - swatch.style.backgroundColor = d.series.color; - - var label = document.createElement('div'); - label.className = 'tag'; - label.innerHTML = d.name + ": " + nf.format(Math.floor(d.formattedYValue)); - - line.appendChild(swatch); - line.appendChild(label); - legend.appendChild(line); - - var dot = document.createElement('div'); - dot.className = 'dot'; - dot.style.top = graph.y(d.value.y0 + d.value.y) + 'px'; - dot.style.borderColor = d.series.color; - - this.element.appendChild(dot); - dot.className = 'dot active'; - this.show(); - }, this ); - } - }); - var hover = new Hover( { graph: graph } ); -} - -var createSeries = function(data) { - var obj = JSON.parse(data); - var series = []; - for (var date in obj) { - var value = obj[date]; - var point = { x: Date.parse(date)/1000, y: value }; - series.unshift(point); - } - return series; -}; - -var updateStatsSummary = function(data) { - document.getElementById("txtProcessed").innerText = nf.format(data.processed); - document.getElementById("txtFailed").innerText = nf.format(data.failed); - document.getElementById("txtBusy").innerText = nf.format(data.busy); - document.getElementById("txtScheduled").innerText = nf.format(data.scheduled); - document.getElementById("txtRetries").innerText = nf.format(data.retries); - document.getElementById("txtEnqueued").innerText = nf.format(data.enqueued); - document.getElementById("txtDead").innerText = nf.format(data.dead); -} - -var updateRedisStats = function(data) { - document.getElementById('redis_version').innerText = data.redis_version; - document.getElementById('uptime_in_days').innerText = data.uptime_in_days; - document.getElementById('connected_clients').innerText = data.connected_clients; - document.getElementById('used_memory_human').innerText = data.used_memory_human; - document.getElementById('used_memory_peak_human').innerText = data.used_memory_peak_human; -} - -var updateFooterUTCTime = function(time) { - document.getElementById('serverUtcTime').innerText = time; -} - -var pulseBeacon = function() { - document.getElementById('beacon').classList.add('pulse'); - window.setTimeout(() => { document.getElementById('beacon').classList.remove('pulse'); }, 1000); -} - -// Render graphs -var renderGraphs = function() { - realtimeGraph(); - historyGraph(); -}; - -var setSliderLabel = function(val) { - document.getElementById('sldr-text').innerText = Math.round(parseFloat(val) / 1000) + ' sec'; -} - -var ready = (callback) => { - if (document.readyState != "loading") callback(); - else document.addEventListener("DOMContentLoaded", callback); -} - -ready(() => { - renderGraphs(); - - var sldr = document.getElementById('sldr'); - if (typeof localStorage.sidekiqTimeInterval !== 'undefined') { - sldr.value = localStorage.sidekiqTimeInterval; - setSliderLabel(localStorage.sidekiqTimeInterval); - } - - sldr.addEventListener("change", event => { - clearInterval(poller); - localStorage.sidekiqTimeInterval = sldr.value; - setSliderLabel(sldr.value); - resetGraphs(); - renderGraphs(); - }); - - sldr.addEventListener("mousemove", event => { - setSliderLabel(sldr.value); - }); -}); - -// Reset graphs -var resetGraphs = function() { - document.getElementById('realtime').innerHTML = ''; - document.getElementById('history').innerHTML = ''; -}; - -// Resize graphs after resizing window -var debounce = function(fn, timeout) { - var timeoutID = -1; - return function() { - if (timeoutID > -1) { - window.clearTimeout(timeoutID); - } - timeoutID = window.setTimeout(fn, timeout); - } -}; - -window.onresize = function() { - var prevWidth = window.innerWidth; - return debounce(function () { - var currWidth = window.innerWidth; - if (prevWidth !== currWidth) { - prevWidth = currWidth; - clearInterval(poller); - resetGraphs(); - renderGraphs(); - } - }, 125); -}(); diff --git a/web/assets/stylesheets/application-dark.css b/web/assets/stylesheets/application-dark.css deleted file mode 100644 index 65e27948..00000000 --- a/web/assets/stylesheets/application-dark.css +++ /dev/null @@ -1,143 +0,0 @@ -html, body { - background-color: #171717 !important; - color: #DEDEDE; -} - -a, -.title, -.summary_bar ul .count, -span.current-interval, -.navbar .navbar-brand { - color: #d04; -} - -.history-graph.active, -.beacon .dot { - background-color: #d04; -} - -.navbar .navbar-brand:hover { - color: #ddd; -} - -.navbar .navbar-brand .status { - color: #ddd; -} - -.navbar-default .navbar-nav > li > a { - color: #ddd; -} - -.navbar-inverse { - background-color: #222; - border-color: #444; -} - -table { - background-color: #1D1D1D; -} - -.table-striped > tbody > tr:nth-of-type(odd) { - background-color: #2E2E2E; -} - -.table-bordered, -.table-bordered > tbody > tr > td, -.table-bordered > tbody > tr > th, -.table-bordered > tfoot > tr > td, -.table-bordered > tfoot > tr > th, -.table-bordered > thead > tr > td, -.table-bordered > thead > tr > th { - border: 1px solid #444; -} - -.table-hover > tbody > tr:hover { - background-color: #444; -} - -.alert { - border: none; - color: #ddd; -} - -.alert-success { - background-color: #484; -} - -.alert-danger { - background-color: #980035; -} - -.alert-info { - background-color: #31708f; -} - -a:link, a:active, a:hover, a:visited { - color: #ddd; -} - -input { - background-color: #444; - color: #ccc; - padding: 3px; -} - -.summary_bar .summary { - background-color: #232323; - border: 1px solid #444; -} - -.navbar-default { - background-color: #0F0F0F; - border-color: #444; -} - -.navbar-default .navbar-nav > .active > a, -.navbar-default .navbar-nav > .active > a:focus, -.navbar-default .navbar-nav > .active > a:hover { - color: #ddd; - background-color: #333; -} - -.navbar-default .navbar-nav > li > a:hover { - color: #ddd; -} - -.pagination > li > a, -.pagination > li > a:hover, -.pagination > li > span { - color: #ddd; - background-color: #333; - border-color: #444; -} -.pagination > .disabled > a, -.pagination > .disabled > a:focus, -.pagination > .disabled > a:hover, -.pagination > .disabled > span, -.pagination > .disabled > span:focus, -.pagination > .disabled > span:hover { - color: #ddd; - background-color: #333; - border-color: #444; -} - -.stat { - border: 1px solid #888; -} - -.rickshaw_graph .detail { - background: #888; -} -.rickshaw_graph .x_tick { - border-color: #888; -} - -.rickshaw_graph .y_ticks.glow text { - fill: #ddd; - color: #ddd; -} - -.info-circle { - color: #222; - background-color: #888; -} diff --git a/web/assets/stylesheets/application-rtl.css b/web/assets/stylesheets/application-rtl.css deleted file mode 100644 index d26f5e54..00000000 --- a/web/assets/stylesheets/application-rtl.css +++ /dev/null @@ -1,242 +0,0 @@ -.navbar-right { - float: left !important; -} - -footer .edits { - margin-right: unset; - margin-left: 40px; -} - -.summary_bar .status { - margin-left: unset; - margin-right: 10px; -} -@media (max-width: 767px) and (min-width: 400px) { - .summary_bar ul .desc { - text-align: right; - } - .summary_bar ul .count { - text-align: left; - } -} -@media (max-width: 979px) and (min-width: 768px) { -} -.summary_bar ul .count { - float: left; -} - -form .btn { - margin-right: unset; - margin-left: 5px; -} - -form .btn-group .btn { - margin-right: unset; - margin-left: 1px; -} - -@media (max-width: 768px) { - .navbar.navbar-fixed-top ul { - margin-right: unset; - margin-left: -15px!important; - } -} - -@media (width: 768px) { - .navbar .poll-wrapper { - margin: 4px 0 0 4px; - } -} - -.navbar-footer .navbar ul.nav a.navbar-brand { - padding: 0 0 0 15px; -} - -.navbar-fixed-bottom li { - margin-right: unset; - margin-left: 20px; -} - -.status-active { - background-position: 100% 0; -} - -.status-idle { - background-position: 100% -54px; -} - -.stat { - float: right; - margin-right: unset; - margin-left: 20px; -} - -.stat:last-child { - margin-left: 0; -} - -@media (max-width: 767px) { - .stat { - float: right; - margin-right: unset; - margin-left: 10px; - text-align: right; - } - .stat h3{ - float: left; - margin: 5px 5px 5px 10px; - } - .stat p{ - font-size: 1em; - margin: 5px 10px 5px 5px; - } -} - -/* Dashboard -********************************** */ -div.dashboard h3 { - float: right; -} - -div.interval-slider { - float: left; -} - -#realtime-legend, -#history-legend { - text-align: right; - float: left; -} -#realtime-legend .timestamp, -#history-legend .timestamp { - text-align: left; -} -#realtime-legend .line, -#history-legend .line { - margin: 0 20px 0 0; -} -#realtime-legend .swatch, -#history-legend .swatch { - margin: 0 0 0 8px; -} - -/* Beacon -********************************** */ - -.beacon .dot, -.beacon .ring { - right: 50%; -} - -.beacon .dot { - margin: -5px -5px 0 0; -} - -.beacon .ring { - margin: -14px -14px 0 0; -} - -.history-heading { - padding-right: unset; - padding-left: 15px; -} - -@media (max-width: 767px) { - .navbar.navbar-fixed-top ul { - margin-left: 0; - } - - .navbar.navbar-fixed-top li { - margin-left: 0; - } - - .navbar.navbar-fixed-bottom ul { - margin-left: 0; - } - - .navbar.navbar-fixed-bottom li { - margin-left: 0; - } -} - -@media (max-width: 500px) { - .navbar-footer .navbar ul.nav a.navbar-brand { - padding-right: unset; - padding-left: 5px; - } -} - -/* Rickshaw */ - -.rickshaw_graph .detail .x_label.left { - right: 0 -} -.rickshaw_graph .detail .x_label.right { - left: 0 -} -.rickshaw_graph .detail .item.left { - right: 0 -} -.rickshaw_graph .detail .item.right { - left: 0 -} -.rickshaw_graph .detail .item.left:after { - left: 0; - right: -5px; - border-right-color: unset; - border-left-color: rgba(0, 0, 0, .8); - border-right-width: 0; - border-left-width: unset; -} -.rickshaw_graph .detail .item.right:after { - right: 0; - left: -5px; - border-left-color: unset; - border-right-color: rgba(0, 0, 0, .8); - border-left-width: 0; - border-right-width: unset; -} -.rickshaw_graph .detail .dot { - margin-right: -3px; - margin-left: unset; -} -.rickshaw_graph .x_tick { - border-left: unset; - border-right: 1px dotted rgba(0, 0, 0, .2); -} -.rickshaw_graph .x_tick .title { - margin-right: 3px; - margin-left: unset; -} -.rickshaw_annotation_timeline .annotation { - margin-right: -2px; - margin-left: unset; -} -.rickshaw_graph .annotation_line { - border-right: 2px solid rgba(0, 0, 0, .3); - border-left: unset; -} -.rickshaw_annotation_timeline .annotation .content { - left: unset; - right: -11px; -} -.rickshaw_graph .x_tick.glow .title, -.rickshaw_graph .y_ticks.glow text { - text-shadow: 1px 1px 0 rgba(255, 255, 255, .1), -1px -1px 0 rgba(255, 255, 255, .1), -1px 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1), 0 -1px 0 rgba(255, 255, 255, .1), -1px 0 0 rgba(255, 255, 255, .1), 1px 0 0 rgba(255, 255, 255, .1), 1px -1px 0 rgba(255, 255, 255, .1) -} -.rickshaw_graph .x_tick.inverse .title, -.rickshaw_graph .y_ticks.inverse text { - text-shadow: 1px 1px 0 rgba(0, 0, 0, .8), -1px -1px 0 rgba(0, 0, 0, .8), -1px 1px 0 rgba(0, 0, 0, .8), 0 1px 0 rgba(0, 0, 0, .8), 0 -1px 0 rgba(0, 0, 0, .8), -1px 0 0 rgba(0, 0, 0, .8), 1px 0 0 rgba(0, 0, 0, .8), 1px -1px 0 rgba(0, 0, 0, .8) -} -.rickshaw_legend .line { - padding-left: 15px; - padding-right: unset; -} -.rickshaw_legend .line .swatch { - margin-left: 3px; - margin-right: unset; -} -.rickshaw_legend .action { - margin-left: .2em; - margin-right: unset; -} diff --git a/web/assets/stylesheets/application.css b/web/assets/stylesheets/application.css deleted file mode 100644 index 7a3d20f1..00000000 --- a/web/assets/stylesheets/application.css +++ /dev/null @@ -1,957 +0,0 @@ -* { - box-sizing: border-box; -} - -body { - color: #585454; - padding: 0; - text-rendering: optimizeLegibility; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - background: #f3f3f3 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAgAElEQVR4nK19d0CO3xv3x+04jsfjEbL3KnvvkZ2s7D1LpEIosyRRVkhKiCghIaSIlmRnj6zsvbM37x/n+71v53vfz6Pf+77nr3uccZ3rXOec61zrAAAKLLCjl1vtkACg7gsLcv7WLob/pKfn50l/vlf7+V54d13oovtvmT9Tge4rpUerbPTVL21jBx1nquofPuU91SqX0P6E4d/nkilL5DaHHrvFJib1J3/m9YrbpTcFw9xci2jPqQNVcC7dGUIBoNrEVAIA4R2ume2+O0/Id87jjQxz5aVWMqyTVwxR9QUAki45SVrfn5S4LPW5Po4BwEnvvJp5NNON3o102Y5dVAXGmwdoIg4AKsZls4HzPTX/z75GjZb7v03vP84XBuTAyhUspZM3ybtqpxSyPE3/5tUcGf6KM1fKeb/6GAgAtCcZNGzAEQNymEoXNKcAYHvqOnu5qjur6BJP5sXuZXvs3QyjviwWYDEflk/y2nKeAkDFCGsCAM86rtEcPCGVMThKvhtuFQaAUd3C6Y+bNeRCkTN7k//mT09+K1QamjRfQPSVcrkYAMzo30fO5+MeIRW7kiIBQMcuv2QkWf3IkI407q9qw1hqXrGjqkNnmuURvi2fskiA59n4g+SQ3UMCACPLQWjLr7GNMKPa6g0MALLbvJSkzo6ayHM4YU3H1cxLASD0bAydHNpGbq/Sq8sUAH6u7qbqU9eew+jmEfslACgfwGfkqOuOcr4a7aLFMllJ7w0lJyfK0/R0SYNRii7pBQYADTvsIQBQu0dzk9R/5ewgBgCv974yivwUx0w6tzxjG7JOkG3vJbKhbRjL/7oJ89pcVHNar2eguZ4d12xXN3+43E6hMHcGAF+3/1K1XXHbFaHuo+HRcn324yfJ+c1citHO/WayyFEFyLgNGXKZXh5HhUF7tbaFqo0xi54rMDbbzQBgRPXJMp5Tlo5R98/5UrrRqeRdbRwln9dIAPAmo93fp9x/0vKq7mZpz3JrIu7n3Xo5qs/3TIhhkX8vtnJ4oBkA7HMaKwEA8T0iIGDU5lVmANDSz5IG1mghU3/C8mQDAOS1Lsb0twsTAPjYY7rRPe+FR4S8jN3o4MB+lq6R4+W2+dbcAoI7Lp9DVm5x0l+9UU+uw7pQKAGAyfk9pOMzKbv1czkxzEwT97/wk75Ud6yJBABDx/Yxe1lmCAGAIju60uQdnJI2xnJEOJk10M2ZX05uoPz4D/Jz/hW84Y8378nIPu17g80d2UoCAF11vXQbg2Wg3QMqG+3s7bcxKor76L2cPA0aKtcd2KmZlLjkPgWA/H6H2YHJNU0O8qUz2zXbu13bXZevV3lyb9Io/ZyJVxgA1PLMpjUeZ0uTJvYQkDywwjwp6EVX+du393XoqU3jyMnUZKMrQMVOthQAilWboGp/kM1kk8yIkHz9y6imVB8XM7nho8UXyP+7RPYkiTMLMACY8iOJAMBs90tmEW9Lspgqy1TAeh4eSQFg957BOd5HAABm21RI3/D4kBkARLqXkim/WeFHpGnfiyY37Zg1TeT8d9dOk59TPWMZALgHD5GR5RxqpzmYZexthO8x7eN1R9Zt1QPApMIlhb5d/Wmec+ZmwrZHFADsylkLg9CvVCABgLY18pAqWctyXGGQlCEAM7/gE3Y7T10BmbMPXWCb/AuTuX3OspB9MXoA2FNJkgCg3Rdfo9Re6aCTtDhqvklYRoxbRWOOLNT7ZNjQn31qsNtxyQwAJtc9KNTbqLUfBYCacXzfrFnfR4a74Zhfct6jy04LlLxyf4pm+79+ck7z6ZHNVAq9Ipd//ZEzRH5XWrDECWfl72njavx96W7st5uObNdQRbljN4hcTVS1VRIAbN3EZOD6fCPCgD5485X5JT3JEc895gTfxEOX3pDri3L9brSsRxdbAgALetmqOpXn1F653Ob5VqploX1YCynxRQo7OGa6GQA0i8pgJPdGal0rWD/xsDb7fte2JXEfV1wPAI8eLZLxc71iUbl952bNdZHX+tGGjrvk2dm7cCCtVPYdA4CX7zqxWbPt2LaayTJ8jnvyGV8lbg3oYXIJWTUkohQA5N7aiq3oPoABQG2L2aTgkiLS7c/fyZD64uYflHFV7lyr71VlIO5cNGM+Fufpucp1hPzWkZDfb5T/oEk94z/MlussUXqhtGZWf7M//yck7tMsl//LUNY0+pJ0+ulTAeELPxaT++zYorVJ4imQmUb36r00cbT8+3cCAKe8xrHRUy4wAAgu9oVOnKDsm9ddo4nUqLxQ/kHwb/6/xYiRMuAks4nRqeNWvo/q35qiS1WAZ+O90FDbLXPk95+dlZlzpzynlgG+1eX/0U23EgCYtqeY9PSBK0sZrD0Ykbf6seTpyRQAWlxW2nuca66Kqjfo0+T/BwPOyP8Xt+0uz5iq91cyANi7bzXLP/qaAP+zWHeTg3P8XoRMCKs8nSkALL1TSAKAVjN6S1YOmzVnmuPiDnL7rpOtlTzrrIpLAFDj/WXNEa9Sr7aqwisWP6SGhVLZWd1XAWFHXcYp620PzlVUbtOc+Huelxt3WXiDAIB7M4O4v6TfoI1dJ1MAqJ1yR/53/msmA4COvSvo9WdDSYX6Ddj0N4tyxJWUWx0gIzPtejrx+3Zc1ce2z8I1EV7B5jlba3FA6PuUAs1Mrhw9LJRZs25NJWn+gSp0RvJiFf4SgzvRURs75UxCsLPkTZK4YxBJeDFDBnRyHjWrOCp2BQWA3kt+EwB4+TgfBQCPtT+MzrLPJ5yFDtVa/J79yrouf0vSm96kAWBiSgsZrp1x4/XXdOUlh9YZJhF1dO8Sud5HvXtLAPAqb25663kzYTCs6zvLsK+aUEH6fU+X43PXuZnOmjDEXuxBbsT4GO3XieU/5XKzFrtrt3d7pp0eADqXf0MB4N1FJyJdui9dfsM3sZh2j1gzEq5q5CuO8f3ks8TK2RzVpLwCp4rTAxfH0vxB4mExuyVR5S9aeKZmRwKr3zUpyDSWMoIemxy4p/aVZYQE5WkvTXVaSgFg2bv5AqJuZr2Q62nx04o8qNbYJCFNDc1FvHyT5Dq6xzwQ8nunKJwYzjU7KwHAutZcrDHG44cEABm+8Yb2y6cLgHiF+9F9p2vrAOBiiU4EAMYNW0faFGopdPRHZG253PfAV+wkW6k5OJ5RCmdRstxm4jzAjjXM1ZICQIlGI+R/31Y/kTsQrXsiwNTn60kCAP4RM4VBulSlEgWAzT3vUgDodfNUjtn1FndusJj8pSgATD5WUWjvSGwho/Xs+PXc6H5zaMl8YjOju/z/dZW1BAC8OxSgtvdrSy/3uSg47H5ugLAmN+tWlY5yWCptfv1Fog+7awIQ3PWBAGi7eh50s3k5Vji7o4oKp1pb8kH3zCeXGbo/iV0btInu7r5Mn3jZSxo6VumMVa/lqqnbzVx9sOwz5o6AgPQ8IcJ7oPVCozPC7+5P3sYXVwoArcJi6LoGQygAzI3eYRY8+IswwIstFtDYu7EquGK/XZd6Br3Rvargy0oMziMlHE7Rz1xdUAIAu1+KyKVJ7lTpyuUqAi6Lnagi11fv+x99uXiO8YNY7aakzoDH1PHZU/rcozu9uM9Zc/Ns7d1Frvjs6B/yc/zhKqrBmzB8+f92CtdIy7I4J2ZzwDnHovLtg14KsDg1LSO/J+XSMwAIr31QYmlWrE+vMLolq4rZs8fZCsLnRchwL8hVz2gf5s4fr7/qnE9G5vrzS+U6ZjT11J9ymCT/a1HFVrXUjpzbhlq/26QMhoMLz5RyZyUrPeK4ruuxDQIVVHPdIg9KSDWuX9h98aZAibSOqwQA50OX6QAg4PUtueH0FuJBsvXjIcxv6lSyMXwnBYBd/8xA67F3JJS7ywDgZpyOZaWcIjfDOIcilXGT2zMkVSMAEFzIUUD4wTNrpdLNi6sQ59PG//+ZIP6XZFH+rgQA+5s9pgDgU8lXAoDSW2KkXXuyCAAkbpAIAAy9ZFy3JKcV/lPMPOrXIot/OgtI75zST0Cst/dSM/wnna62R+78PNurZsmPuGT1h+dvCgDx5W+ZBOCAo4Vcvl5xvu9czPgkt5vew1aGaUR+a9oxUz0j/0wnnCYZXdMBILR7kFx3n0CdTHjJtcNYn16xctnEJC+z2GkvpYw3l6QiP27QWG9/Vb37D1VkjKwxOvi5K9aUevgnUQA4cvYo50yX/pDeOJxUpMB7HRkmPRymAwCrpclsUqa9btK+6ZwqO05WrZd3F1elN1vySgv6JBrtbOW0yop27lYUTTsgAlpohHpgHqRba3amV3aySaQWGMB1ElMcCxIAOHInRFpuHaaCvVfgADazSznpxC8u6gnN3vFXlrbmaGchj0vqCXbVzZ2MHlbFAABdK/2WXAPVB+bAfpZy//LmWm10kJxqThLwYG/VRcSL2ZbfdMSVEKGBS6u363te0f112o/+EEpu+k+V87VyvG2yTHzn0Wzx4mqaSGnfZYi0d90QCQBmXuYDvzdKUW6FjGlPH9X/nSOuqc/qAN2904UlX6/kv7LKs+K5PqXuOH626mR1Tp41Q3OVok3rT9U1eVNQBfPK1YqQ8fOAcGrwdteEzaFuT2UWRi+UCS3XPmsGALmGdiXoUPwJvXymvpxxu3t5FUXOD/NlTdd75Fxm//8hTbLtJzk8HC0BwIzM2ToAqGGoyfbXf8BKDj8vwDjJLkZAQPjQTPl/m8bLSNL7RgJxXM4sQvtWqEqqxDfTHKR2sco5427wfbku61NFmf+aHsKA5Ns1Um57R9m+EgC0nlBJLr+6/FO6o3BZqfenTGmPWVsydjk/09DqH0lEYENaKrKYXP5UUi5e7kW4WqnTv4EyK35O0auoPWZehLCHrKn2W8rO3CYgKq+1RY6ouNpuRbHzfO4bXVCH7dKCdt1Zdv1U+m3KbgkAfCd/ZUXjvOT6rjSuzvy3TjfKdbVPfStNCpip2udi6vFD2acqLeU+66JE+VVyVIj+Qv5dBAAOphcTubX8jVS4iKz9jRReU1YCAJenU+nKg/0pAAwJoBIA3M/1TsBvg+pc4VVnlhkd8mMjA4A7Lq3VK8rKnmNzfHha9mWPjPzSpQya63zWBbWlyaxFY8rUPFqaGup9EwA4sesQrf8zjuS+N14CgOXV3Ei56o3IiXvczOZC1kc1wM7RRuFtOjrU5JI5fdx5GUne5fvQBStmsXxFIPdjQrhiohPmXFjVzudHfeT6Lze4QzZMDaIAMNRxGQOAa0XXkkVTZ+tqftOzHZ2uUwBo3yBGc5X59PUPff/B+0UEZK5t+nd50p+pPxS9uPtwhZ+OTLFhbkFBhf+bv1ryOaP1jzpkqQeAq9O46nbAFG1d9rXvM1TILoq2Jjd/n8ztOgAIdApXlc1VcxBrcINLvacG7JVebfmUc3upP5LnwYaqctE9uwvtFbM4Qlfd4YdRi4D9FABu5D3LJdczK4rlHZcm6QDg9O89whSr7aY3CuBNPz7lNsU01qTITsxV+rmoo2b5B6leQjtWdtUkADhaqpwOALZ9ns88Zu2jnfo+Va31DxdZSHVu2PyVeK5ERFIAOOEhmuaMLqtwftNjq8r1HJrvLlNxm24KG/7ie28GANbbNvIN/wmRy8SltJXu/FIOkj1eOzLHKT3/384/vy69p8dH8UPU14sPJXu7mmYA0KnNHLKtzTHNjue6Vo4BwO60/TJiDV6l6PpTXeWZ0TOqocGmy129T9R8PQCsKDCetjw/Ws6fEjPgr9R4qH05aX8fzkqmDdrLZhoaE5uYtQbvXCWFAR32vqTkUF6xvTqQvZ/NNbclBwqtVrXx7M55oU/X230klsmi/UBmUDm5/t/dFdGGhXMU2f8zWyhPiybR8cttGADs6ria3djRgAHAvf3amsc/U82UUDK//gYlX8jcmzoAeFh9LtlpXkHo5CcPN+H9V9W5EgB8qMT10CdvbhManOW1nmU1WSF8O3bjJgOA3oZP9OKDVEVQeOiIwXZUsNQm2Vdfu2VuuZ2FG7iOebPlAQYABz5EqJiOxsUqkSvvztCtLgNF26rtp1Ub/dVbwXyJuD6fpi/aySbE55dm3/2sScHdmkSR6kstNdf6XOOKKIQ01ofce82l32P69TOJ9PRXtnRufIrUprAbAYC9Lsr+4zD7FvFbOEnpX/2ZtaSkzh5yhi4L9uoBIMmlqtGDU/Pjfsz35iRds/M3/nq4mvvzpCpPgLerAQAct/fUxeWOkKbYztWsx2mjtQ4ABkRv08cv2qKZZ/nSPVLcunha5aAHCTjjSfeFTmLXD84miRe30uV97c2TfeqYXDryXD0ptQ4UDdyGWfMD2t3N400iekDuUNJxMD+zrNjUQc47v9RECQDWdtuuIo4Pl4wbCzrMdVeW5ttzuHJk5m7vv06x8mUaych5mdBBOn5f2253x8UpLKbldhW1lX88ULWEDOtWhTYoU1u66eImA/zJLJnsCPmmqF0XTJSf32AXTU3iqtfn9dazewHipp66pIjRg2C7os8EePN8rvR/vd5/sYuWXGwnGnpPHSu3d334OFor9pkwGHkLzGNT9rr+nWGaMDPezDnxLYtZcZkBwM+8gTJwm8aslfeCk9sU62+zy6JOwKesWkR/2/o0tS+1j15cncYmdissIyukAbdT8ivZWi7jUM6VfD1+m1YtP5D+fnfgr7Nuf8vVFABmDfU3fEtcIW3M0snI6Ou4VbP8l0JcULl+QQ+aUIArixa0CFXkSEVfC30Y4czlZA/d6tKRPa9R28wXmsj0OTDYbOuZrgwA9oQfE4hgy8y2FABG+BnIipuzyMEj3YQ6Or8QxVOZRfNIGDHhi1HqyDuIcxJbu6wXKgqoZa0CbtD+pYoVSFXC8oZ9ohbN85PMRb7ELcRDppZLKb30ABB12pF5rN2vA4Dfvz8qA3bLRercsRLJN7Q+AYAWh/vQm4tzS9nBBQgAHHs9XQWv0/1I3U3vS0b7sS10jr6mQ2MGANGOjQQkDJoyVT9oouJS0NeuAgWAsGt3hT7uT2qv6PnT+pqkdAefIfTr+GxNwvD1XC2UtYkdRgHgx9YvUr/2TZR/o15uJwBwYUB9k9O304QhBgCYzJ4JDZY/3JcBwPdeXyUAkNZy81PzzPsC1czc/ClH6tetdvY0fj9nJR2ti7C+nbaz6804T+9plUCtUgeyL53sZRjW3v2gCffmBty6fulavvQ9sC8kD36+m1zmtL5AtrDcjbnatzAAxP7a8j+dyf5M6w8yoazupcT6BHSSv+2s2NE4Hu58Fq3Iy4zQZkU/n+OylgjJQvg/olEvkpB7KQGAW+cTyEVdJ3q9PVfxTkz20uxUwTWKDv3tGE718cN1qrx+Nn4qwDOzAkVDPIlKN3dw1e6cd6fYHafrcj0XvowSCOd51YHCwPWqYm8GAGFxKZoDmujvIwHAy8+KVNeqcmWh/dHrd8nv0yvlkutZMaOjIqdykqS8y4uw/s9Hy/1pVHiwGjcxzfiptM7pwbTv92IMAN4Ofyx0IquXaLKz78v/ZgX/PamwkH+L63UzADi+WfvU3p7aqr67VOpJAWB+WRfp7P54ku/EdwYABZ4UI0/tRdZ7Rjn1IAIAm3Mhx3B/aRFPz6ZvZmMmshyVsWnO95zGCVESAPTqdlTasUFcbWofsdQ3ajXS9LnLNeWQ0SWqenR9BgB9y+/I8dT1crybo7xJrgvJMU9uJ+zTZZkMpO1Ifj7w38ptvma147OnE42g9ervNnR+cVSGt0KjIwwAXpXrY7LNaTX36QFg1Rpfs6/R86WsfIX/ehi9Ni6vMKjtmyhGFxltMykArF2ykK7ezI0hQoe1pDU7f1HV2zO+r8HJaiqrlzbaKIzDFsWRoMXb1OPQ59cjAgCn+iny/Q59lc1sfvxnCQCmWdeX//ddEK8HgAGHFb1zcJV2rOJxT1q4dmPi11Lb98TsSgRZcoZbvNhdGcLauiSoADq120dy2t9LKK+veFACgLbLx+prWu+ixyotoSujEoU8yzc1ZdKiL3TqJe4MM2XJE8niH46x7r5XDABq9nlHXq0QKfjVLW7c7RndU0Ds1lpDNJFZqkgvCQA2R3+X24+Jz0UazpZyRJQehSKVfCWClDZnXwnWTbyt5pGDSDJpcZNvnJY13uaYV/9cvgEFgHSLkZpLR4d/1tZegcNFwWbjZnLHDt0VLSnvhaTL8LkVW/U/nxtWTRgjDY2dZXxmhOQV+j9q2gDzOVUrsnNj77MfzdWG3P+mXTVvkTkt+RJ78kqYKKp3KEkSbEuxljO+y/Ae2zzIaF1PvP/j5dXG3lZmT302dmfD+7dV6RMAwLE/I+e7daAW/W2ksfNFnUSIeyHi7s+VWT/7epLVRc9IxT4tJkM67JGBnbVuB31/pRVt2z0mx1LVAa39SbVqlqL91zUruXPhF4fJdYVe4iap3l5biK+3v4CASX5MNZhni/czCUdSKU4oJZakqAhMl71FAgCbb1XkOnoOrCW02ZRGa9YfPGSpIaRaJN0zfJ80/egBNZFd6XlX/jjIr6lmJfencMOFmElLuPQzvjmRcnHTl68f5tKMO6L9bOXxXYVOtI3eSwBgQ+BpTUppso1ba/TuqBg0NGXxAuW1HFlcs2z5bRz5oe9KEAC4eaChLCXYcVp0f5tagkuwFwXmlb+3/PpSbjOp0ypNYlz83l4CgO4vx2vCsPj+OgkAno5IlGFe2+MD8x9TRHNW1z1XWf19ZdxYVeWblimHtYByLnR0tWRVnktbN0oLdvjrACDU7bPRqThi4kZ9YkSW/H9imrbPYdv+aZrfo/Ncl9bXfSYBwHHXNzniel6eNM5R+XUpLSBhaUKC3qfoQ4EAp37yo/vGTGd5LjQxOnuqvzqu+jdxwl6ZAIPOhtOBa9tpDsQys94q+KJnnhLFTJe7fxIQMn45t4oYZKsYqM1Y4GBY11Gxge0Qeo81X7jWqL59WcXWclnnPYd0GR+MK6j+m8ZbP1Dl7eY9zQAAESU/yMj4+vwOuZKfBz6wdC0olFm0jRsQFKb2+gaX12i2vWSX8cPw6P1PiY3kIyDvTuzZvxLFrkaHJQBYv0FtFDFrwOSc4WBRlBKdYW0BbSC7JZZUfd/o29N80RYnYVAqd0imbr8lub6VJxXD6UlrXdi41m21648fJHy/fcNa6FCTHeGaS8nbakvlcvvdd+ua31eWxNV7Qtn19e/k9p/qttCkKVkUACocUJut/jfl6j5GE9av5TaoZki9aqOEvJ2eXRHqbzSksBT/9TQLPnWYAMD5GQdZcOtG4gCN2rmPWXlbsJK75tGH/R7JjeRe35QAwOCHiq6ifLEFoizmchWTG2LofcV/Tud4S7I8FUTzT+Kq3mGx041SSsMsbtwwdIdo8N0xyVYPAKfOvCZHFvQQynuFK1Lcnz08hH9n6vEls0v7hoZZlstlmCvb+es+jP1sFI4gC1c9APzoyPuddaQoG+C22uQg5g9RTvXhOj5AfrZB9OB9xXt4p+6VCm8ji//HpsFx60rhw8fzah3xv+nwjfsMAOKaigfLqFrvpJHBntKUdbH0aVntU+n5DVWp+Q5FSbMyV0GpyAaFouO6l9QBwDnn25odj+vKz0sFeu4V4M0dYc7K9H1ndPl5Pa86yxcjOhnVa3nUAADzZoyjAEBGDeKHVYcAgXu8+nyeyWXmc6YXNVzmZ4nj05dr5nUxz6bPgh10AJBUuBmLe8Kt4SekrFPnL/3inUlKL1nS2yhVpFRprj86VrTn2j3cSd7cPnRRzg1RbU/RyfG2Jf59vw9tbuvfdPh8vLAUMstLqvz9ep4RBuHg6t5yB7sPUlvN285RDNkOPy8rPzcsstEoDo60CSNLw5xp78SSbOTZUaTbx3ImB+je9MOs0qS99PT86sr+m7+Uqv5dd+qSuuZzSYmV0TTi9FiGI1cvSgBQ5b7at6FF0DO5M8NWch309ivc4uNgrQ5sWXqI5npefbfC4bic6yDqCGbf1tl6iRtd+oMw8qVKpnS7S0s9AGTFKizqooJXhI733WgllD0ztA898Ps1S/rlJy02XNOcHQGHvjAACPbgPoxFF3GFWjfynrqY6yQAcIysxpYMm0znWhYg+94qLGqVfHfZdFJJk2h6u4XKfQsZvl1vu1wROhZeOpKQzso5a9KmyXTiArX784FfhWinzXlJ90g+U3HAmfvwtXQoZACAaLseZp4Xa7FVx/iaW7cQN6Ben7GJLF+nbb7ZcBhfBj6SArrUz6LN7/56oh1vPQ8vo2Jn17kzcmwdmb0uSK73beo1oY1qqS5S2pYMkzP+TXoDo/87jrMnA+cco7nbB5Gl/UIIAFyu9VjO36D6VMm11FdS8iwPqNPth520upBoCNKxmYvw/qLTEfLi8TjNgU2q+9Fg3eEfScGcgQVY/pMNKQC0OriXNozncbEc7o9gm+/HkrSZpeVKHkw6TlvXryXdqarNdZSf4EsAgL3nUXFsV83R76S3ubXIzTk0X6PpZjN3i6Lr7jv1FABirqX+z7ZQ3XceljYO5AFdrPKXpiMPO8iwDr1eiAFA46Q9qnr99mrHuPo3bZrGbXB/VNaecX9LzULb6/tGPlK12631WvXy6etlZvPJSm91Zxgr+vAfV5Dvu4awsnX2sNEH0yQAaGdmXFZ0OZz7e1jEpJCjvxS9RIlWCgNwsjU383m5nwsCh/jwyAgXgyvIVGMxvozQhrn9ZK5rqe0hdT3nq5qJ9/x3SeNTnQgAeI0VrTzW59/G6NITKgTkLtmetEm6rIn8fYdFvUjV3OqgbQCwsHURKWBwcZLqUoYBQJ+FC1S4uXxmr6psdgLX7XTuskzfrfUF2vaxJXsY7KLKZ7tFHUEIKWbaMavGBVnJnXx/pQBx9i4hv9d+fE882b7ep7mcXV6qLHNbZpxRW/attJMmTA1k90tvzTE1NkziZ4gFfdRyqY0XueQg17cJcltTnTdowhblz0X5FrM95LxXCnLNp924QDK31iEKAM8iqa0hA78AAB4aSURBVNxOtfWenCgyrtNZlO8/XX8oB8fWjmrzomMJirS47MJuct46XbkEutsuLuvSL9Y2yZVTozWjJZ8ODn91I7syyE/6ek9xWuzqO4C2/pksICG57GsZkLtLTUdW8/nCfRJLXcgmu4dwy8iJhQYa5WoSupRgAGBIaCN36Mw+HlTm5WPFonBrT26oMDPXPjlfmep9FQ5wuRVLKUxY74w30tmJV1iCHaHnn46Q886zS9UkmpNnHknfQywFZNZ7ItEi10LYrAPa4iCTKdy/KLvSz4EAwKd6cTKyklo4qBDnlmrJhg87rAcAi/FcPTp9QYh+sdMaQm5OkPM3vtuOC++si6sGlAXyM8hTVx6qqG+kgelq84AwKzYWljt9u/vjHHWmXIF0AgCttyZKVZK4JUn5PiVV1JbxtiUpUTKfXGdgRyWSUcdnnhIADJjuxd7YcWOD1mGvjLZvPuAYPdV/1v+kOQWA7DeryWELtUORnOL3fKOVL00yC0/szt6EJynG0o/qUgBIdOXi9X7lFPfgfifKyvm8l/OgLu0zh+TYIXNWFbVFxu/9YggLK9+VegBo+WyakHfDfK5d7NyqmYCsFlmZmsg7ESzJA7ysbQ2uHz/gKi2O32pY1+2LdLSyvVwuPpkPkF1KmDAT5uu44mpeO+Pq15+NTwvtS72aUgDYlM0PsZ3G8mNDB/pIBefcC3PNUj6V5nWPK3Fc2rTCko3ZzBXu9+aPEhrN90OJZmCbHML2nmoiA9v4pRiw69c4xR99QT119M+NqwoS70QxsOavNzXY4GrehQHg1dYyrFVgkB4A5rhv0gHAoXxiNNQ9j9eR1/3v08odg41S2bb7x6WWY/bI7c9DY01E1jMoZxzvZss062uz9RgNnXHF6Ez5cOQcnZTGBZs/nB0FfPjmTRPq9PsjwhEAPC4zgvg9N6Md7CyknROnmJ5tGXWViJ+nR+1SBINW9Vnd3TweYc07iezItPuqznad4ErnXlU7ZF5Zs0TIe+mAmuKuDhTN9zvqLAVAnxm+E6lzuAr4VB/uHOTRlc/UbcVLyu3nr7g9xwyDh0sLo8iPqB0rwLtxDfcScC2sfP91cCMFgLA178mLvefJz4vxpOeN/kK5Ro9E8X+XUSel5nHfSbfJFgbUehVqcp32iR4mip5PPVQ2yfLFNQ9ynxd+01Y+jbxMfC9wM9Jzjg9VSMr+fUiGxX51ODXfpW05Pmx1Y13Pwg3Z3SKzNREd1a4Vq/ytKRlzqoAOACqeL6o7W1AvwBT2jrOb7GJb4r5+o1HqnFVdsS7c/EWU5FYI50tgrn1P2GKvo7onu4LYtbxKcAS/KUq8GPf+U03i+VLCVQWGBVcjVcuL2/fHbPCq65oIv3uS++AVODCe1Kik4dn0n3Ri4WgZsOY3q0izlyibdbmowwIy7PN9Yvb+o0wa07HvjdnRfGrDg5fz0unhx7XI8ERXTQQvK3tGVWbyhCBWrHWWss9UT5eipoiq63k71KsAAMxs3UkPALbhpQQcJNRwytFm/20qt03r8SFEG4fvxj0nzk5dDAAwNHmnsgZ3UbxHM6dXYM9fKaJj6vlAAoC9Vwbrg6qK4f+sf/AgY1n+imPNsu7rybBPtYR8S83MGQC0ufdRhbAL1evK32o48ghxR50U0bqVlC0NXN9YCqDcA3iNzQ+TlBjSfTKzm3mKRTskcHu0zgVIv9uKYaBlmQkCcp5vOSC1suLuEE8HzWAAEOgQRQCgarirlH/LG/JjWLpQJs0nTLKUutDek4N0i4fW04Tn/RoeC6zb9kDj8HbprR0GY3mJMGLTOIkOSDmg61NjpsHnAvf3ONZlBE3skqqLP1NLAoD+J+pJGxqUFqiDFasjAcCv3G6SfWV3AgAD23I/i9OVXaTKoU+lYb1a/JWihu4G9XpdjDUODdQUaP6ZRjTv/tf6Gn5SgtHM9VmkOSM996wv9u9zi7a1pJKLaqk5pMPbTbZVvYcSQW9Y17P0xS5RopxQ54nxFab5I2tWpUAbocCgs/0V5VRsAg39qYhIXo8eSgGg2En1JgsAx/dulvNm2EyU88wcUruwnbUSQ6rXXksZIcfLKmzh6oY35eelvYPMvtjXl04Er5DrnO5i3DznfJgYwu/sEa5Ucn6s+L2Y5/opWU84QQHA4Q63K8gdF0wAYPjISDp40DcBF8En7aUJ9QeyrSes2fr66mBmv/v+lOIOr2J1rmpHxf437c43T5q+oNpfCQuXQg5SAHDuNsDoNPqRVZEtGj2MjF0mzoQ37/qqANwyROFu5uzYYgCA5iXsaakOwzWp4oJtIwEBi1bZCUDrXhYhCz5z791ClX2lQSMVZZM11ILANldXGkWMy+Ci5NWm0jIhbO86XAcAw+OOmgHAIrcagsNqmeWp0uoAV0PtMO4H2a/FUOPazrAl0pKsDvrzuXnMsIJtXppcRvu13/53A/QHN5SZMqTvC9pi/SJV5yIfV2dbJnFxwojZiQQAuhxrY7TyMJ/8Rg9WV1YEkIA0RdeReipDfnadqCixxuVvKIq5l3IXu8P+PN7h08raDqr+DqYDW/6Zhs0dLrc3YM0V1UBb9Som//cfUdPsrndtATfZTX8Rfchu1SB0mTiD5Kq8kcUOmSHnL9LXhK3voNs8glyet7tNTrnX2WZCJcmNjhjVY3xZeEvukN8WxdBN/76r0TYyrm8i5rWCjK6tG7ZwC7/MRdyc6OcZHnTNbfYkfZMdeaTIf9Sw/6Zaz/hZZuAC9QY6aS0PUPO2dGWjiAk+NZ2mpzRUwVu12lu5zK4hin6lRd8m9MDKDdLVeoqKuVM8ZzpmfuDq2nW92rF2JZf+da8T0kffGJNTaeeAZian4v+aLswcoULKjrHcMKBu5Dn2qXhVzfYeWeuF7/vGrJGO2s/RtvGKH0jKOW+lAGBzWGGDl1imkRFp91Rlkv6ImvrfdCt5icGizgQCL8641PLmZ6vB7hNlFXW+hC4EAB5M6M6iFxg3uJbTqsb7VdTtPLm9PHINSzyVDK8aqUZy1Wh1iKI/U+Lla9LRYtpxFxPLLjQsKCPGsaqdxT25Hq8YoULAzAWLWdWPhVmAm7YY3VQqOumSZhmydQud09vXULO/OgiB7z3TQsMy9YyHDJlfeSyxzH3dJG4muhUjI9dsp4M9y8u47z3Lnq5t1YrgxcHW5Pb4InoAiHS8K1QUUVAdRmnVjzi6ZZDI3TjYcjY2X55ukuXLa+z+IBdWMEq9hj60VNyK514T/cRTN07T7GToIjvD00hO7ZY1h0k7pmjn+zfd731Pk6Lz377KDencnpOr84y7I/SefFxz+X3bjOt71idsE4SotU++YVffqqNDAECrA9qu1wDw+ME2MmfsQynhcW1Wxi9SFMwua7vcpC57YWZVMmjIZ7Jppit1W7fHON8M4FdnLoy028AdK0eVXEanZdb9n9bKb2Nmk40LSwtIm7buoeQSVVVuu/hIxfpPV1wxX7rVKkGRRI8zfWL+Uvoiq/9OlCZ/TLjJ4vqNJACQMovHgg+J4W7Ss6vHy+2f7TOLAsCIqooH8vTp6n3vWoEdBAB0tyJITD21i/jJyIUy7i3TzCUcfeotAcChedyk/pfdy/+rEKz/puw8AYphnHWWlNrVi2S99ZKeejhIABA0UPHgtUo8z/rPVCSpadb+dI4Hv+Zi66FBPP8o4xfLbPzAA25+qmpBZ0+9JJl9/sRKgVOlTXEnoR8dM5oqdl+6U9Jvg/pEz+KUqySWBWw1z9ddMYT2GzqbPanMrU/u7+SDYDk4UrfK6p7JQXfv34ZaxOWh6+cNlyo41RIGbFwd5Z6qX1Zh/Ll/Zxv5o+2En4qYIn0wm7FaDE10ezh33mmrUzbRtLWOcvnQlHT2PjPR5JJS8qQSbfr5Vy48XPXqmNypPvV6kIJ3XAkArAzvaYi06iNM5+WznsmI7mg+gdX7WYj+POLMPn+2FxCz9mFj/egMRWLtYcElrH0CxL2rwfW8DADKNYvXnPmBk79I3eqsZwCw7ZDS1+hdt2i/Cwf/ykof7P3of1Zi/dPCfda861LpYgvlIq0PPyLl5+C2/hQA2o0ao0L49Nn8yqOHI5Srj1xbKVEQllRoZXKZi+swVQ8AIzZxvfrgsnqTgxoRevOviNh1YQvxzIgwOF7m0lqP/ecIAJSZay5VGKrElD9cdqhmXe6JSiCA9AItafusvoTcOUabzhlhcnkfljJB/p9SpgMBgN4eXGTk3pkH4D9n01puf0N35U6vhdZcWAmLuMHs8IfFOgAIyyjKbJ/kERBY7kkKScviMWsfxKxgUac+kl6J9SQpu5sOAJ5uVSIB3Yk8Kz+/fcGpccF1JcpClnMHoxTj1qyHSXFCy6qMza7gx76dpsSi0ykCALNtbem56ds0B7DEKoNoVNeUW8zcHsc5ufIHc+4E+m9q16iT9P7wBV1qzHVNIms6t6tJzWmLo6Kw8VLzONPE1XifnzzCdZ5wG1WX2FUsKtOOdVhkp+rAhFMhKmSs87f6KwVnN1fMbrzMHjC3VB4DvbKZ+h6Nlbf5zW5PrTbQmslLScOpfnT64Tlk0jYuZl/dl+vvaw/saAYAr75nS1bzDfRN9kepR+oP0nqdjSF3vrJkdlzO4jQaS443tLWUt/xfkPE3XKXT22uYndnlZLCro8SOGVxpvsnV4d8U+ziIjgoabVq+9aqEYvX+rL0Su3xRpFpx1Pa2EsFzw97dJiueX1aR7ZTu+Fg1eIFZncirzAGk85llLOon92ryu8w9XPdbr5cpMOQlt0SJGynKxrx38TIT8w6TvsdrB9Q/MLezvmHbuhIAJI3NI30dUE6AY46D4hVbfl+UUL9T/Au5zsK/uULqrUH0HFv9Uls+Va8Zj063ZKO4l3VLaEIaLCpP8dBMiSh3+oOf1PosX3IKLJqgGtn4rEY098B0CQDyDubhNl5+Li6XH3w9U0aWp3u0HgD6jrXRnC2Wre6ovrf+wRG0c/lpwqZdZr2u2tCo1R5yxw7EOJiktosLucnNmys6Zs2U4Aa7l9VRWOFURXe9qLO3UN/o1a3oBLfLdGcNfidJ39a5tWdEEheJtN5bWCp4m7tArPiqfXGAVmp6Oor6LqjH2pifpVNmaksUAACZt9YKP/ccVQJF9rtaRP6nr3yATG7kJ9W9lCL/n53WQIWsYfWizebm12ZbmwwQI/KM7aZstBUi+ab+3bOfXOfA/K45smp59zhdNdAbifpb2fbNCQDkOqLhCvCf5ODJiS45D+i59p0NAPCxwhtazqyUMGDtgxS/EWc3dXirYINxD9wBgZEK/hokrJB6RnHnl7i0oyoAFx3kzpSpFfUGAPCcUkGF/K4bhxEA8IxXdPT5TwWz3mFP9QBgd+uXgBSbbTxSdh3rxaqpbRa2WeBkfuzMJABQbyWPUdg1aDyb7a6+fg4A1k2IIp55FD/Ekb6SyVn1vs8tFZL6P+NRWAvkU+5NafRQOxrQ6KIxdLv9UP3AJZuEdnbNN24s8d+UPtCMzfZeRTLSczE0sVZEBVd+VyHWxR+rAOxxWQyR13bCG/rx8TQCAHuacO3cza1+KgDsP3M+/d3pLQQAFp/n0tZ1OzhX9mFVMuv9YSBpcXWs3Jn29TMoADyxL6wb25MH5LTJLcblbb24umJTbMkN4novOCLn2Xf5gjRgU4rZyn4P5HqTxlaSMmbxWZg2I+qvTIdTPrUvh7Hk6TGNvpz4kQHAXlKFOCaKopCkuAChrgcleGCeikWHkwY9i1IAeB7zT6yVUaGJUm+XfqyszzxSzZc7c57Y9Z4BQKW++wgAOD3oLcq4Dq7QAcDJlcG05Foep/BiZnUZIdXqe8qDumdINju0bYYU20m0x5pv5CD2b9oXlirXkeIxllSt8UDIb/6Euxskp3E9+9BoH5NLWtrPN3L7Xr4OZgCwbPk+KayGaNrjEm9CpfpP6nA0VTpTt6pqwI5H8MgUnkn2Uv+hChdZunE78t7ylOYAV2/1n+s9xkRulBFZoUOEXMjCV30A/P+d+qz1Mrqm5iTdHKaW1EZ2WKMHgBupOyUAqPZHsPu6v0Qt34lOC4X2ffKsk8JH/nG901nRUWdjkSNS2T6XiWcvfnI/PeW1tLfnDJmjjO+rvjvFud8s5pk4UnOQZzSsK2Xv3KgHgEOh/7F3/lHjGB1pWMVgJW5yZyqpA6oAwJDlhIwspu08DwCxYw5Sx36hhU9u5LZWXRurLd/ltodyx9GsjzxmSPhhUR8+c0iiqp3AUZNU33wO8+v5Uo6Zy/9eZ5c3SViuHUqR1s8fkBVPeuR4ieoee4h9dXwr1Jt4aowAT/gadeCFy1uOkqGOJ/RzBvOrNs7Hpwn7Z4GQGB1GvHliEuCmv5Q46GtuK+v0wP7c4M3rzTrqU185MPUoO4VrHZvtEupNLdNBOjujrfztUKM44f+NgVRFRSc6XiLHz4nidrdX6tj0plIRWkwa00TcF6M8RMeZT2PGCe9JtebRGckN5HYtt/JgPKkZCvwxLmUJAHRMmJ8j7m9268EyDC9aagfm3Bz+Hy/c2EJ35A9H7ZsZAGDW+ZYCAnq+fiC/b/ihWJVEnFDC/m0bWJmmnxhuAABW2Z9t91Y7Xv6Zxp/m9yFGDOLubs2szqvyhxSsRGdXO0e69ln716U03UeJEF0jkLPjlvOViyPHVuU3OgemJjH745xjbFjxPd9cLw+X6/+cpb3u3zikvmf+3+TxVgmSU6TdIOnHpp0EAM4OdKfPd9uQMb9rizZcoxVRknPlf/T3y/YrV1S4DCio6nBayDyBwoL2X6C2v5SgAL9CFRexDc1uSZluPVSsbOeaYqB+kqp4IoXfUZRKHTPFmdIpg4dtmjJPtM/NSxIU86TWiknpq/nTWAeni5qn5Pyjpv51zzJvcdnogPt6zte+TWFlBLN4rdggP+mlvkyy7Z6i8rfOaTyIQakwLk1/Uec2iVlbXN1uxOEmtEeBL+TQI0Xo9i59ID3sZFop1fFhQ1Le10E4OwSai/rvjLYFFY/VWSdMIsYG9dlqf67L7rq3ErtfT3GF63CutArwzYUmaSIx3TvR/M/3Wt+Nuz0DwK4dinSirhOXdJdMePPXJdKtuaJyOFWbe5ktqynGIh5aJoqVSistNVvL2dvNCyA9n8S5stXr1okDXds1T464nRGditHZC9YUA4CsCeJmn+oxhc6rKbKe+4/v0UX35/dTpb/hVxdVa8D1Dzb0IxsRM5Yk1/4u5c3m/nuVD3Cn0a4Dd7DG3bmR3L3R71XIHmKlSKNLpHkQ9MxW5amdV2QEToxea7KPXe25dnFEdC46sIVoGpqynd+6dqK8GKrQnin+i0Xj1rGNr7wkAEidpFz/YfvVnhzbz40fugSUNArD0TdLybnHHymGn+fmOOm76mtSwuOkX0Yp5Ez6drO0QjYmZ0/R4vxmzqEu/OrrKl/E2Xb4Pr8h0+mSti7837RvR3tVZ+K2vqUranMfjE1rX7FHs5VLtmIcOAu/NmS8Jvy1R96Svy+O6W4OAD1KtSEAsOmaEoe48yYn+dn7bDsKAD02+pEdsUdleKa3m8Haddggw799aEnNGRswJ0LFAAR3vcEN7+JnK/3bnB1gdrTzEgMA/PLlYpJqNjXYAe9QmjIjhHw9ztfEmNPlyPmjRIW48C/KTQDtmyg3K/See0ak0kPzNJFTvV2wUKfl24pCh+Y83fJX//XozUlCHauGvGJV60bTxt24k2XtR6XZhix/TURVOa7EJnEN5zr2gaPekpcDNpLiESNztHrsG6yONVlmCpcS22bySBMeTt1V/Yiu0En3u+wQBfbZ+jNkzRBFlPBrU0tpg3c10mrVFE12bvOgRGlH33v6mJtuAgLW/Dz+V+6nbfE5bMdzLnqvH5kqdDQwQfS7u1iTX2fa64P2XR7WPxS9yY0fVmQP7qnyfe7WUO8YzEU7n2Mrs5k1lEhvs8LsCAC0b8N1+mueNqDstNro2usfrmnM/il/7d+K30qYxBse5f46kGXc22jXmey8y6hhw6Q8h+i2fVOlwe4BOgBouFOtkIpcFKtLmeagy9NKWwRdzn3BX4ELnhYmEMD5za1Jr4jRUnTgbbbDWX1F35/pQ8MyZMYGfh/5yCsT6WVaWtXeeR2/I76xX2MVob2dZmeIHS2e2pOP7xAIrufQVFWdlku4C1u5C+rrMACgcyaP4Ttgv9oSdGxIc7n+Ps8Ud3P4juJRQx0b7VQhs+ni59z8ZTiPShAR6Gtwb1mX9TQTb0aOSi1tdP1fZO8q7hmpCp+/5L0SvcFwup1iSzzVSqJRf48BDwAf1r0S8ukq1JMe7z7M9oaX+Ct39N+UsYprRPeeCyFXb4zRdxw4x2i/AvdyiXTZrT2NzpyBI5WLBGZP6inDeW6mj36M0w/6ZFR19tr/udjGJKuq8uyoFW3aYmTFsiE5ikVypr1yt+6hV8Yt0P9Nr4OP0xqPE6QNrIyq/b35R+vc23O9jC5XEwH4+MLcmTP/DO1L5AGg3mHlllDnpmrd+6rViuv389ILaZnp9mTYl5dCOx9sjVuvF2qgeIdd7bmCrSnanfZvqNyW2qfbYLmuHiFHpB6ZyuU3UsRiFvqtijZ+Tm+dzJ77cwf85TpFrrMyjlNalbG/SNP0Bfove0MZAGRcLCADWaZoWWnVtogcCyPdY7inr1/4Gf3UFZzLuGHRWyo9lA/IuSK92NBS6gg8j525uVA3miBlBdyV7Cd/ZClBi0pYZo9WzYbnvvOYWaQ329hDxyYeeKOC7SSJpQAwuqcS7qJ8bUZ+taom5D36ROGmupqlCQO1sW5F+X1E//UqeCf+FONQDstqJS+XF/K70TUdj5of2c4P4m5t/jEU2XdWuQ0noPoW9nUZd8gZc2sXGRfXhp6s6aW39Oc66/1Tzwt7zfMtXBcRNq0nO9fgsfDP2TudHLxRTDXlJ1/movxEq8Nyu3EXnjGPt28IAOQpvI1mT2xDrzutz/EAG0tjbPqRR1IBWuvoJmHAPg7jfpGdXx2kvme/C//K7D1B8pxMl24G5WIznrqQvH1FxVTTNCU4wpsPnJEwxPJZ8Ks3j7QdnJEl42LO5tEGAJgb5mt0CX06wI+8ssnP2/nhpb5OaPdTtcfo0z9cFG6mXJQRnXD1EAOAMlvmCAPSbwW3LM+VsI6+SufyorLTFst1+B0KZI1cUzQ5uX3N+ZUO7rZrzV1Liq7Ul6fWkS5858HvWX4l3u5Fd+55dXPyVQH29kV5bMdH5ooRd+7BalnZn+ldLb6v5plykJx1TZcqF95N90RAhdBn40qQ9m2qUbvzPE7kjyDuPGpT3ksT+ecGKjqjfo/FwG3HXs1ho9tdJhi107ie989k7yY6ZFZcQZlnHi553eB2TgDA7ycXf4fofwh1ZxfgwWK+v0qmFU94kA/jWrP35nwmLH1YVEDSjKOhf92vfq9SblrzfztIGujPZWpDX3NN5fPcHFGBgYsF+B7dsjQ6+/wqqI0vTo6O4sYTp9Kk4p8vqMpeOj5SemI3hnj5RMv9/ZgySz/oV8L/NMtbXfsnal/6lP0C4qx7NBbfnxi/129lyk/NRut2Uosq1vYNM2uWdUSoq7fjJ1q9BTffn+voKAxCmPsUCQACZoSy2/20A5ItCa5OrP648bPvqywBnpZ1ue9gl0x+a9qTK3z2++9XwgDaN8kS6j6YqzqJtlGur3Bbv0fVF1u3An8l5MqN/eiaWwtVhFVl91navMULWnSCRI6/sDE0tO7Lqib+w2ZfbaKEf6hY14lltDV9PcPscxcMq6JDzT/m2ia1xNscUUBkGA95tKMIFxi+ieIatBKTAunKLvwAenCshXTAepaMBN+uxgWQgzbkrN3h68VrjBym+SsxhNe2Zb+9m5M5TxTFWaOMRkYJDwDOv2lGjxcLYyOneuomTA0kj+6fZZNz66WRy/nlBH7/WRGMpTvtXVXtLG7QnW503EbhPmkiLRjgwYbFnNcEpsYlC6l20Z9CQ7fiROlkbgPnVJaWXWpybQaAjUbCykZEzDFzChC9V609D0rSnOLShrLBtMZ4a3Yy64jRgfD/rej991sYv9R+00k7OV+vkBOq+k5/UdTKjdy4mdPOjzy42roJDUje+QE5skQEgANPD8j1t9mUR7d3tvrM0plw65Q9scWpTUnjV4cgIj1C2uq1VZhurs35yJ5tevevQF2vTwXk5n6wiQHAvrAyJt0dgqfkzOBsdY8rwsBODW+nGuieTQOkvBOUg2j0s1L/T5xbkx656WZ/rlbQbe8s4KDc2AoSAPi8VqTTVV1a0cklhqnaDFr8U9d+TF16tY+LlHzTnOxPVQwi/g/t+mmB3wVFwgAAAABJRU5ErkJggg==); -} - -.ltr { - direction: ltr; -} - -.rtl { - direction: rtl; -} - -a { - color: #b1003e; -} -a:active, a:hover, a:focus { - color: #4b001a; -} - -h1, h2, h3, h4, h5, h6, strong { - font-weight: 700; -} - -.navbar-brand, .navbar .navbar-brand, h5, h4, h3, h2, h1 { - font-family: Armata, "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: 400; -} - -.title { - color: #b1003e; -} - -pre { - font-size: 11px; -} - -code { - background: none; - border: none; -} - -section { - padding-top: 10px; -} - -code { - padding: 0; -} - -footer { - padding: 40px 20px; - text-align: center; -} -footer .edits { - margin-right: 40px; -} - -body { - padding: 0 20px; -} - -h3 { - line-height: 45px; -} - -.centered { - text-align: center; -} - -.admin #page { - padding: 60px 0; -} - -header.row .pagination { - margin: 12px 0; -} - -.summary_bar .status { - margin-left: 10px; -} -.summary_bar .summary { - margin-top: 12px; - background-color: #fff; - border-radius: 4px; - border: 1px solid rgba(0, 0, 0, 0.1); - padding: 8px; - margin-bottom: 10px; -} -.poll-wrapper { - margin: 9px; -} -.live-poll.active { - background-color: #009300; -} -.live-poll.active:hover { - background-color: #777; -} -.summary_bar ul { - margin: 0 0 38px 0; -} -.summary_bar ul h3 { - font-size: 1em; - margin: 0; - font-weight: normal; - line-height: 1em; -} -.summary_bar ul li { - padding: 4px 0 2px 0; - text-align: center; - width: 14%; -} -@media (max-width: 767px) and (min-width: 200px) { - .summary_bar { - font-size: 1.5em; - } - - .summary_bar ul li { - width: 100%; - } - - .summary_bar ul li span { - width: 50% !important; - } - .summary_bar ul .desc { - text-align: left; - } - .summary_bar ul .count { - text-align: right; - } -} -@media (max-width: 979px) and (min-width: 768px) { - .summary_bar ul li.col-sm-2 { - margin: 0 10px; - width: 96px !important; - } -} -.summary_bar ul .desc { - display: block; - font-size: 1em; - font-weight: normal; - width: 100%; -} -.summary_bar ul .count { - color: #b1003e; - display: block; - font-size: 1em; - font-weight: bold; - float: right; - padding: 0 0 2px 0; - width: 100%; -} - -.table_container { - overflow: overlay; -} - -.queues form { - margin: 0; -} - -form .btn { - margin-right: 5px; -} - -form .btn-group .btn { - margin-right: 4px; -} - -td form { - margin-bottom: 0; -} - -.table tr > td.table-checkbox, .table tr > th.table-checkbox { - padding: 0; -} - -.jobtag, .jobtag a { - color: black; -} - -table .table-checkbox label { - height: 100%; - width: 100%; - padding: 0 16px; - margin-bottom: 0; - line-height: 32px; -} - -.navbar .navbar-brand { - color: #b1003e; - padding: 13px; - text-shadow: none; -} - -.navbar .navbar-brand .status { - color: #585454; - display: inline-block; - width: 75px; -} - - -.nav.navbar-nav{ - display: flex; - width: 100%; -} - -.navbar-livereload{ - margin-left: auto; - white-space: nowrap; -} - -.navbar-livereload .poll-wrapper a:last-child{ - margin-left: 8px; -} - -.navbar-right{ - margin-right: 0; -} - -.navbar-collapse.collapse{ - overflow-x: auto !important; -} - -@media (max-width: 768px) { - .navbar .navbar-header .navbar-livereload { - border: none; - margin: 9px 10px 0; - padding: 0; - } - - .navbar .navbar-collapse { - max-height: 400px; - } - - .navbar .navbar-collapse .navbar-livereload { - display: none; - } - - .nav.navbar-nav{ - display: block; - width: auto; - } - - .navbar.navbar-fixed-top ul { - margin-right: -15px!important; - } - - .navbar .nav a { - text-align: center; - } -} - -@media (width: 768px) { - .navbar .navbar-collapse .navbar-livereload { - display: block; - margin-top: 5px; - } - - .navbar .poll-wrapper { - margin: 4px 4px 0 0; - } -} - -.navbar-footer .navbar ul.nav { - text-align: center; - float: none; -} -.navbar-footer .navbar ul.nav a { - font-weight: 700; - font-size: 16px; - padding: 15px; -} - -.navbar-footer .navbar ul.nav a.navbar-brand { - font-weight: 400; - padding: 0 15px 0 0; -} - -.navbar-footer .navbar ul.nav li { - display: inline-block; - float: none; -} -.navbar-footer .navbar.affix { - top: 0; - width: 100%; - z-index: 10; -} - -img.smallogo { - width: 30px; - margin: 0 0 6px 0; -} - -.navbar-fixed-bottom li { - margin-right: 20px; -} - -.status-sprite { - background-image: url(../images/status.png); - height: 19px; - width: 20px; - display: inline-block; - background-size: 20px; -} -.status-active { - background-position: 0 0; -} - -.status-idle { - background-position: 0 -54px; -} - -.btn { - font-weight: 700; - border: none; - border-radius: 3px; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.4); - background-image: linear-gradient(#b1003e, #980035); - background-color: #980035; - color: #ddd; -} -.btn:hover { - color: #000; - background-image: none; - background-color: #ddd; -} - -.poll-status { - padding: 10px 0; -} - -.stats-wrapper { - width: 100%; - text-align: center; -} - -.stats-container { - display: inline-block; -} - -.stat { - float: left; - text-align: center; - margin-right: 20px; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 4px; - padding: 5px; - width: 150px; - margin-bottom: 20px; -} - -.stat:last-child { - margin-right: 0; -} - -.stat p { - font-size: 0.9em; -} -@media (max-width: 767px) { - .stats-container { - display: block; - } - .stat { - float: left; - margin-right: 10px; - width: 100%; - text-align: left; - line-height: 45px; - } - .stat h3{ - float: right; - margin: 5px 10px 5px 5px; - } - .stat p{ - font-size: 1.5em; - margin: 5px 5px 5px 10px; - } -} - -/* Dashboard -********************************** */ -div.dashboard h3 { - float: left; -} - -div.interval-slider { - float: right; - line-height: 1.3; - font-size: 0.95em; - padding: 15px 0 0; -} - -span.current-interval { - min-width: 40px; - display: inline-block; - padding: 0 0 5px 0; - color: #B1003E; -} - -div.interval-slider input { - width: 160px; - border-radius: 2px; - background: currentcolor; -} - -#realtime-legend, -#history-legend { - width: 580px; - text-align: left; - margin-top: 5px; - float: right; -} -#realtime-legend .timestamp, -#history-legend .timestamp { - display: inline-block; - width: 220px; - text-align: right; -} -#realtime-legend .line, -#history-legend .line { - display: inline-block; - margin: 0 0 0 20px; -} -#realtime-legend .swatch, -#history-legend .swatch { - display: inline-block; - width: 10px; - height: 10px; - margin: 0 8px 0 0; -} -#realtime-legend .tag, -#history-legend .tag { - display: inline-block; -} - -@media (max-width: 790px) { - #realtime-legend, - #history-legend { - float: none; - width: 100%; - margin-bottom: 20px; - } -} - -@media (max-width: 500px) { - #realtime-legend, - #history-legend { - text-align: center; - } -} -/* Beacon -********************************** */ - -.beacon { - position: relative; - width: 20px; - height: 20px; - display: inline-block; -} - -.beacon .dot, -.beacon .ring { - position: absolute; - top: 50%; - left: 50%; -} - -.beacon .dot { - width: 10px; - height: 10px; - margin: -5px 0 0 -5px; - background-color: #80002d; - border-radius: 10px; - box-shadow: 0 0 9px #666; - border: 3px solid transparent; - z-index: 10; -} - -.beacon.pulse .dot { - animation: beacon-dot-pulse 1s ease-out; -} - -@keyframes beacon-dot-pulse { - from { - background-color: #50002d; - box-shadow: 0 0 9px #666; - } - 50% { - background-color: #c90047; - box-shadow: 0 0 18px #666; - } - to { - background-color: #50002d; - box-shadow: 0 0 9px #666; - } -} - -.beacon .ring { - width: 28px; - height: 28px; - margin: -14px 0 0 -14px; - border-radius: 28px; - border: 3px solid #80002d; - z-index: 5; - opacity: 0; -} - -.beacon.pulse .ring { - animation: beacon-ring-pulse 1s; -} - -@keyframes beacon-ring-pulse { - 0% { - opacity: 1; - transform: scale(0.3); - } - 100% { - opacity: 0; - transform: scale(1); - } -} - -.chart { - margin: 0; -} - -.history-heading { - padding-right: 15px; -} - -.history-graph { - padding: 3px; - border-radius: 3px; -} - -.history-graph.active { - background-color: #B1003E; - color: white; -} - -.history-graph.active:hover { - text-decoration: none; -} - -@media (max-width: 767px) { - .navbar .navbar-brand { - float: none; - display: block; - } - - .navbar.navbar-fixed-top ul { - margin-right: 0; - } - - .navbar.navbar-fixed-top li { - margin-right: 0; - } - - .navbar #navbar-menu{ - display: none; - } - - .poll-wrapper { - width: 100%; - text-align: center; - } - - .poll-wrapper > a { - display: inline-block; - margin: 5px; - } - - .navbar.navbar-fixed-bottom ul { - float: none; - margin-right: 0; - } - - .navbar.navbar-fixed-bottom li { - float: none; - margin-right: 0; - } - - .navbar-text { - float:none; - line-height: 30px; - margin: 15px auto; - } -} - -@media (max-width: 767px) { - .navbar-fixed-top, .navbar-fixed-bottom { - margin: 0 -20px; - } - - .navbar ul.nav li a { - padding: 0 8px; - } - - .admin #page { - padding-top: 10px; - } -} - -@media (max-width: 500px) { - .navbar-footer .navbar ul.nav a.navbar-brand { - padding-right: 5px; - } -} - -.rickshaw_graph .detail { - pointer-events: none; - position: absolute; - top: 0; - z-index: 2; - background: rgba(0, 0, 0, .9); - bottom: 0; - width: 1px; - transition: opacity .25s linear; -} -.rickshaw_graph .detail.inactive { - opacity: 0 -} -.rickshaw_graph .detail .item.active { - opacity: 1 -} -.rickshaw_graph .detail .x_label { - font-family: Arial, sans-serif; - border-radius: 3px; - padding: 6px; - opacity: .7; - border: 1px solid #e0e0e0; - font-size: 12px; - position: absolute; - background: #fff; - white-space: nowrap -} -.rickshaw_graph .detail .x_label.left { - left: 0 -} -.rickshaw_graph .detail .x_label.right { - right: 0 -} -.rickshaw_graph .detail .item { - position: absolute; - z-index: 2; - border-radius: 3px; - padding: .25em; - font-size: 12px; - font-family: Arial, sans-serif; - opacity: 0; - background: rgba(0, 0, 0, .4); - color: #fff; - border: 1px solid rgba(0, 0, 0, .4); - margin-left: 1em; - margin-right: 1em; - margin-top: -1em; - white-space: nowrap -} -.rickshaw_graph .detail .item.left { - left: 0 -} -.rickshaw_graph .detail .item.right { - right: 0 -} -.rickshaw_graph .detail .item.active { - opacity: 1; - background: rgba(0, 0, 0, .8) -} -.rickshaw_graph .detail .item:after { - position: absolute; - display: block; - width: 0; - height: 0; - content: ""; - border: 5px solid transparent -} -.rickshaw_graph .detail .item.left:after { - top: 1em; - left: -5px; - margin-top: -5px; - border-right-color: rgba(0, 0, 0, .8); - border-left-width: 0 -} -.rickshaw_graph .detail .item.right:after { - top: 1em; - right: -5px; - margin-top: -5px; - border-left-color: rgba(0, 0, 0, .8); - border-right-width: 0 -} -.rickshaw_graph .detail .dot { - width: 4px; - height: 4px; - margin-left: -3px; - margin-top: -3.5px; - border-radius: 5px; - position: absolute; - box-shadow: 0 0 2px rgba(0, 0, 0, .6); - box-sizing: content-box; - background: #fff; - border-width: 2px; - border-style: solid; - display: none; - background-clip: padding-box -} -.rickshaw_graph .detail .dot.active { - display: block -} -.rickshaw_graph { - position: relative -} -.rickshaw_graph svg { - display: block; - overflow: hidden -} -.rickshaw_graph .x_tick { - position: absolute; - top: 0; - bottom: 0; - width: 0; - border-left: 1px dotted rgba(0, 0, 0, .5); - pointer-events: none -} -.rickshaw_graph .x_tick .title { - position: absolute; - font-family: Arial, sans-serif; - white-space: nowrap; - margin-left: 3px; - bottom: 1px -} -.rickshaw_graph .y_axis, -.rickshaw_graph .x_axis_d3 { - fill: none -} -.rickshaw_graph .y_ticks .tick line, -.rickshaw_graph .x_ticks_d3 .tick { - stroke: rgba(0, 0, 0, .16); - stroke-width: 2px; - shape-rendering: crisp-edges; - pointer-events: none -} -.rickshaw_graph .y_grid .tick, -.rickshaw_graph .x_grid_d3 .tick { - z-index: -1; - stroke: rgba(0, 0, 0, .2); - stroke-width: 1px; - stroke-dasharray: 1 1 -} -.rickshaw_graph .y_grid .tick[data-y-value="0"] { - stroke-dasharray: 1 0 -} -.rickshaw_graph .y_grid path, -.rickshaw_graph .x_grid_d3 path { - fill: none; - stroke: none -} -.rickshaw_graph .y_ticks path, -.rickshaw_graph .x_ticks_d3 path { - fill: none; - stroke: gray -} -.rickshaw_graph .y_ticks text, -.rickshaw_graph .x_ticks_d3 text { - opacity: .7; - font-size: 12px; - pointer-events: none -} -.rickshaw_graph .x_tick.glow .title, -.rickshaw_graph .y_ticks.glow text { - fill: #000; - color: #000; - text-shadow: -1px 1px 0 rgba(255, 255, 255, .1), 1px -1px 0 rgba(255, 255, 255, .1), 1px 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1), 0 -1px 0 rgba(255, 255, 255, .1), 1px 0 0 rgba(255, 255, 255, .1), -1px 0 0 rgba(255, 255, 255, .1), -1px -1px 0 rgba(255, 255, 255, .1) -} -.rickshaw_graph .x_tick.inverse .title, -.rickshaw_graph .y_ticks.inverse text { - fill: #fff; - color: #fff; - text-shadow: -1px 1px 0 rgba(0, 0, 0, .8), 1px -1px 0 rgba(0, 0, 0, .8), 1px 1px 0 rgba(0, 0, 0, .8), 0 1px 0 rgba(0, 0, 0, .8), 0 -1px 0 rgba(0, 0, 0, .8), 1px 0 0 rgba(0, 0, 0, .8), -1px 0 0 rgba(0, 0, 0, .8), -1px -1px 0 rgba(0, 0, 0, .8) -} -.rickshaw_legend { - font-family: Arial; - color: #fff; - background: #404040; - display: inline-block; - padding: 12px 5px; - border-radius: 2px; - position: relative -} -.rickshaw_legend:hover { - z-index: 10 -} -.rickshaw_legend .swatch { - width: 10px; - height: 10px; - border: 1px solid rgba(0, 0, 0, .2) -} -.rickshaw_legend .line { - clear: both; - line-height: 140%; - padding-right: 15px -} -.rickshaw_legend .line .swatch { - display: inline-block; - margin-right: 3px; - border-radius: 2px -} -.rickshaw_legend .label { - margin: 0; - white-space: nowrap; - display: inline; - font-size: inherit; - background-color: transparent; - color: inherit; - font-weight: 400; - line-height: normal; - padding: 0; - text-shadow: none -} -.rickshaw_legend .action:hover { - opacity: .6 -} -.rickshaw_legend .action { - margin-right: .2em; - opacity: .5; - cursor: pointer; -} -.rickshaw_legend .line.disabled { - opacity: .4 -} -.rickshaw_legend ul { - list-style-type: none; - margin: 0; - padding: 0; - margin: 2px; - cursor: pointer -} -.rickshaw_legend li { - padding: 0 0 0 2px; - min-width: 80px; - white-space: nowrap -} -.rickshaw_legend li:hover { - background: rgba(255, 255, 255, .08); - border-radius: 3px -} -.rickshaw_legend li:active { - background: rgba(255, 255, 255, .2); - border-radius: 3px -} - -.code-wrap { - white-space: normal; -} -.args { - overflow-y: auto; - max-height: 100px; - word-break: break-all; -} -.args-extended { - overflow-y: scroll; - max-height: 500px; - word-break: break-all; -} - - -/* BOOTSTRAP 3 FIXES */ -/* @grid-float-breakpoint -1 */ -.container { - padding: 0; -} -@media (max-width: 767px) { - .navbar-fixed-top, .navbar-fixed-bottom { - position: relative; - top: auto; - } -} - -.redis-url, .redis-namespace { - overflow: hidden; - white-space: nowrap; -} - -@media (min-width: 768px) { - .redis-url { - max-width: 200px; - } - - .redis-namespace { - max-width: 150px; - } -} - -@media (min-width: 992px) { - .redis-url { - max-width: 390px; - } - - .redis-namespace { - max-width: 300px; - } -} -@media (min-width: 1200px) { - .redis-url { - max-width: 480px; - } - - .redis-namespace { - max-width: 350px; - } -} - -.redis-url { - text-overflow: ellipsis; -} - -.product-version { - color:white; -} - -.warning-messages { - margin-top: 20px; - margin-bottom: 10px; -} - -.toggle { - display: none; -} - -.box { - width: 50%; -} - -.checkbox-column { - width: 20px; -} - -.delete-confirm { - width: 20%; -} - -.info-circle { - color: #ccc; - background-color: #000; - border-radius: 50%; - text-align: center; - vertical-align: middle; - padding: 3px 7px; - margin-left: 5px; -} diff --git a/web/assets/stylesheets/bootstrap-rtl.min.css b/web/assets/stylesheets/bootstrap-rtl.min.css deleted file mode 100644 index 7f3b13ee..00000000 --- a/web/assets/stylesheets/bootstrap-rtl.min.css +++ /dev/null @@ -1,9 +0,0 @@ -/******************************************************************************* - * bootstrap-rtl (version 3.3.4) - * Author: Morteza Ansarinia (http://github.com/morteza) - * Created on: August 13,2015 - * Project: bootstrap-rtl - * Copyright: Unlicensed Public Domain - *******************************************************************************/ - -html{direction:rtl}body{direction:rtl}.flip.text-left{text-align:right}.flip.text-right{text-align:left}.list-unstyled{padding-right:0;padding-left:initial}.list-inline{padding-right:0;padding-left:initial;margin-right:-5px;margin-left:0}dd{margin-right:0;margin-left:initial}@media (min-width:768px){.dl-horizontal dt{float:right;clear:right;text-align:left}.dl-horizontal dd{margin-right:180px;margin-left:0}}blockquote{border-right:5px solid #eee;border-left:0}.blockquote-reverse,blockquote.pull-left{padding-left:15px;padding-right:0;border-left:5px solid #eee;border-right:0;text-align:left}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:right}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{left:100%;right:auto}.col-xs-pull-11{left:91.66666667%;right:auto}.col-xs-pull-10{left:83.33333333%;right:auto}.col-xs-pull-9{left:75%;right:auto}.col-xs-pull-8{left:66.66666667%;right:auto}.col-xs-pull-7{left:58.33333333%;right:auto}.col-xs-pull-6{left:50%;right:auto}.col-xs-pull-5{left:41.66666667%;right:auto}.col-xs-pull-4{left:33.33333333%;right:auto}.col-xs-pull-3{left:25%;right:auto}.col-xs-pull-2{left:16.66666667%;right:auto}.col-xs-pull-1{left:8.33333333%;right:auto}.col-xs-pull-0{left:auto;right:auto}.col-xs-push-12{right:100%;left:0}.col-xs-push-11{right:91.66666667%;left:0}.col-xs-push-10{right:83.33333333%;left:0}.col-xs-push-9{right:75%;left:0}.col-xs-push-8{right:66.66666667%;left:0}.col-xs-push-7{right:58.33333333%;left:0}.col-xs-push-6{right:50%;left:0}.col-xs-push-5{right:41.66666667%;left:0}.col-xs-push-4{right:33.33333333%;left:0}.col-xs-push-3{right:25%;left:0}.col-xs-push-2{right:16.66666667%;left:0}.col-xs-push-1{right:8.33333333%;left:0}.col-xs-push-0{right:auto;left:0}.col-xs-offset-12{margin-right:100%;margin-left:0}.col-xs-offset-11{margin-right:91.66666667%;margin-left:0}.col-xs-offset-10{margin-right:83.33333333%;margin-left:0}.col-xs-offset-9{margin-right:75%;margin-left:0}.col-xs-offset-8{margin-right:66.66666667%;margin-left:0}.col-xs-offset-7{margin-right:58.33333333%;margin-left:0}.col-xs-offset-6{margin-right:50%;margin-left:0}.col-xs-offset-5{margin-right:41.66666667%;margin-left:0}.col-xs-offset-4{margin-right:33.33333333%;margin-left:0}.col-xs-offset-3{margin-right:25%;margin-left:0}.col-xs-offset-2{margin-right:16.66666667%;margin-left:0}.col-xs-offset-1{margin-right:8.33333333%;margin-left:0}.col-xs-offset-0{margin-right:0;margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:right}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{left:100%;right:auto}.col-sm-pull-11{left:91.66666667%;right:auto}.col-sm-pull-10{left:83.33333333%;right:auto}.col-sm-pull-9{left:75%;right:auto}.col-sm-pull-8{left:66.66666667%;right:auto}.col-sm-pull-7{left:58.33333333%;right:auto}.col-sm-pull-6{left:50%;right:auto}.col-sm-pull-5{left:41.66666667%;right:auto}.col-sm-pull-4{left:33.33333333%;right:auto}.col-sm-pull-3{left:25%;right:auto}.col-sm-pull-2{left:16.66666667%;right:auto}.col-sm-pull-1{left:8.33333333%;right:auto}.col-sm-pull-0{left:auto;right:auto}.col-sm-push-12{right:100%;left:0}.col-sm-push-11{right:91.66666667%;left:0}.col-sm-push-10{right:83.33333333%;left:0}.col-sm-push-9{right:75%;left:0}.col-sm-push-8{right:66.66666667%;left:0}.col-sm-push-7{right:58.33333333%;left:0}.col-sm-push-6{right:50%;left:0}.col-sm-push-5{right:41.66666667%;left:0}.col-sm-push-4{right:33.33333333%;left:0}.col-sm-push-3{right:25%;left:0}.col-sm-push-2{right:16.66666667%;left:0}.col-sm-push-1{right:8.33333333%;left:0}.col-sm-push-0{right:auto;left:0}.col-sm-offset-12{margin-right:100%;margin-left:0}.col-sm-offset-11{margin-right:91.66666667%;margin-left:0}.col-sm-offset-10{margin-right:83.33333333%;margin-left:0}.col-sm-offset-9{margin-right:75%;margin-left:0}.col-sm-offset-8{margin-right:66.66666667%;margin-left:0}.col-sm-offset-7{margin-right:58.33333333%;margin-left:0}.col-sm-offset-6{margin-right:50%;margin-left:0}.col-sm-offset-5{margin-right:41.66666667%;margin-left:0}.col-sm-offset-4{margin-right:33.33333333%;margin-left:0}.col-sm-offset-3{margin-right:25%;margin-left:0}.col-sm-offset-2{margin-right:16.66666667%;margin-left:0}.col-sm-offset-1{margin-right:8.33333333%;margin-left:0}.col-sm-offset-0{margin-right:0;margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:right}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{left:100%;right:auto}.col-md-pull-11{left:91.66666667%;right:auto}.col-md-pull-10{left:83.33333333%;right:auto}.col-md-pull-9{left:75%;right:auto}.col-md-pull-8{left:66.66666667%;right:auto}.col-md-pull-7{left:58.33333333%;right:auto}.col-md-pull-6{left:50%;right:auto}.col-md-pull-5{left:41.66666667%;right:auto}.col-md-pull-4{left:33.33333333%;right:auto}.col-md-pull-3{left:25%;right:auto}.col-md-pull-2{left:16.66666667%;right:auto}.col-md-pull-1{left:8.33333333%;right:auto}.col-md-pull-0{left:auto;right:auto}.col-md-push-12{right:100%;left:0}.col-md-push-11{right:91.66666667%;left:0}.col-md-push-10{right:83.33333333%;left:0}.col-md-push-9{right:75%;left:0}.col-md-push-8{right:66.66666667%;left:0}.col-md-push-7{right:58.33333333%;left:0}.col-md-push-6{right:50%;left:0}.col-md-push-5{right:41.66666667%;left:0}.col-md-push-4{right:33.33333333%;left:0}.col-md-push-3{right:25%;left:0}.col-md-push-2{right:16.66666667%;left:0}.col-md-push-1{right:8.33333333%;left:0}.col-md-push-0{right:auto;left:0}.col-md-offset-12{margin-right:100%;margin-left:0}.col-md-offset-11{margin-right:91.66666667%;margin-left:0}.col-md-offset-10{margin-right:83.33333333%;margin-left:0}.col-md-offset-9{margin-right:75%;margin-left:0}.col-md-offset-8{margin-right:66.66666667%;margin-left:0}.col-md-offset-7{margin-right:58.33333333%;margin-left:0}.col-md-offset-6{margin-right:50%;margin-left:0}.col-md-offset-5{margin-right:41.66666667%;margin-left:0}.col-md-offset-4{margin-right:33.33333333%;margin-left:0}.col-md-offset-3{margin-right:25%;margin-left:0}.col-md-offset-2{margin-right:16.66666667%;margin-left:0}.col-md-offset-1{margin-right:8.33333333%;margin-left:0}.col-md-offset-0{margin-right:0;margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:right}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{left:100%;right:auto}.col-lg-pull-11{left:91.66666667%;right:auto}.col-lg-pull-10{left:83.33333333%;right:auto}.col-lg-pull-9{left:75%;right:auto}.col-lg-pull-8{left:66.66666667%;right:auto}.col-lg-pull-7{left:58.33333333%;right:auto}.col-lg-pull-6{left:50%;right:auto}.col-lg-pull-5{left:41.66666667%;right:auto}.col-lg-pull-4{left:33.33333333%;right:auto}.col-lg-pull-3{left:25%;right:auto}.col-lg-pull-2{left:16.66666667%;right:auto}.col-lg-pull-1{left:8.33333333%;right:auto}.col-lg-pull-0{left:auto;right:auto}.col-lg-push-12{right:100%;left:0}.col-lg-push-11{right:91.66666667%;left:0}.col-lg-push-10{right:83.33333333%;left:0}.col-lg-push-9{right:75%;left:0}.col-lg-push-8{right:66.66666667%;left:0}.col-lg-push-7{right:58.33333333%;left:0}.col-lg-push-6{right:50%;left:0}.col-lg-push-5{right:41.66666667%;left:0}.col-lg-push-4{right:33.33333333%;left:0}.col-lg-push-3{right:25%;left:0}.col-lg-push-2{right:16.66666667%;left:0}.col-lg-push-1{right:8.33333333%;left:0}.col-lg-push-0{right:auto;left:0}.col-lg-offset-12{margin-right:100%;margin-left:0}.col-lg-offset-11{margin-right:91.66666667%;margin-left:0}.col-lg-offset-10{margin-right:83.33333333%;margin-left:0}.col-lg-offset-9{margin-right:75%;margin-left:0}.col-lg-offset-8{margin-right:66.66666667%;margin-left:0}.col-lg-offset-7{margin-right:58.33333333%;margin-left:0}.col-lg-offset-6{margin-right:50%;margin-left:0}.col-lg-offset-5{margin-right:41.66666667%;margin-left:0}.col-lg-offset-4{margin-right:33.33333333%;margin-left:0}.col-lg-offset-3{margin-right:25%;margin-left:0}.col-lg-offset-2{margin-right:16.66666667%;margin-left:0}.col-lg-offset-1{margin-right:8.33333333%;margin-left:0}.col-lg-offset-0{margin-right:0;margin-left:0}}caption{text-align:right}th{text-align:right}@media screen and (max-width:767px){.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-right:0;border-left:initial}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-left:0;border-right:initial}}.radio label,.checkbox label{padding-right:20px;padding-left:initial}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{margin-right:-20px;margin-left:auto}.radio-inline,.checkbox-inline{padding-right:20px;padding-left:0}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-right:10px;margin-left:0}.has-feedback .form-control{padding-left:42.5px;padding-right:12px}.form-control-feedback{left:0;right:auto}@media (min-width:768px){.form-inline label{padding-right:0;padding-left:initial}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{margin-right:0;margin-left:auto}}@media (min-width:768px){.form-horizontal .control-label{text-align:left}}.form-horizontal .has-feedback .form-control-feedback{left:15px;right:auto}.caret{margin-right:2px;margin-left:0}.dropdown-menu{right:0;left:auto;float:left;text-align:right}.dropdown-menu.pull-right{left:0;right:auto;float:right}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group>.btn,.btn-group-vertical>.btn{float:right}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-right:-1px;margin-left:0}.btn-toolbar{margin-right:-5px;margin-left:0}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:right}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-right:5px;margin-left:0}.btn-group>.btn:first-child{margin-right:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:4px;border-bottom-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:4px;border-bottom-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group{float:right}.btn-group.btn-group-justified>.btn,.btn-group.btn-group-justified>.btn-group{float:none}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:4px;border-bottom-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.btn .caret{margin-right:0}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-right:0}.input-group .form-control{float:right}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:4px;border-top-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:first-child{border-left:0;border-right:1px solid}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:last-child{border-left-width:1px;border-left-style:solid;border-right:0}.input-group-btn>.btn+.btn{margin-right:-1px;margin-left:auto}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-left:-1px;margin-right:auto}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-right:-1px;margin-left:auto}.nav{padding-right:0;padding-left:initial}.nav-tabs>li{float:right}.nav-tabs>li>a{margin-left:auto;margin-right:-2px;border-radius:4px 4px 0 0}.nav-pills>li{float:right}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-right:2px;margin-left:auto}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-right:0;margin-left:auto}.nav-justified>.dropdown .dropdown-menu{right:auto}.nav-tabs-justified>li>a{margin-left:0;margin-right:auto}@media (min-width:768px){.nav-tabs-justified>li>a{border-radius:4px 4px 0 0}}@media (min-width:768px){.navbar-header{float:right}}.navbar-collapse{padding-right:15px;padding-left:15px}.navbar-brand{float:right}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-right:-15px;margin-left:auto}}.navbar-toggle{float:left;margin-left:15px;margin-right:auto}@media (max-width:767px){.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 25px 5px 15px}}@media (min-width:768px){.navbar-nav{float:right}.navbar-nav>li{float:right}}@media (min-width:768px){.navbar-left.flip{float:right!important}.navbar-right:last-child{margin-left:-15px;margin-right:auto}.navbar-right.flip{float:left!important;margin-left:-15px;margin-right:auto}.navbar-right .dropdown-menu{left:0;right:auto}}@media (min-width:768px){.navbar-text{float:right}.navbar-text.navbar-right:last-child{margin-left:0;margin-right:auto}}.pagination{padding-right:0}.pagination>li>a,.pagination>li>span{float:right;margin-right:-1px;margin-left:0}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-right-radius:4px;border-top-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{margin-right:-1px;border-bottom-left-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.pager{padding-right:0;padding-left:initial}.pager .next>a,.pager .next>span{float:left}.pager .previous>a,.pager .previous>span{float:right}.nav-pills>li>a>.badge{margin-left:0;margin-right:3px}.list-group-item>.badge{float:left}.list-group-item>.badge+.badge{margin-left:5px;margin-right:auto}.alert-dismissable,.alert-dismissible{padding-left:35px;padding-right:15px}.alert-dismissable .close,.alert-dismissible .close{right:auto;left:-21px}.progress-bar{float:right}.media>.pull-left{margin-right:10px}.media>.pull-left.flip{margin-right:0;margin-left:10px}.media>.pull-right{margin-left:10px}.media>.pull-right.flip{margin-left:0;margin-right:10px}.media-right,.media>.pull-right{padding-right:10px;padding-left:initial}.media-left,.media>.pull-left{padding-left:10px;padding-right:initial}.media-list{padding-right:0;padding-left:initial;list-style:none}.list-group{padding-right:0;padding-left:initial}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-right-radius:3px;border-top-left-radius:0}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-left-radius:3px;border-top-right-radius:0}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px;border-top-right-radius:0}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px;border-top-left-radius:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-right:0;border-left:none}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:none;border-left:0}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{right:0;left:auto}.close{float:left}.modal-footer{text-align:left}.modal-footer.flip{text-align:right}.modal-footer .btn+.btn{margin-left:auto;margin-right:5px}.modal-footer .btn-group .btn+.btn{margin-right:-1px;margin-left:auto}.modal-footer .btn-block+.btn-block{margin-right:0;margin-left:auto}.popover{left:auto;text-align:right}.popover.top>.arrow{right:50%;left:auto;margin-right:-11px;margin-left:auto}.popover.top>.arrow:after{margin-right:-10px;margin-left:auto}.popover.bottom>.arrow{right:50%;left:auto;margin-right:-11px;margin-left:auto}.popover.bottom>.arrow:after{margin-right:-10px;margin-left:auto}.carousel-control{right:0;bottom:0}.carousel-control.left{right:auto;left:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;right:auto;margin-right:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;left:auto;margin-left:-10px}.carousel-indicators{right:50%;left:0;margin-right:-30%;margin-left:0;padding-left:0}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:0;margin-right:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-left:0;margin-right:-15px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}}.pull-right.flip{float:left!important}.pull-left.flip{float:right!important} \ No newline at end of file diff --git a/web/assets/stylesheets/bootstrap.css b/web/assets/stylesheets/bootstrap.css deleted file mode 100644 index 6b8c2562..00000000 --- a/web/assets/stylesheets/bootstrap.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - Glyphicons - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.label,sub,sup{vertical-align:baseline}hr,img{border:0}body,figure{margin:0}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.dropdown-menu{float:left}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{background:0 0!important;color:#000!important;-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.btn,.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.form-control,.navbar-toggle,.open>.dropdown-toggle.btn-danger,.open>.dropdown-toggle.btn-default,.open>.dropdown-toggle.btn-info,.open>.dropdown-toggle.btn-primary,.open>.dropdown-toggle.btn-warning{background-image:none}.img-thumbnail,body{background-color:#fff}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}dt,kbd kbd,label{font-weight:700}address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}pre code,table{background-color:transparent}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}dl,ol,ul{margin-top:0}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}address,dl{margin-bottom:20px}ol,ul{margin-bottom:10px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.container{width:750px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;color:#777}legend,pre{display:block;color:#333}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}code,kbd{padding:2px 4px;font-size:90%}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{font-style:normal}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;-webkit-box-shadow:none;box-shadow:none}pre{padding:9.5px;margin:0 0 10px;font-size:13px;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}.container,.container-fluid{margin-right:auto;margin-left:auto}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-left:15px;padding-right:15px}.pre-scrollable{overflow-y:scroll}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.row{margin-left:-15px;margin-right:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}caption{padding-top:8px;padding-bottom:8px;color:#777}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{margin:0;min-width:0}legend{width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}.form-control,output{font-size:14px;line-height:1.42857143;color:#555;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:7px}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\9}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.form-group-sm .form-control,.input-sm{padding:5px 10px;border-radius:3px;font-size:12px}.input-sm{height:30px;line-height:1.5}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;line-height:1.5}.form-group-lg .form-control,.input-lg{border-radius:6px;padding:10px 16px;font-size:18px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;line-height:1.3333333}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;line-height:1.3333333}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle,.btn.active,.btn:active,.dropdown-toggle:focus,.navbar-toggle:focus,.open>a{outline:0}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary.active,.btn-primary:active,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success.active,.btn-success:active,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info.active,.btn-info:active,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:400;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu-right,.dropdown-menu.pull-right{right:0;left:auto}.dropdown-header,.dropdown-menu>li>a{display:block;padding:3px 20px;line-height:1.42857143;white-space:nowrap}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{clear:both;font-weight:400;color:#333}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.dropdown-menu-left{right:auto;left:0}.dropdown-header{font-size:12px;color:#777}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.nav>li,.nav>li>a{display:block;position:relative}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px;margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0;border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li{float:none}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-static-top{border-radius:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:768px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:8px -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.breadcrumb>li,.pagination{display:inline-block}.btn .badge,.btn .label{top:-1px;position:relative}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{padding-left:0;margin:20px 0;border-radius:4px}.pager li,.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#337ab7;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;background-color:#337ab7;border-color:#337ab7;cursor:default}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.badge,.label{font-weight:700;line-height:1;white-space:nowrap;text-align:center}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}a.badge:focus,a.badge:hover,a.label:focus,a.label:hover{color:#fff;cursor:pointer;text-decoration:none}.label{display:inline;padding:.2em .6em .3em;font-size:75%;color:#fff;border-radius:.25em}.label:empty{display:none}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;color:#fff;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.media-object,.thumbnail{display:block}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.center-block,.thumbnail a>img,.thumbnail>img{margin-left:auto;margin-right:auto}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;background-color:#eee}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.alert,.thumbnail{margin-bottom:20px}.alert .alert-link,.close{font-weight:700}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-title{margin-top:0;font-size:16px}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{content:" ";display:table}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.affix{position:fixed}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}} \ No newline at end of file diff --git a/web/locales/ar.yml b/web/locales/ar.yml deleted file mode 100644 index a9143ac4..00000000 --- a/web/locales/ar.yml +++ /dev/null @@ -1,87 +0,0 @@ -# elements like %{queue} are variables and should not be translated -ar: - TextDirection: 'rtl' - Dashboard: لوحة التحكم - Status: حالة - Time: وقت - Namespace: مساحة الاسم - Realtime: الزمن الفعلي - History: تاريخ - Busy: مشغول - Utilization: الاستخدام - Processed: تمت المعالجة - Failed: فشل - Scheduled: مجدول - Retries: إعادة محاولة - Enqueued: في الرتل - Worker: عامل - LivePoll: استعلام مباشر - StopPolling: إيقاف الاستعلامات - Queue: رتل - Class: نوع - Job: وظيفة - Arguments: مدخلات - Extras: إضافات - Started: بدأت - ShowAll: عرض الكل - CurrentMessagesInQueue: الرسائل الحالية في الرتل %{queue} - AddToQueue: إضافة إلى الرتل - AreYouSureDeleteJob: هل أنت متأكد من حذف الوظيفة؟ - AreYouSureDeleteQueue: هل أنت متأكد من حذف الرتل %{queue}؟ - Delete: حذف - Queues: أرتال - Size: حجم - Actions: إجراءات - NextRetry: إعادة المحاولة القادمة - RetryCount: عدد مرات إعادة المحاولة - RetryNow: إعادة المحاولة الآن - Kill: إبطال - LastRetry: إعادة المحاولة الأخيرة - OriginallyFailed: فشل أصلا - AreYouSure: هل انت متأكد؟ - DeleteAll: حذف الكل - RetryAll: إعادة المحاولة للكل - KillAll: إبطال الكل - NoRetriesFound: لاتوجد أي إعادة محاولة - Error: خطأ - ErrorClass: نوع الخطأ - ErrorMessage: رسالة الخطأ - ErrorBacktrace: تتبع الخطأ - GoBack: إلى الخلف - NoScheduledFound: لايوجد وظائف مجدولة - When: متى - ScheduledJobs: وظائف مجدولة - idle: خامل - active: نشيط - Version: إصدار - Connections: اتصالات - MemoryUsage: استخدام الذاكرة - PeakMemoryUsage: ذروة استخدام الذاكرة - Uptime: زمن العمل - OneWeek: أسبوع - OneMonth: شهر - ThreeMonths: ثلاثة أشهر - SixMonths: ستة أشهر - Failures: فشل - DeadJobs: وظائف ميتة - NoDeadJobsFound: لاتوجد وظائف ميتة - Dead: ميتة - Process: عملية - Processes: عمليات - Name: الاسم - Thread: نيسب - Threads: نياسب - Jobs: وظائف - Paused: موقفة مؤقتاً - Stop: إيقاف - Quiet: هدوء - StopAll: إيقاف الكل - QuietAll: هدوء الكل - PollingInterval: الفاصل الزمني بين الاستعلامات - Plugins: الإضافات - NotYetEnqueued: لم تدخل الرتل بعد - CreatedAt: أنشئت في - BackToApp: العودة إلى التطبيق - Latency: زمن الانتظار - Pause: إيقاف مؤقت - Unpause: متابعة \ No newline at end of file diff --git a/web/locales/cs.yml b/web/locales/cs.yml deleted file mode 100644 index d258b30c..00000000 --- a/web/locales/cs.yml +++ /dev/null @@ -1,78 +0,0 @@ -# elements like %{queue} are variables and should not be translated -cs: - Dashboard: Kontrolní panel - Status: Stav - Time: Čas - Namespace: Jmenný prostor - Realtime: V reálném čase - History: Historie - Busy: Zaneprázdněné - Processed: Zpracované - Failed: Nezdařené - Scheduled: Naplánované - Retries: Opakování - Enqueued: Zařazené - Worker: Worker - LivePoll: Průběžně aktualizovat - StopPolling: Zastavit průběžnou aktualizaci - Queue: Fronta - Class: Třída - Job: Úkol - Arguments: Argumenty - Extras: Doplňky - Started: Začal - ShowAll: Ukázat všechny - CurrentMessagesInQueue: Aktuální úkoly ve frontě %{queue} - Delete: Odstranit - AddToQueue: Přidat do fronty - AreYouSureDeleteJob: Jste si jisti, že chcete odstranit tento úkol? - AreYouSureDeleteQueue: Jste si jisti, že chcete odstranit frontu %{queue}? - Queues: Fronty - Size: Velikost - Actions: Akce - NextRetry: Další opakování - RetryCount: Počet opakování - RetryNow: Opakovat teď - Kill: Zabít - LastRetry: Poslední opakování - OriginallyFailed: Původně se nezdařilo - AreYouSure: Jste si jisti? - DeleteAll: Odstranit vše - RetryAll: Opakovat vše - NoRetriesFound: Nebyla nalezena žádná opakování - Error: Chyba - ErrorClass: Třída chyby - ErrorMessage: Chybová zpráva - ErrorBacktrace: Chybový výstup - GoBack: ← Zpět - NoScheduledFound: Nebyly nalezeny žádné naplánované úkoly - When: Kdy - ScheduledJobs: Naplánované úkoly - idle: nečinný - active: aktivní - Version: Verze - Connections: Připojení - MemoryUsage: Využití paměti - PeakMemoryUsage: Nejvyšší využití paměti - Uptime: Uptime (dny) - OneWeek: 1 týden - OneMonth: 1 měsíc - ThreeMonths: 3 měsíce - SixMonths: 6 měsíců - Failures: Selhání - DeadJobs: Mrtvé úkoly - NoDeadJobsFound: Nebyly nalezeny žádné mrtvé úkoly - Dead: Mrtvé - Processes: Procesy - Thread: Vlákno - Threads: Vlákna - Jobs: Úkoly - Paused: Pozastavené - Stop: Zastavit - Quiet: Ztišit - StopAll: Zastavit vše - QuietAll: Ztišit vše - PollingInterval: Interval obnovení - Plugins: Doplňky - NotYetEnqueued: Ještě nezařazeno - CreatedAt: Vytvořeno diff --git a/web/locales/da.yml b/web/locales/da.yml deleted file mode 100644 index 0ec298c0..00000000 --- a/web/locales/da.yml +++ /dev/null @@ -1,68 +0,0 @@ -# elements like %{queue} are variables and should not be translated -da: - Dashboard: Instrumentbræt - Status: Status - Time: Tid - Namespace: Namespace - Realtime: Real-time - History: Historik - Busy: Travl - Processed: Processeret - Failed: Fejlet - Scheduled: Planlagt - Retries: Forsøg - Enqueued: I kø - Worker: Arbejder - LivePoll: Live Poll - StopPolling: Stop Polling - Queue: Kø - Class: Klasse - Job: Job - Arguments: Argumenter - Extras: Ekstra - Started: Startet - ShowAll: Vis alle - CurrentMessagesInQueue: Nuværende beskeder i %{queue} - Delete: Slet - AddToQueue: Tilføj til kø - AreYouSureDeleteJob: Er du sikker på at du vil slette dette job? - AreYouSureDeleteQueue: Er du sikker på at du vil slette %{queue} køen? - Queues: Køer - Size: Størrelse - Actions: Actions - NextRetry: Næste forsøg - RetryCount: Antal forsøg - RetryNow: Prøv igen nu - LastRetry: Sidste forsøg - OriginallyFailed: Oprindeligt fejlet - AreYouSure: Er du sikker? - DeleteAll: Slet alle - RetryAll: Forsøg alle - NoRetriesFound: Ingen gen-forsøg var fundet - Error: Fejl - ErrorClass: Fejl klasse - ErrorMessage: Fejl besked - ErrorBacktrace: Fejl backtrace - GoBack: ← Tilbage - NoScheduledFound: Ingen jobs i kø fundet - When: Når - ScheduledJobs: Jobs i kø - idle: idle - active: aktiv - Version: Version - Connections: Forbindelser - MemoryUsage: RAM forbrug - PeakMemoryUsage: Peak RAM forbrug - Uptime: Oppetid (dage) - OneWeek: 1 uge - OneMonth: 1 måned - ThreeMonths: 3 måneder - SixMonths: 6 måneder - Failures: Fejl - DeadJobs: Døde jobs - NoDeadJobsFound: Ingen døde jobs fundet - Dead: Død - Processes: Processer - Thread: Tråd - Threads: Tråde - Jobs: Jobs diff --git a/web/locales/de.yml b/web/locales/de.yml deleted file mode 100644 index 2caad4ae..00000000 --- a/web/locales/de.yml +++ /dev/null @@ -1,81 +0,0 @@ -# elements like %{queue} are variables and should not be translated -de: - Dashboard: Dashboard - Status: Status - Time: Zeit - Namespace: Namensraum - Realtime: Echtzeit - History: Verlauf - Busy: Beschäftigt - Processed: Verarbeitet - Failed: Fehlgeschlagen - Scheduled: Geplant - Retries: Versuche - Enqueued: In der Warteschlange - Worker: Arbeiter - LivePoll: Echtzeitabfrage - StopPolling: Abfrage stoppen - Queue: Warteschlange - Class: Klasse - Job: Job - Extras: Extras - Arguments: Argumente - Started: Gestartet - ShowAll: Alle anzeigen - CurrentMessagesInQueue: Aktuelle Nachrichten in %{queue} - Delete: Löschen - AddToQueue: In Warteschlange einreihen - AreYouSureDeleteJob: Möchtest du diesen Job wirklich löschen? - AreYouSureDeleteQueue: Möchtest du %{queue} wirklich löschen? - Queues: Warteschlangen - Size: Größe - Actions: Aktionen - NextRetry: Nächster Versuch - RetryCount: Anzahl der Versuche - RetryNow: Jetzt erneut versuchen - Kill: Vernichten - LastRetry: Letzter Versuch - OriginallyFailed: Ursprünglich fehlgeschlagen - AreYouSure: Bist du sicher? - DeleteAll: Alle löschen - RetryAll: Alle erneut versuchen - KillAll: Alle vernichten - NoRetriesFound: Keine erneuten Versuche gefunden - Error: Fehler - ErrorClass: Fehlerklasse - ErrorMessage: Fehlernachricht - ErrorBacktrace: Fehlerbericht - GoBack: ← Zurück - NoScheduledFound: Keine geplanten Jobs gefunden - When: Wann - ScheduledJobs: Jobs in der Warteschlange - idle: untätig - active: aktiv - Version: Version - Connections: Verbindungen - MemoryUsage: RAM-Nutzung - PeakMemoryUsage: Maximale RAM-Nutzung - Uptime: Laufzeit - OneWeek: 1 Woche - OneMonth: 1 Monat - ThreeMonths: 3 Monate - SixMonths: 6 Monate - Failures: Ausfälle - DeadJobs: Gestorbene Jobs - NoDeadJobsFound: Keine toten Jobs gefunden - Dead: Tot - Processes: Prozesse - Thread: Thread - Threads: Threads - Jobs: Jobs - Paused: Pausiert - Stop: Stopp - Quiet: Leise - StopAll: Alle stoppen - QuietAll: Alle leise - PollingInterval: Abfrageintervall - Plugins: Erweiterungen - NotYetEnqueued: Noch nicht in der Warteschlange - CreatedAt: Erstellt - BackToApp: Zurück zur Anwendung - Latency: Latenz diff --git a/web/locales/el.yml b/web/locales/el.yml deleted file mode 100644 index 65d38f5e..00000000 --- a/web/locales/el.yml +++ /dev/null @@ -1,68 +0,0 @@ -# elements like %{queue} are variables and should not be translated -el: # <---- change this to your locale code - Dashboard: Πίνακας Ελέγχου - Status: Κατάσταση - Time: Χρόνος - Namespace: Namespace - Realtime: Τρέχουσα Κατάσταση - History: Ιστορικό - Busy: Απασχολημένο - Processed: Επεξεργάστηκε - Failed: Απέτυχε - Scheduled: Προγραματίστηκε - Retries: Προσπάθειες - Enqueued: Μπήκαν στην στοίβα - Worker: Εργάτης - LivePoll: Τρέχουσα Κατάσταση - StopPolling: Διακοπή Τρέχουσας Κατάστασης - Queue: Στοίβα - Class: Κλάση - Job: Εργασία - Arguments: Ορίσματα - Extras: Extras - Started: Ξεκίνησαν - ShowAll: Εμφάνιση Όλων - CurrentMessagesInQueue: Τρέχουσες εργασίες %{queue} - Delete: Διαγραφή - AddToQueue: Προσθήκη στην στοίβα - AreYouSureDeleteJob: Θέλετε να διαγράψετε την εργασία αυτη; - AreYouSureDeleteQueue: Θέλετε να διαγράψετε την %{queue} στοίβα? - Queues: Στοίβες - Size: Μέγεθος - Actions: Ενέργειες - NextRetry: Επόμενη προσπάθεια - RetryCount: Αριθμός προσπαθειών - RetryNow: Προσπάθησε τώρα - LastRetry: Τελευταία προσπάθεια - OriginallyFailed: Αρχικές Αποτυχίες - AreYouSure: Είστε σίγουρος? - DeleteAll: Διαγραφή όλων - RetryAll: Επανάληψη Όλων - NoRetriesFound: Δεν βρέθηκαν προσπάθειες - Error: Σφάλμα - ErrorClass: Κλάση σφάλματος - ErrorMessage: Μήνυμα Σφάλματος - ErrorBacktrace: Σφάλμα Backtrace - GoBack: ← Πίσω - NoScheduledFound: Δεν βρέθηκαν προγραμματισμένες εργασίες - When: Πότε - ScheduledJobs: Προγραμματισμένες Εργασίες - idle: αδρανής - active: ενεργή - Version: Έκδοση - Connections: Συνδέσεις - MemoryUsage: Χρήση Μνήμης - PeakMemoryUsage: Μέγιστη Χρήση Μνήμης - Uptime: Διάρκεια Λειτουργείας (ημέρες) - OneWeek: 1 εβδομάδα - OneMonth: 1 μήνας - ThreeMonths: 3 μήνες - SixMonths: 6 μήνες - Failures: Αποτυχίες - DeadJobs: Αδρανείς Εργασίες - NoDeadJobsFound: Δεν βρέθηκαν αδρανείς εργασίες - Dead: Αδρανείς - Processes: Διεργασίες - Thread: Νήμα - Threads: Νήματα - Jobs: Εργασίες diff --git a/web/locales/en.yml b/web/locales/en.yml deleted file mode 100644 index 35159eab..00000000 --- a/web/locales/en.yml +++ /dev/null @@ -1,86 +0,0 @@ -# elements like %{queue} are variables and should not be translated -en: # <---- change this to your locale code - Dashboard: Dashboard - Status: Status - Time: Time - Namespace: Namespace - Realtime: Real-time - History: History - Busy: Busy - Utilization: Utilization - Processed: Processed - Failed: Failed - Scheduled: Scheduled - Retries: Retries - Enqueued: Enqueued - Worker: Worker - LivePoll: Live Poll - StopPolling: Stop Polling - Queue: Queue - Class: Class - Job: Job - Arguments: Arguments - Extras: Extras - Started: Started - ShowAll: Show All - CurrentMessagesInQueue: Current jobs in %{queue} - Delete: Delete - AddToQueue: Add to queue - AreYouSureDeleteJob: Are you sure you want to delete this job? - AreYouSureDeleteQueue: Are you sure you want to delete the %{queue} queue? This will delete all jobs within the queue, it will reappear if you push more jobs to it in the future. - Queues: Queues - Size: Size - Actions: Actions - NextRetry: Next Retry - RetryCount: Retry Count - RetryNow: Retry Now - Kill: Kill - LastRetry: Last Retry - OriginallyFailed: Originally Failed - AreYouSure: Are you sure? - DeleteAll: Delete All - RetryAll: Retry All - KillAll: Kill All - NoRetriesFound: No retries were found - Error: Error - ErrorClass: Error Class - ErrorMessage: Error Message - ErrorBacktrace: Error Backtrace - GoBack: ← Back - NoScheduledFound: No scheduled jobs were found - When: When - ScheduledJobs: Scheduled Jobs - idle: idle - active: active - Version: Version - Connections: Connections - MemoryUsage: Memory Usage - PeakMemoryUsage: Peak Memory Usage - Uptime: Uptime (days) - OneWeek: 1 week - OneMonth: 1 month - ThreeMonths: 3 months - SixMonths: 6 months - Failures: Failures - DeadJobs: Dead Jobs - NoDeadJobsFound: No dead jobs were found - Dead: Dead - Process: Process - Processes: Processes - Name: Name - Thread: Thread - Threads: Threads - Jobs: Jobs - Paused: Paused - Stop: Stop - Quiet: Quiet - StopAll: Stop All - QuietAll: Quiet All - PollingInterval: Polling interval - Plugins: Plugins - NotYetEnqueued: Not yet enqueued - CreatedAt: Created At - BackToApp: Back to App - Latency: Latency - Pause: Pause - Unpause: Unpause diff --git a/web/locales/es.yml b/web/locales/es.yml deleted file mode 100644 index 1f0ed522..00000000 --- a/web/locales/es.yml +++ /dev/null @@ -1,86 +0,0 @@ -# elements like %{queue} are variables and should not be translated -es: - Dashboard: Panel de Control - Status: Estatus - Time: Tiempo - Namespace: Espacio de Nombre - Realtime: Tiempo Real - History: Historial - Busy: Ocupado - Utilization: Utilización - Processed: Procesadas - Failed: Fallidas - Scheduled: Programadas - Retries: Reintentos - Enqueued: En Cola - Worker: Trabajador - LivePoll: Sondeo en Vivo - StopPolling: Detener Sondeo - Queue: Cola - Class: Clase - Job: Trabajo - Arguments: Argumentos - Extras: Extras - Started: Hora de Inicio - ShowAll: Mostrar Todo - CurrentMessagesInQueue: Mensajes actualmente en %{queue} - Delete: Eliminar - AddToQueue: Añadir a la cola - AreYouSureDeleteJob: ¿Estás seguro de eliminar este trabajo? - AreYouSureDeleteQueue: ¿Estás seguro de eliminar la cola %{queue}? - Queues: Colas - Size: Tamaño - Actions: Acciones - NextRetry: Siguiente Intento - RetryCount: Numero de Reintentos - RetryNow: Reintentar Ahora - Kill: Matar - LastRetry: Último Reintento - OriginallyFailed: Falló Originalmente - AreYouSure: ¿Estás seguro? - DeleteAll: Borrar Todo - RetryAll: Reintentar Todo - KillAll: Matar Todo - NoRetriesFound: No se encontraron reintentos - Error: Error - ErrorClass: Clase del Error - ErrorMessage: Mensaje de Error - ErrorBacktrace: Trazado del Error - GoBack: ← Regresar - NoScheduledFound: No se encontraron trabajos pendientes - When: Cuando - ScheduledJobs: Trabajos programados - idle: inactivo - active: activo - Version: Versión - Connections: Conexiones - MemoryUsage: Uso de Memoria - PeakMemoryUsage: Máximo Uso de Memoria - Uptime: Tiempo de Funcionamiento (días) - OneWeek: 1 semana - OneMonth: 1 mes - ThreeMonths: 3 meses - SixMonths: 6 meses - Failures: Fallas - DeadJobs: Trabajos muertos - NoDeadJobsFound: No hay trabajos muertos - Dead: Muerto - Process: Proceso - Processes: Procesos - Name: Nombre - Thread: Hilo - Threads: Hilos - Jobs: Trabajos - Paused: Pausado - Stop: Detener - Quiet: Silenciar - StopAll: Detener Todo - QuietAll: Silenciar Todo - PollingInterval: Intervalo de Sondeo - Plugins: Plugins - NotYetEnqueued: Aún no en cola - CreatedAt: Creado en - BackToApp: Volver a la Aplicación - Latency: Latencia - Pause: Pausar - Unpause: Reanudar diff --git a/web/locales/fa.yml b/web/locales/fa.yml deleted file mode 100644 index f3ec44e2..00000000 --- a/web/locales/fa.yml +++ /dev/null @@ -1,80 +0,0 @@ -# elements like %{queue} are variables and should not be translated -fa: # <---- change this to your locale code - TextDirection: 'rtl' - Dashboard: داشبورد - Status: اعلان - Time: رمان - Namespace: فضای نام - Realtime: زنده - History: تاریخچه - Busy: مشغول - Processed: پردازش شده - Failed: ناموفق - Scheduled: زمان بندی - Retries: تکرار - Enqueued: صف بندی نشدند - Worker: کارگزار - LivePoll: Live Poll - StopPolling: Stop Polling - Queue: صف - Class: کلاس - Job: کار - Arguments: آرگومنت - Extras: اضافی - Started: شروع شده - ShowAll: نمایش همه - CurrentMessagesInQueue: کار فعلی در %{queue} - Delete: حذف - AddToQueue: افزودن به صف - AreYouSureDeleteJob: آیا شما مطمعن هستید از حذف این کار ؟ - AreYouSureDeleteQueue: ایا شما مطمعنید از حذف %{queue} ? - Queues: صف ها - Size: سایز - Actions: اعمال - NextRetry: بار دیگر تلاش کنید - RetryCount: تعداد تلاش ها - RetryNow: تلاش مجدد - Kill: کشتن - LastRetry: آخرین تلاش - OriginallyFailed: Originally Failed - AreYouSure: آیا مطمعن هستید? - DeleteAll: حذف همه - RetryAll: تلاش مجدد برای همه - NoRetriesFound: هیچ تلاش پیدا نشد - Error: خطا - ErrorClass: خطا کلاس - ErrorMessage: پیغام خطا - ErrorBacktrace: خطای معکوس - GoBack: ← برگشت - NoScheduledFound: هیچ کار برنامه ریزی شده ای یافت نشد - When: وقتی که - ScheduledJobs: کار برنامه ریزی شده - idle: بیهودی - active: فعال - Version: ورژن - Connections: ارتباطات - MemoryUsage: حافظه استفاده شده - PeakMemoryUsage: اوج حافظه استفاده شده - Uptime: آپ تایم (روز) - OneWeek: ۱ هفته - OneMonth: ۱ ماه - ThreeMonths: ۳ ماه - SixMonths: ۶ ماه - Failures: شکست ها - DeadJobs: کار مرده - NoDeadJobsFound: کار مرده ای یافت نشد - Dead: مرده - Processes: پردازش ها - Thread: رشته - Threads: رشته ها - Jobs: کار ها - Paused: مکث - Stop: توقف - Quiet: خروج - StopAll: توقف همه - QuietAll: خروج همه - PollingInterval: Polling interval - Plugins: پلاگین ها - NotYetEnqueued: بدون صف بندی - CreatedAt: ساخته شده در - BackToApp: برگشت به برنامه diff --git a/web/locales/fr.yml b/web/locales/fr.yml deleted file mode 100644 index db402b56..00000000 --- a/web/locales/fr.yml +++ /dev/null @@ -1,85 +0,0 @@ -# elements like %{queue} are variables and should not be translated -fr: - Dashboard: Tableau de Bord - Status: État - Time: Heure - Namespace: Namespace - Realtime: Temps réel - History: Historique - Busy: En cours - Utilization: Utilisation - Processed: Traitées - Failed: Échouées - Scheduled: Planifiées - Retries: Tentatives - Enqueued: En attente - Worker: Travailleur - LivePoll: Temps réel - StopPolling: Arrêt du temps réel - Queue: Queue - Class: Classe - Job: Tâche - Arguments: Arguments - Extras: Extras - Started: Démarrée - ShowAll: Tout montrer - CurrentMessagesInQueue: Messages actuellement dans %{queue} - Delete: Supprimer - AddToQueue: Ajouter à la queue - AreYouSureDeleteJob: Êtes-vous certain de vouloir supprimer cette tâche ? - AreYouSureDeleteQueue: Êtes-vous certain de vouloir supprimer la queue %{queue} ? - Queues: Queues - Size: Taille - Actions: Actions - NextRetry: Prochain essai - RetryCount: Nombre d'essais - RetryNow: Réessayer maintenant - Kill: Tuer - LastRetry: Dernier essai - OriginallyFailed: Échec initial - AreYouSure: Êtes-vous certain ? - DeleteAll: Tout supprimer - RetryAll: Tout réessayer - KillAll: Tout tuer - NoRetriesFound: Aucune tâche à réessayer n’a été trouvée - Error: Erreur - ErrorClass: Classe d’erreur - ErrorMessage: Message d’erreur - ErrorBacktrace: Backtrace d’erreur - GoBack: ← Retour - NoScheduledFound: Aucune tâche planifiée n'a été trouvée - When: Quand - ScheduledJobs: Tâches planifiées - idle: inactif - active: actif - Version: Version - Connections: Connexions - MemoryUsage: Mémoire utilisée - PeakMemoryUsage: Mémoire utilisée (max.) - Uptime: Uptime (jours) - OneWeek: 1 semaine - OneMonth: 1 mois - ThreeMonths: 3 mois - SixMonths: 6 mois - Failures: Echecs - DeadJobs: Tâches mortes - NoDeadJobsFound: Aucune tâche morte n'a été trouvée - Dead: Mortes - Process: Processus - Processes: Processus - Thread: Thread - Threads: Threads - Jobs: Tâches - Paused: Mise en pause - Stop: Arrêter - Quiet: Clore - StopAll: Tout arrêter - QuietAll: Tout clore - PollingInterval: Intervalle de rafraîchissement - Plugins: Plugins - NotYetEnqueued: Pas encore en file d'attente - CreatedAt: Créée le - Back to App: Retour à l'application - Latency: Latence - Pause: Pause - Unpause: Unpause diff --git a/web/locales/he.yml b/web/locales/he.yml deleted file mode 100644 index 6555c6c7..00000000 --- a/web/locales/he.yml +++ /dev/null @@ -1,79 +0,0 @@ -he: - TextDirection: 'rtl' - Dashboard: לוח מחוונים - Status: מצב - Time: שעה - Namespace: מרחב שם - Realtime: זמן אמת - History: היסטוריה - Busy: עסוקים - Processed: עובדו - Failed: נכשלו - Scheduled: מתוכננים - Retries: נסיונות חוזרים - Enqueued: בתור - Worker: עובד - LivePoll: תשאול חי - StopPolling: עצור תשאול - Queue: תור - Class: מחלקה - Job: עבודה - Arguments: ארגומנטים - Extras: תוספות - Started: הותחלו - ShowAll: הצג את הכל - CurrentMessagesInQueue: עבודות נוכחיות בתור %{queue} - Delete: מחק - AddToQueue: הוסף לתור - AreYouSureDeleteJob: האם אתם בטוחים שברצונכם למחוק את העבודה הזאת? - AreYouSureDeleteQueue: האם אתם בטוחים שברצונכם למחוק את התור %{queue}? - Queues: תורים - Size: אורך - Actions: פעולות - NextRetry: ניסיון חוזר הבא - RetryCount: מספר נסיונות חוזרים - RetryNow: נסה שוב עכשיו - Kill: הרוג - LastRetry: ניסיון חוזר אחרון - OriginallyFailed: נכשל בניסיון הראשון - AreYouSure: אתם בטוחים? - DeleteAll: מחק הכל - RetryAll: נסה שוב את הכל - NoRetriesFound: לא נמצאו נסיונות חוזרים - Error: שגיאה - ErrorClass: סוג השגיאה - ErrorMessage: הודעת השגיאה - ErrorBacktrace: מעקב לאחור של השגיאה - GoBack: ← אחורה - NoScheduledFound: לא נמצאו עבודות מתוכננות - When: מתי - ScheduledJobs: עבודות מתוכננות - idle: במנוחה - active: פעיל - Version: גירסה - Connections: חיבורים - MemoryUsage: שימוש בזיכרון - PeakMemoryUsage: שיא השימוש בזיכרון - Uptime: זמן פעילות (ימים) - OneWeek: שבוע 1 - OneMonth: חודש 1 - ThreeMonths: 3 חדשים - SixMonths: 6 חדשים - Failures: כשלונות - DeadJobs: עבודות מתות - NoDeadJobsFound: לא נמצאו עבודות מתות - Dead: מתים - Processes: תהליכים - Thread: חוט - Threads: חוטים - Jobs: עבודות - Paused: הופסקו - Stop: עצור - Quiet: שקט - StopAll: עצור הכל - QuietAll: השקט את כולם - PollingInterval: מרווח זמן בין תשאולים - Plugins: תוספים - NotYetEnqueued: עוד לא בתור - CreatedAt: נוצר ב - BackToApp: חזרה לאפליקציה diff --git a/web/locales/hi.yml b/web/locales/hi.yml deleted file mode 100644 index 65c446c9..00000000 --- a/web/locales/hi.yml +++ /dev/null @@ -1,75 +0,0 @@ -# elements like %{queue} are variables and should not be translated -hi: - Dashboard: डैशबोर्ड - Status: स्थिती - Time: समय - Namespace: नामस्थान - Realtime: रिअल टाईम - History: वृत्तान्त - Busy: व्यस्थ - Processed: कार्रवाई कृत - Failed: असफल - Scheduled: परिगणित - Retries: पुनर्प्रयास - Enqueued: कतारबद्ध - Worker: वर्कर - LivePoll: लाईव सर्वेक्षण - StopPolling: सर्वेक्षण रोको - Queue: कतार - Class: क्लास - Job: कार्य - Arguments: अर्गुमेन्ट्स् - Extras: अतिरिक्त - Started: शुरु हुआ - ShowAll: सब दिखाएं - CurrentMessagesInQueue: %{queue} कतार मे वर्तमान कार्य - Delete: हटाओ - AddToQueue: कतार मे जोड़ें - AreYouSureDeleteJob: क्या आप इस कार्य को हटाना चाहते है? - AreYouSureDeleteQueue: क्या आप %{queue} कतार को हटाना चाहते है? - Queues: कतारे - Size: आकार - Actions: कार्रवाई - NextRetry: अगला पुन:प्रयास - RetryCount: पुन:प्रयास संख्या - RetryNow: पुन:प्रयास करे - Kill: नष्ट करे - LastRetry: अंतिम पुन:प्रयास - OriginallyFailed: पहिले से विफल - AreYouSure: क्या आपको यकीन है? - DeleteAll: सब हटाओ - RetryAll: सब पुन:प्रयास करे - NoRetriesFound: कोई पुनर्प्रयास नही पाए गए - Error: एरर - ErrorClass: एरर क्लास - ErrorMessage: एरर संदेश - ErrorBacktrace: एरर बैकट्रेस - GoBack: ← पीछे - NoScheduledFound: कोई परिगणित कार्य नही पाए गए - When: कब - ScheduledJobs: परिगणित कार्य - idle: निष्क्रिय - active: सक्रिय - Version: वर्जन - Connections: कनेक्श्न - MemoryUsage: मेमरी उपयोग - PeakMemoryUsage: अधिकतम मेमरी उपयोग - Uptime: उपरिकाल (दिवस) - OneWeek: १ सप्ताह - OneMonth: १ महीना - ThreeMonths: ३ महीने - SixMonths: ६ महीने - Failures: असफलता - DeadJobs: निष्प्राण कार्य - NoDeadJobsFound: कोई निष्प्राण कार्य नही पाए गए - Dead: निष्प्राण - Processes: प्रोसेसेस् - Thread: थ्रेड - Threads: थ्रेड्स् - Jobs: कार्य - Paused: थमे हुए - Stop: रोको - Quiet: शांत करो - StopAll: सब रोको - QuietAll: सब शांत करो - PollingInterval: सर्वेक्षण अंतराल diff --git a/web/locales/it.yml b/web/locales/it.yml deleted file mode 100644 index a2b24d5e..00000000 --- a/web/locales/it.yml +++ /dev/null @@ -1,69 +0,0 @@ -# elements like %{queue} are variables and should not be translated -it: - Dashboard: Dashboard - Status: Stato - Time: Ora - Namespace: Namespace - Realtime: Tempo reale - History: Storia - Busy: Occupato - Processed: Processato - Failed: Fallito - Scheduled: Pianificato - Retries: Nuovi tentativi - Enqueued: In coda - Worker: Lavoratore - LivePoll: Live poll - StopPolling: Ferma il polling - Queue: Coda - Class: Classe - Job: Lavoro - Arguments: Argomenti - Extras: Extra - Started: Iniziato - ShowAll: Mostra tutti - CurrentMessagesInQueue: Messaggi in %{queue} - Delete: Cancella - AddToQueue: Aggiungi alla coda - AreYouSureDeleteJob: Sei sicuro di voler cancellare questo lavoro? - AreYouSureDeleteQueue: Sei sicuro di voler cancellare la coda %{queue}? - Queues: Code - Size: Dimensione - Actions: Azioni - NextRetry: Prossimo tentativo - RetryCount: Totale tentativi - RetryNow: Riprova - Kill: Uccidere - LastRetry: Ultimo tentativo - OriginallyFailed: Primo fallimento - AreYouSure: Sei sicuro? - DeleteAll: Cancella tutti - RetryAll: Riprova tutti - NoRetriesFound: Non sono stati trovati nuovi tentativi - Error: Errore - ErrorClass: Classe dell'errore - ErrorMessage: Messaggio di errore - ErrorBacktrace: Backtrace dell'errore - GoBack: ← Indietro - NoScheduledFound: Non ci sono lavori pianificati - When: Quando - ScheduledJobs: Lavori pianificati - idle: inattivo - active: attivo - Version: Versione - Connections: Connessioni - MemoryUsage: Memoria utilizzata - PeakMemoryUsage: Memoria utilizzata (max.) - Uptime: Uptime (giorni) - OneWeek: 1 settimana - OneMonth: 1 mese - ThreeMonths: 3 mesi - SixMonths: 6 mesi - Failures: Fallimenti - DeadJobs: Lavori arrestati - NoDeadJobsFound: Non ci sono lavori arrestati - Dead: Arrestato - Processes: Processi - Thread: Thread - Threads: Thread - Jobs: Lavori diff --git a/web/locales/ja.yml b/web/locales/ja.yml deleted file mode 100644 index 9d7ad260..00000000 --- a/web/locales/ja.yml +++ /dev/null @@ -1,86 +0,0 @@ -# elements like %{queue} are variables and should not be translated -ja: - Dashboard: ダッシュボード - Status: 状態 - Time: 時間 - Namespace: ネームスペース - Realtime: リアルタイム - History: 履歴 - Busy: 実行中 - Utilization: 使用率 - Processed: 完了 - Failed: 失敗 - Scheduled: 予定 - Retries: 再試行 - Enqueued: 待機状態 - Worker: 動作中の作業 - LivePoll: ポーリング開始 - StopPolling: ポーリング停止 - Queue: キュー - Class: クラス - Job: ジョブ - Arguments: 引数 - Extras: エクストラ - Started: 開始 - ShowAll: 全て見せる - CurrentMessagesInQueue: %{queue}に メッセージがあります - Delete: 削除 - AddToQueue: キューに追加 - AreYouSureDeleteJob: このジョブを削除しますか? - AreYouSureDeleteQueue: この %{queue} キューを削除しますか? - Queues: キュー - Size: サイズ - Actions: アクション - NextRetry: 再試行 - RetryCount: 再試行 - RetryNow: 今すぐ再試行 - Kill: 強制終了 - LastRetry: 再試行履歴 - OriginallyFailed: 失敗 - AreYouSure: よろしいですか? - DeleteAll: 全て削除 - RetryAll: 全て再試行 - KillAll: 全て強制終了 - NoRetriesFound: 再試行するジョブはありません - Error: エラー - ErrorClass: エラークラス - ErrorMessage: エラーメッセージ - ErrorBacktrace: エラーバックトレース - GoBack: ← 戻る - NoScheduledFound: 予定されたジョブはありません - When: いつ - ScheduledJobs: 予定されたジョブ - idle: アイドル - active: アクティブ - Version: バージョン - Connections: 接続 - MemoryUsage: メモリー使用量 - PeakMemoryUsage: 最大メモリー使用量 - Uptime: 連続稼働時間 (日) - OneWeek: 1 週 - OneMonth: 1 ヶ月 - ThreeMonths: 3 ヶ月 - SixMonths: 6 ヶ月 - Failures: 失敗 - DeadJobs: デッドジョブ - NoDeadJobsFound: デッドジョブはありません - Dead: デッド - Process: プロセス - Processes: プロセス - Name: 名前 - Thread: スレッド - Threads: スレッド - Jobs: ジョブ - Paused: 一時停止中 - Stop: 停止 - Quiet: 処理終了 - StopAll: すべて停止 - QuietAll: すべて処理終了 - PollingInterval: ポーリング間隔 - Plugins: プラグイン - NotYetEnqueued: キューに入っていません - CreatedAt: 作成日時 - BackToApp: アプリに戻る - Latency: レイテンシ - Pause: 一時停止 - Unpause: 一時停止を解除 diff --git a/web/locales/ko.yml b/web/locales/ko.yml deleted file mode 100644 index 42422ce7..00000000 --- a/web/locales/ko.yml +++ /dev/null @@ -1,68 +0,0 @@ -# elements like %{queue} are variables and should not be translated -ko: - Dashboard: 대시보드 - Status: 상태 - Time: 시간 - Namespace: 네임스페이스 - Realtime: 실시간 - History: 히스토리 - Busy: 작동 - Processed: 처리완료 - Failed: 실패 - Scheduled: 예약 - Retries: 재시도 - Enqueued: 대기 중 - Worker: 워커 - LivePoll: 폴링 시작 - StopPolling: 폴링 중단 - Queue: 큐 - Class: 클래스 - Job: 작업 - Arguments: 인자 - Started: 시작 - ShowAll: 모두 보기 - CurrentMessagesInQueue: %{queue}에 대기 중인 메시지 - Delete: 삭제 - AddToQueue: 큐 추가 - AreYouSureDeleteJob: 이 작업을 삭제하시겠습니까? - AreYouSureDeleteQueue: 이 %{queue} 큐를 삭제하시겠습니까? - Queues: 큐 - Size: 크기 - Actions: 동작 - NextRetry: 다음 재시도 - RetryCount: 재시도 횟수 - RetryNow: 지금 재시도 - LastRetry: 최근 재시도 - OriginallyFailed: 실패 - AreYouSure: 정말입니까? - DeleteAll: 모두 삭제 - RetryAll: 모두 재시도 - NoRetriesFound: 재시도 내역이 없습니다 - Error: 에러 - ErrorClass: 에러 클래스 - ErrorMessage: 에러 메시지 - ErrorBacktrace: 에러 Backtrace - GoBack: ← 뒤로 - NoScheduledFound: 예약된 작업이 없습니다 - When: 언제 - ScheduledJobs: 예약된 작업 - idle: 대기 중 - active: 동작 중 - Version: 버전 - Connections: 커넥션 - MemoryUsage: 메모리 사용량 - PeakMemoryUsage: 최대 메모리 사용량 - Uptime: 업타임 (일) - OneWeek: 1 주 - OneMonth: 1 달 - ThreeMonths: 3 달 - SixMonths: 6 달 - Batches: 배치 - Failures: 실패 - DeadJobs: 죽은 작업 - NoDeadJobsFound: 죽은 작업이 없습니다 - Dead: 죽음 - Processes: 프로세스 - Thread: 스레드 - Threads: 스레드 - Jobs: 작업 diff --git a/web/locales/lt.yml b/web/locales/lt.yml deleted file mode 100644 index 2f45d8c4..00000000 --- a/web/locales/lt.yml +++ /dev/null @@ -1,83 +0,0 @@ -# elements like %{queue} are variables and should not be translated -lt: - Dashboard: Valdymo skydas - Status: Būsena - Time: Laikas - Namespace: Vardų erdvė - Realtime: Realiu laiku - History: Istorija - Busy: Užimti - Processed: Įvykdyti - Failed: Nepavykę - Scheduled: Suplanuoti - Retries: Kartojami - Enqueued: Eilėje - Worker: Darbuotojas - LivePoll: Užklausti gyvai - StopPolling: Stabdyti užklausas - Queue: Eilė - Class: Klasė - Job: Darbas - Arguments: Parametrai - Extras: Papildomi - Started: Pradėti - ShowAll: Rodyti Visus - CurrentMessagesInQueue: Esami darbai eilėje %{queue} - Delete: Pašalinti - AddToQueue: Pridėti į eilę - AreYouSureDeleteJob: Ar tikrai norite pašalinti šį darbą? - AreYouSureDeleteQueue: Ar tikrai norite pašalinti šią eilę %{queue}? - Queues: Eilės - Size: Dydis - Actions: Veiksmai - NextRetry: Sekantis Kartojimas - RetryCount: Kartojimų Skaičius - RetryNow: Kartoti Dabar - Kill: Priverstinai Nutraukti - LastRetry: Paskutinis Kartojimas - OriginallyFailed: Iš pradžių Nepavykę - AreYouSure: Ar jūs įsitikinę? - DeleteAll: Pašalinti Visus - RetryAll: Kartoti Visus - KillAll: Priverstinai Nutraukti Visus - NoRetriesFound: Nerasta kartojimų - Error: Klaida - ErrorClass: Klaidos Klasė - ErrorMessage: Klaidos Žinutė - ErrorBacktrace: Klaidos Pėdsakai - GoBack: ← Atgal - NoScheduledFound: Planuojamų darbų nerasta - When: Kada - ScheduledJobs: Planuojami Darbai - idle: neveiksnus - active: aktyvus - Version: Versija - Connections: Ryšiai - MemoryUsage: Atminties Vartojimas - PeakMemoryUsage: Atminties Vartojimo Pikas - Uptime: Gyvavimo laikas (dienomis) - OneWeek: 1 savaitė - OneMonth: 1 mėnuo - ThreeMonths: 3 mėnesiai - SixMonths: 6 mėnesiai - Failures: Nesėkmingi vykdymai - DeadJobs: Negyvi Darbai - NoDeadJobsFound: Negyvų darbų nerasta - Dead: Negyvi - Processes: Procesai - Thread: Gija - Threads: Gijos - Jobs: Darbai - Paused: Pristabdytas - Stop: Sustabdyti - Quiet: Nutildyti - StopAll: Sustabdyti Visus - QuietAll: Nutildyti Visus - PollingInterval: Užklausimų intervalas - Plugins: Įskiepiai - NotYetEnqueued: Dar neįtraukti į eilę - CreatedAt: Sukurta - BackToApp: Atgal į Aplikaciją - Latency: Vėlavimas - Pause: Pristabdyti - Unpause: Pratęsti diff --git a/web/locales/nb.yml b/web/locales/nb.yml deleted file mode 100644 index 60caf2b2..00000000 --- a/web/locales/nb.yml +++ /dev/null @@ -1,77 +0,0 @@ -# elements like %{queue} are variables and should not be translated -nb: - Dashboard: Oversikt - Status: Status - Time: Tid - Namespace: Navnerom - Realtime: Sanntid - History: Historikk - Busy: Opptatt - Processed: Prosessert - Failed: Mislykket - Scheduled: Planlagt - Retries: Forsøk - Enqueued: I kø - Worker: Arbeider - LivePoll: Automatisk oppdatering - StopPolling: Stopp automatisk oppdatering - Queue: Kø - Class: Klasse - Job: Jobb - Arguments: Argumenter - Extras: Ekstra - Started: Startet - ShowAll: Vis alle - CurrentMessagesInQueue: Nåværende melding i %{queue} - Delete: Slett - AddToQueue: Legg til i kø - AreYouSureDeleteJob: Er du sikker på at du vil slette denne jobben? - AreYouSureDeleteQueue: Er du sikker på at du vil slette køen %{queue}? - Queues: Køer - Size: Størrelse - Actions: Handlinger - NextRetry: Neste forsøk - RetryCount: Antall forsøk - RetryNow: Forsøk igjen nå - Kill: Kill - LastRetry: Forrige forsøk - OriginallyFailed: Feilet opprinnelig - AreYouSure: Er du sikker? - DeleteAll: Slett alle - RetryAll: Forsøk alle på nytt - NoRetriesFound: Ingen forsøk funnet - Error: Feil - ErrorClass: Feilklasse - ErrorMessage: Feilmelding - ErrorBacktrace: Feilbakgrunn - GoBack: ← Tilbake - NoScheduledFound: Ingen planlagte jobber funnet - When: Når - ScheduledJobs: Planlagte jobber - idle: uvirksom - active: aktiv - Version: Versjon - Connections: Tilkoblinger - MemoryUsage: Minneforbruk - PeakMemoryUsage: Høyeste minneforbruk - Uptime: Oppetid (dager) - OneWeek: 1 uke - OneMonth: 1 måned - ThreeMonths: 3 måneder - SixMonths: 6 måneder - Failures: Feil - DeadJobs: Døde jobber - NoDeadJobsFound: Ingen døde jobber funnet - Dead: Død - Processes: Prosesser - Thread: Tråd - Threads: Tråder - Jobs: Jobber - Paused: Pauset - Stop: Stopp - Quiet: Demp - StopAll: Stopp alle - QuietAll: Demp alle - PollingInterval: Oppdateringsintervall - Plugins: Innstikk - NotYetEnqueued: Ikke køet enda diff --git a/web/locales/nl.yml b/web/locales/nl.yml deleted file mode 100644 index 4915e08d..00000000 --- a/web/locales/nl.yml +++ /dev/null @@ -1,68 +0,0 @@ -# elements like %{queue} are variables and should not be translated -nl: - Dashboard: Dashboard - Status: Status - Time: Tijd - Namespace: Namespace - Realtime: Real-time - History: Geschiedenis - Busy: Bezet - Processed: Verwerkt - Failed: Mislukt - Scheduled: Gepland - Retries: Opnieuw proberen - Enqueued: In de wachtrij - Worker: Werker - LivePoll: Live bijwerken - StopPolling: Stop live bijwerken - Queue: Wachtrij - Class: Klasse - Job: Taak - Arguments: Argumenten - Extras: Extra's - Started: Gestart - ShowAll: Toon alle - CurrentMessagesInQueue: Aantal berichten in %{queue} - Delete: Verwijderen - AddToQueue: Toevoegen aan wachtrij - AreYouSureDeleteJob: Weet u zeker dat u deze taak wilt verwijderen? - AreYouSureDeleteQueue: Weet u zeker dat u wachtrij %{queue} wilt verwijderen? - Queues: Wachtrijen - Size: Grootte - Actions: Acties - NextRetry: Volgende opnieuw proberen - RetryCount: Aantal opnieuw geprobeerd - RetryNow: Nu opnieuw proberen - LastRetry: Laatste poging - OriginallyFailed: Oorspronkelijk mislukt - AreYouSure: Weet u het zeker? - DeleteAll: Alle verwijderen - RetryAll: Alle opnieuw proberen - NoRetriesFound: Geen opnieuw te proberen taken gevonden - Error: Fout - ErrorClass: Fout Klasse - ErrorMessage: Foutmelding - ErrorBacktrace: Fout Backtrace - GoBack: ← Terug - NoScheduledFound: Geen geplande taken gevonden - When: Wanneer - ScheduledJobs: Geplande taken - idle: inactief - active: actief - Version: Versie - Connections: Verbindingen - MemoryUsage: Geheugengebruik - PeakMemoryUsage: Piek geheugengebruik - Uptime: Looptijd (dagen) - OneWeek: 1 week - OneMonth: 1 maand - ThreeMonths: 3 maanden - SixMonths: 6 maanden - Failures: Mislukt - DeadJobs: Overleden taken - NoDeadJobsFound: Geen overleden taken gevonden - Dead: Overleden - Processes: Processen - Thread: Thread - Threads: Threads - Jobs: Taken diff --git a/web/locales/pl.yml b/web/locales/pl.yml deleted file mode 100644 index a6161cf4..00000000 --- a/web/locales/pl.yml +++ /dev/null @@ -1,59 +0,0 @@ -# elements like %{queue} are variables and should not be translated -pl: - Dashboard: Kokpit - Status: Status - Time: Czas - Namespace: Przestrzeń nazw - Realtime: Czas rzeczywisty - History: Historia - Busy: Zajęte - Processed: Ukończone - Failed: Nieudane - Scheduled: Zaplanowane - Retries: Do ponowienia - Enqueued: Zakolejkowane - Worker: Worker - LivePoll: Wczytuj na żywo - StopPolling: Zatrzymaj wczytywanie na żywo - Queue: Kolejka - Class: Klasa - Job: Zadanie - Arguments: Argumenty - Started: Rozpoczęte - ShowAll: Pokaż wszystko - CurrentMessagesInQueue: Aktualne wiadomości w kolejce %{queue} - Delete: Usuń - AddToQueue: dodaj do kolejki - AreYouSureDeleteJob: Czy na pewno usunąć to zadanie? - AreYouSureDeleteQueue: Czy na pewno usunąć kolejkę %{queue}? - Queues: Kolejki - Size: Rozmiar - Actions: Akcje - NextRetry: Następne ponowienie - RetryCount: Ilość ponowień - RetryNow: Ponów teraz - LastRetry: Ostatnie ponowienie - OriginallyFailed: Ostatnio nieudane - AreYouSure: Na pewno? - DeleteAll: Usuń wszystko - RetryAll: Powtórz wszystko - NoRetriesFound: Brak zadań do ponowienia - Error: Błąd - ErrorClass: Klasa błędu - ErrorMessage: Wiadomosć błędu - ErrorBacktrace: Wyjście błędu - GoBack: ← Wróć - NoScheduledFound: Brak zaplanowanych zadań - When: Kiedy - ScheduledJobs: Zaplanowane zadania - idle: bezczynne - active: aktywne - Version: Wersja - Connections: Połączenia - MemoryUsage: Wykorzystanie pamięci - PeakMemoryUsage: Największe wykorzystanie pamięci - Uptime: Uptime (dni) - OneWeek: 1 tydzień - OneMonth: 1 miesiąc - ThreeMonths: 3 miesiące - SixMonths: 6 miesięcy diff --git a/web/locales/pt-br.yml b/web/locales/pt-br.yml deleted file mode 100644 index a6731148..00000000 --- a/web/locales/pt-br.yml +++ /dev/null @@ -1,68 +0,0 @@ -# elements like %{queue} are variables and should not be translated -"pt-br": - Dashboard: Painel - Status: Status - Time: Tempo - Namespace: Namespace - Realtime: Tempo real - History: Histórico - Busy: Ocupados - Processed: Processados - Failed: Falhas - Scheduled: Agendados - Retries: Tentativas - Enqueued: Na fila - Worker: Trabalhador - LivePoll: Live Poll - StopPolling: Parar Polling - Queue: Fila - Class: Classe - Job: Tarefa - Arguments: Argumentos - Extras: Extras - Started: Iniciados - ShowAll: Mostrar todos - CurrentMessagesInQueue: Mensagens atualmente na %{queue} - Delete: Apagar - AddToQueue: Adicionar à fila - AreYouSureDeleteJob: Deseja deletar esta tarefa? - AreYouSureDeleteQueue: Deseja deletar a %{queue} fila? - Queues: Filas - Size: Tamanho - Actions: Ações - NextRetry: Próxima Tentativa - RetryCount: Número de Tentativas - RetryNow: Tentar novamente agora - LastRetry: Última tentativa - OriginallyFailed: Falhou originalmente - AreYouSure: Tem certeza? - DeleteAll: Apagar tudo - RetryAll: Tentar tudo novamente - NoRetriesFound: Nenhuma tentativa encontrada - Error: Erro - ErrorClass: Classe de erro - ErrorMessage: Mensagem de erro - ErrorBacktrace: Rastreamento do erro - GoBack: ← Voltar - NoScheduledFound: Nenhuma tarefa agendada foi encontrada - When: Quando - ScheduledJobs: Tarefas agendadas - idle: ocioso - active: ativo - Version: Versão - Connections: Conexões - MemoryUsage: Uso de memória - PeakMemoryUsage: Pico de uso de memória - Uptime: Dias rodando - OneWeek: 1 semana - OneMonth: 1 mês - ThreeMonths: 3 meses - SixMonths: 6 meses - Failures : Falhas - DeadJobs : Tarefas mortas - NoDeadJobsFound : Nenhuma tarefa morta foi encontrada - Dead : Morta - Processes : Processos - Thread : Thread - Threads : Threads - Jobs : Tarefas diff --git a/web/locales/pt.yml b/web/locales/pt.yml deleted file mode 100644 index c8174f03..00000000 --- a/web/locales/pt.yml +++ /dev/null @@ -1,67 +0,0 @@ -# elements like %{queue} are variables and should not be translated -pt: - Dashboard: Dashboard - Status: Estado - Time: Tempo - Namespace: Namespace - Realtime: Tempo real - History: Histórico - Busy: Ocupado - Processed: Processados - Failed: Falhados - Scheduled: Agendados - Retries: Tentativas - Enqueued: Em espera - Worker: Worker - LivePoll: Live Poll - StopPolling: Desactivar Live Poll - Queue: Fila - Class: Classe - Job: Tarefa - Arguments: Argumentos - Started: Iniciados - ShowAll: Mostrar todos - CurrentMessagesInQueue: Mensagens na fila %{queue} - Delete: Apagar - AddToQueue: Adicionar à fila - AreYouSureDeleteJob: Tem a certeza que deseja eliminar esta tarefa? - AreYouSureDeleteQueue: Tem a certeza que deseja eliminar a fila %{queue}? - Queues: Filas - Size: Tamanho - Actions: Acções - NextRetry: Próxima Tentativa - RetryCount: Tentativas efectuadas - RetryNow: Tentar novamente - LastRetry: Última Tentativa - OriginallyFailed: Falhou inicialmente - AreYouSure: Tem a certeza? - DeleteAll: Eliminar todos - RetryAll: Tentar tudo novamente - NoRetriesFound: Não foram encontradas tentativas - Error: Erro - ErrorClass: Classe de Erro - ErrorMessage: Mensagem de erro - ErrorBacktrace: Backtrace do Erro - GoBack: ← Voltar - NoScheduledFound: Não foram encontradas tarefas agendadas - When: Quando - ScheduledJobs: Tarefas agendadas - idle: livre - active: activo - Version: Versão - Connections: Conexões - MemoryUsage: Utilização de Memória - PeakMemoryUsage: Pico de utilização de memória - Uptime: Uptime (em dias) - OneWeek: 1 semana - OneMonth: 1 mês - ThreeMonths: 3 meses - SixMonths: 6 meses - Failures: Falhas - DeadJobs: Tarefas mortas - NoDeadJobsFound: Não foram encontradas tarefas mortas - Dead: Morto - Processes: Processos - Thread: Thread - Threads: Threads - Jobs: Tarefas diff --git a/web/locales/ru.yml b/web/locales/ru.yml deleted file mode 100644 index 0c28f790..00000000 --- a/web/locales/ru.yml +++ /dev/null @@ -1,82 +0,0 @@ -ru: - Dashboard: Панель управления - Status: Статус - Time: Время - Namespace: Пространство имен - Realtime: Сейчас - History: История - Busy: Занят - Processed: Обработано - Failed: Провалено - Scheduled: Запланировано - Retries: Попытки - Enqueued: В очереди - Worker: Обработчик - LivePoll: Постоянный опрос - StopPolling: Остановить опрос - Queue: Очередь - Class: Класс - Job: Задача - Arguments: Аргументы - Extras: Дополнительно - Started: Запущено - ShowAll: Показать все - CurrentMessagesInQueue: Текущие задачи в очереди %{queue} - Delete: Удалить - AddToQueue: Добавить в очередь - AreYouSureDeleteJob: Вы уверены, что хотите удалить эту задачу? - AreYouSureDeleteQueue: Вы уверены, что хотите удалить очередь %{queue}? - Queues: Очереди - Size: Размер - Actions: Действия - NextRetry: Следующая попытка - RetryCount: Кол-во попыток - RetryNow: Повторить сейчас - Kill: Убиваем - LastRetry: Последняя попытка - OriginallyFailed: Первый провал - AreYouSure: Вы уверены? - DeleteAll: Удалить все - RetryAll: Повторить все - KillAll: Убить всё - NoRetriesFound: Нет попыток - Error: Ошибка - ErrorClass: Класс ошибки - ErrorMessage: Сообщение об ошибке - ErrorBacktrace: Трассировка ошибки - GoBack: ← Назад - NoScheduledFound: Нет запланированных задач - When: Когда - ScheduledJobs: Запланированные задачи - idle: отдыхает - active: активен - Version: Версия - Connections: Соединения - MemoryUsage: Использование памяти - PeakMemoryUsage: Максимальный расход памяти - Uptime: Дня(ей) бесперебойной работы - OneWeek: 1 неделя - OneMonth: 1 месяц - ThreeMonths: 3 месяца - SixMonths: 6 месяцев - Failures: Провалы - DeadJobs: Убитые задачи - NoDeadJobsFound: Нет убитых задач - Dead: Убито - Processes: Процессы - Thread: Поток - Threads: Потоки - Jobs: Задачи - Paused: Приостановлено - Stop: Остановить - Quiet: Отдыхать - StopAll: Остановить все - QuietAll: Отдыхать всем - PollingInterval: Интервал опроса - Plugins: Плагины - NotYetEnqueued: Пока не в очереди - CreatedAt: Создан - BackToApp: Назад - Latency: Задержка - Pause: Пауза - Unpause: Возобновить diff --git a/web/locales/sv.yml b/web/locales/sv.yml deleted file mode 100644 index 224c2f6e..00000000 --- a/web/locales/sv.yml +++ /dev/null @@ -1,68 +0,0 @@ -# elements like %{queue} are variables and should not be translated -sv: # <---- change this to your locale code - Dashboard: Panel - Status: Status - Time: Tid - Namespace: Namnrymd - Realtime: Realtid - History: Historik - Busy: Upptagen - Processed: Processerad - Failed: Misslyckad - Scheduled: Schemalagd - Retries: Försök - Enqueued: Köad - Worker: Worker - LivePoll: Live poll - StopPolling: Stoppa polling - Queue: Kö - Class: Klass - Job: Jobb - Arguments: Argument - Extras: Extra - Started: Startad - ShowAll: Visa alla - CurrentMessagesInQueue: Jobb i %{queue} - Delete: Ta bort - AddToQueue: Lägg till i kö - AreYouSureDeleteJob: Är du säker på att du vill ta bort detta jobb? - AreYouSureDeleteQueue: Är du säker på att du vill ta bort kön %{queue}? - Queues: Köer - Size: Storlek - Actions: Åtgärder - NextRetry: Nästa försök - RetryCount: Antal försök - RetryNow: Försök nu - LastRetry: Senaste försök - OriginallyFailed: Misslyckades ursprungligen - AreYouSure: Är du säker? - DeleteAll: Ta bort alla - RetryAll: Försök alla igen - NoRetriesFound: Inga försök hittades - Error: Fel - ErrorClass: Felklass - ErrorMessage: Felmeddelande - ErrorBacktrace: Backtrace för fel - GoBack: ← Bakåt - NoScheduledFound: Inga schemalagda jobb hittades - When: När - ScheduledJobs: Schemalagda jobb - idle: avvaktande - active: aktiv - Version: Version - Connections: Anslutningar - MemoryUsage: Minnesanvändning - PeakMemoryUsage: Minnesanvändning (peak) - Uptime: Upptid (dagar) - OneWeek: 1 vecka - OneMonth: 1 månad - ThreeMonths: 3 månader - SixMonths: 6 månader - Failures: Failures - DeadJobs: Döda jobb - NoDeadJobsFound: Inga döda jobb hittades - Dead: Död - Processes: Processer - Thread: Tråd - Threads: Trådar - Jobs: Jobb diff --git a/web/locales/ta.yml b/web/locales/ta.yml deleted file mode 100644 index bede7fec..00000000 --- a/web/locales/ta.yml +++ /dev/null @@ -1,75 +0,0 @@ -# elements like %{queue} are variables and should not be translated -ta: # <---- change this to your locale code - Dashboard: டாஷ்போர்டு - Status: நிலைமை - Time: நேரம் - Namespace: பெயர்வெளி - Realtime: நேரலை - History: வரலாறு - Busy: பணிமிகுதி - Processed: நிறையுற்றது - Failed: தோல்வி - Scheduled: திட்டமிடப்பட்ட - Retries: மீண்டும் முயற்சிக்க, - Enqueued: வரிசைப்படுத்தப்பட்டவை - Worker: பணியாளர் - LivePoll: நேரடி கணிப்பு - StopPolling: நிறுத்து வாக்குப்பதிவு - Queue: வரிசை - Class: வகுப்பு - Job: வேலை - Arguments: வாதங்கள், - Extras: உபரி - Started: தொடங்குதல் - ShowAll: அனைத்து காட்டு - CurrentMessagesInQueue: தற்போதைய வேலைகள் %{queue} - Delete: நீக்கு - AddToQueue: வரிசையில் சேர் - AreYouSureDeleteJob: நீ இந்த வேலையை நீக்க வேண்டும் என்று உறுதியாக இருக்கிறீர்களா? - AreYouSureDeleteQueue: நீங்கள் %{queue} வரிசையில் நீக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா? - Queues: வரிசை - Size: அளவு - Actions: செயல்கள் - NextRetry: அடுத்த, மீண்டும் முயற்சிக்கவும் - RetryCount: கணிப்பீடு, மீண்டும் முயற்சிக்கவும் - RetryNow: இப்போது மீண்டும் முயற்சி செய்க - Kill: கொல் - LastRetry: கடைசியாக, மீண்டும் முயற்சிக்கவும் - OriginallyFailed: முதலில் தோல்வி - AreYouSure: நீங்கள் உறுதியாக இருக்கிறீர்களா? - DeleteAll: அனைத்து நீக்கு - RetryAll: அனைத்து, மீண்டும் முயற்சிக்கவும் - NoRetriesFound: இல்லை மீண்டும் காணப்படவில்லை - Error: பிழை - ErrorClass: பிழை வகுப்பு - ErrorMessage: பிழை செய்தி - ErrorBacktrace: பிழை பின்தேடுலை - GoBack: பின்புறம் - NoScheduledFound: திட்டமிட்ட வேலைகள் காணப்படவில்லை - When: எப்பொழுது? - ScheduledJobs: திட்டமிட்ட வேலைகள் - idle: முடங்கு நேரம் - active: செயலில் - Version: பதிப்பு - Connections: இணைப்புகள் - MemoryUsage: நினைவக பயன்பாடு - PeakMemoryUsage: உச்ச நினைவக பயன்பாடு - Uptime: இயக்க நேரம் (நாட்கள்) - OneWeek: 1 வாரம் - OneMonth: 1 மாதம் - ThreeMonths: 3 மாதங்கள் - SixMonths: 6 மாதங்கள் - Failures: தோல்விகள் - DeadJobs: டெட் வேலைகள் - NoDeadJobsFound: இறந்த வேலை எதுவும் இல்லை - Dead: இறந்துபோன - Processes: செயல்முறைகள் - Thread: நூல் - Threads: நூல்கள் - Jobs: வேலை வாய்ப்புகள் - Paused: தற்காலிக பணிநிறுத்தம் - Stop: நிறுத்து - Quiet: அமைதியான - StopAll: நிறுத்து அனைத்து - QuietAll: அமைதியான அனைத்து - PollingInterval: வாக்குப்பதிவு இடைவெளி \ No newline at end of file diff --git a/web/locales/uk.yml b/web/locales/uk.yml deleted file mode 100644 index 3bb14abe..00000000 --- a/web/locales/uk.yml +++ /dev/null @@ -1,76 +0,0 @@ -uk: - Dashboard: Панель керування - Status: Статус - Time: Час - Namespace: Простір імен - Realtime: Зараз - History: Історія - Busy: Зайнятих - Processed: Опрацьовано - Failed: Невдалих - Scheduled: Заплановано - Retries: Спроби - Enqueued: У черзі - Worker: Обробник - LivePoll: Постійне опитування - StopPolling: Зупинити опитування - Queue: Черга - Class: Клас - Job: Задача - Arguments: Аргументи - Extras: Додатково - Started: Запущено - ShowAll: Відобразити усі - CurrentMessagesInQueue: Поточні задачі у черзі %{queue} - Delete: Видалити - AddToQueue: Додати до черги - AreYouSureDeleteJob: Ви впевнені у тому, що хочете видалити задачу? - AreYouSureDeleteQueue: Ви впевнені у тому, що хочете видалити чергу %{queue}? - Queues: Черги - Size: Розмір - Actions: Дії - NextRetry: Наступна спроба - RetryCount: Кількість спроб - RetryNow: Повторити зараз - Kill: Вбиваємо - LastRetry: Остання спроба - OriginallyFailed: Перша невдала спроба - AreYouSure: Ви впевнені? - DeleteAll: Видалити усі - RetryAll: Повторити усі - NoRetriesFound: Спроб не знайдено - Error: Помилка - ErrorClass: Клас помилки - ErrorMessage: Повідомлення про помилку - ErrorBacktrace: Трасування помилки - GoBack: ← Назад - NoScheduledFound: Запланованих задач не знайдено - When: Коли - ScheduledJobs: Заплановані задачі - idle: незайнятий - active: активний - Version: Версія - Connections: З'єднань - MemoryUsage: Використання пам'яті - PeakMemoryUsage: Максимальне використання пам'яті - Uptime: Днів безперебійної роботи - OneWeek: 1 тиждень - OneMonth: 1 місяць - ThreeMonths: 3 місяці - SixMonths: 6 місяців - Failures: Невдачі - DeadJobs: Вбиті задачі - NoDeadJobsFound: Вбитих задач не знайдено - Dead: Вбитих - Processes: Процеси - Thread: Потік - Threads: Потоки - Jobs: Задачі - Paused: Призупинено - Stop: Зупинити - Quiet: Призупинити - StopAll: Зупинити усі - QuietAll: Призупинити усі - PollingInterval: Інтервал опитування - Plugins: Плагіни - NotYetEnqueued: Ще не в черзі diff --git a/web/locales/ur.yml b/web/locales/ur.yml deleted file mode 100644 index b6cd1516..00000000 --- a/web/locales/ur.yml +++ /dev/null @@ -1,80 +0,0 @@ -# elements like %{queue} are variables and should not be translated -ur: - TextDirection: 'rtl' - Dashboard: صفحۂ اول - Status: اسٹیٹس - Time: ﻭﻗﺖ - Namespace: Namespace - Realtime: ﺑﺮاﮦ ﺭاﺳﺖ - History: ﺗﺎﺭﻳﺦ - Busy: مصروف - Processed: مکمل شدہ - Failed: ﻧﺎکاﻡ ﺷﺪﮦ - Scheduled: ﻁےﺷﺪﮦ - Retries: ﺩﻭﺑﺎﺭﮦ کﻭﺷﻴﺶ - Enqueued: قطار ميں شامل - Worker: ورکر - LivePoll: ﺑﺮاﮦ ﺭاﺳﺖ - StopPolling: ﺑﺮاﮦ ﺭاﺳﺖ روکيے - Queue: قطار - Class: کلاس - Job: جاب - Arguments: دلائل - Extras: اﺻﺎﻑی - Started: شروع - ShowAll: سارے دکھاو - CurrentMessagesInQueue: قطار ميں موجود تمام پيغامات %{queue} - AddToQueue: ﻗﻄﺎﺭ ميں شامل کريں - AreYouSureDeleteJob: کيا آپ یقین جاب حتم کرنا چاھتے ہيں ؟ - AreYouSureDeleteQueue: کيا آپ یقین قطار حتم کرنا چاھتے ہيں ؟ - Delete: ﺣﺬﻑ - Queues: قطاريں - Size: ﺣﺠﻢ - Actions: ﻋﻮاﻣﻞ - NextRetry: اگلی کﻭﺷﻴﺶ - RetryCount: دوبارہ کوشش کا مکمل شمار - RetryNow: ابھی دوبارہ کوشش - Kill: ختم کرديں - LastRetry: گزشتہ کوشش - OriginallyFailed: ابتادائ ناکامی - AreYouSure: کيا یقین ؟ - DeleteAll: ﺗﻤﺎﻡ ﺣﺬﻑ کر ديں - RetryAll: ﺗﻤﺎﻡ کی ﺩﻭﺑﺎﺭﮦ کﻭﺷﻴﺶ کﺭيں - NoRetriesFound: کویٔ ﺩﻭﺑﺎﺭﮦ کﻭﺷﻴﺶ نھيں ملی - Error: مسئلہ - ErrorClass: مسئلہ کی کلاس - ErrorMessage: مسئلہ کی وجہ - ErrorBacktrace: مسئلہ کی کی تحقیقات کريں - GoBack: واپس جايں - NoScheduledFound: کویٔ ﻁےﺷﺪﮦچيز نہیں ملی - When: ﺏک - ScheduledJobs: ﻁےﺷﺪﮦجاب - idle: بیکار - active: فعال - Version: ورژن - Connections: کنکشنز - MemoryUsage: یاداشت کا استعمال - PeakMemoryUsage: سب سے زيادہ یاداشت کا استعمال - Uptime: اپ ٹائم - OneWeek: ایک ہفتہ - OneMonth: ایک مہینہ - ThreeMonths: تین ماہ - SixMonths: چھ ماہ - Failures: ناکامیاں - DeadJobs: ختم شدہ جاب - NoDeadJobsFound: کویٔ ختم شدہ جاب نہيی ملی - Dead: ختم شدہ - Processes: ﻋﻤﻠﻴﺎﺕ - Thread: موضوع - Threads: موضوع - Jobs: جابز - Paused: موقوف - Stop: بند کرو - Quiet: ﺣﺘﻢ کﺭﻭ - StopAll: ﺗﻤﺎﻡ ﺑﻨﺪ کﺭﻭ - QuietAll: ﺗﻤﺎﻡ ﺣﺘﻢ کﺭﻭ - PollingInterval: ﺑﺮاﮦ ﺭاﺳﺖ کا ﺩﻭﺭاﻧﻴﮧ - Plugins: پلگ انز - NotYetEnqueued: ﻗﺘﺎﺭميں شامل نھيں - CreatedAt: ﺗﺎﺭﻳﺢ آﻏﺎﺯ - BackToApp: ﻭاپﺱ صفحۂ اﻭﻝ پر \ No newline at end of file diff --git a/web/locales/vi.yml b/web/locales/vi.yml deleted file mode 100644 index 00169d5a..00000000 --- a/web/locales/vi.yml +++ /dev/null @@ -1,83 +0,0 @@ -# elements like %{queue} are variables and should not be translated -vi: # <---- change this to your locale code - Dashboard: Bảng điều khiển - Status: Trạng thái - Time: Thời gian - Namespace: Không gian tên - Realtime: Thời gian thực - History: Lịch sử - Busy: Bận rộn - Processed: Đã xử lí - Failed: Đã thất bại - Scheduled: Đã lên lịch - Retries: Số lần thử - Enqueued: Đã xếp hàng đợi - Worker: Máy xử lí - LivePoll: Thăm dò trực tiếp - StopPolling: Ngừng thăm dò - Queue: Hàng đợi - Class: Lớp - Job: Tác vụ - Arguments: Tham số - Extras: Thêm - Started: Đã bắt đầu - ShowAll: Hiện tất cả - CurrentMessagesInQueue: Số lượng công việc trong %{queue} - Delete: Xóa - AddToQueue: Thêm vào hàng đợi - AreYouSureDeleteJob: Bạn có chắc là muốn xóa tác vụ này? - AreYouSureDeleteQueue: Bạn có chắc là muốn xóa %{queue} này? - Queues: Các hàng đợi - Size: Kích thước - Actions: Những hành động - NextRetry: Lần thử lại tiếp theo - RetryCount: Số lần thử lại - RetryNow: Thử lại ngay bây giờ - Kill: Giết - LastRetry: Lần thử cuối - OriginallyFailed: Đã thất bại từ đầu - AreYouSure: Bạn chắc chứ? - DeleteAll: Xóa hết - RetryAll: Thử lại tất cả - KillAll: Giết hết - NoRetriesFound: Không có lần thử nào được tìm thấy - Error: Lỗi - ErrorClass: Lớp lỗi - ErrorMessage: Tin nhắn lỗi - ErrorBacktrace: Dấu vết của lỗi - GoBack: ← Trở lại - NoScheduledFound: Không có tác vụ đã lên lịch nào được tìm thấy - When: Khi nào - ScheduledJobs: Những Tác Vụ Được Hẹn - idle: Đang chờ - active: Đang hoạt động - Version: Phiên bản - Connections: Các kết nối - MemoryUsage: Lượng bộ nhớ sử dụng - PeakMemoryUsage: Lượng bộ nhớ sử dụng đỉnh điểm - Uptime: Thời gian hệ thống đã online (days) - OneWeek: 1 tuần - OneMonth: 1 tháng - ThreeMonths: 3 tháng - SixMonths: 6 tháng - Failures: Các thất bại - DeadJobs: Những tác vụ đã chết - NoDeadJobsFound: Không có tác vụ đã chết nào được tìm thấy - Dead: Chết - Processes: Tiến trình xử lí - Thread: Luồng xử lí - Threads: Những luồng xử lí - Jobs: Các tác vụ - Paused: Đã tạm dừng - Stop: Dừng Lại - Quiet: Im lặng - StopAll: Dừng lại tất cả - QuietAll: Làm cho tất cả im lặng - PollingInterval: Khoảng thời gian giữa các lần thăm dò - Plugins: Hệ thống đính kèm - NotYetEnqueued: Chưa được bỏ vào hàng đợi - CreatedAt: Được tạo vào lúc - BackToApp: Trở về ứng dụng - Latency: Độ trễ - Pause: Tạm dừng - Unpause: Hủy tạm dừng diff --git a/web/locales/zh-cn.yml b/web/locales/zh-cn.yml deleted file mode 100644 index 09bf4c2a..00000000 --- a/web/locales/zh-cn.yml +++ /dev/null @@ -1,68 +0,0 @@ -# elements like %{queue} are variables and should not be translated -zh-cn: # <---- change this to your locale code - Dashboard: 信息板 - Status: 状态 - Time: 时间 - Namespace: 命名空间 - Realtime: 实时 - History: 历史记录 - Busy: 执行中 - Processed: 已处理 - Failed: 已失败 - Scheduled: 已计划 - Retries: 重试 - Enqueued: 已进入队列 - Worker: 工人 - LivePoll: 实时轮询 - StopPolling: 停止轮询 - Queue: 队列 - Class: 类别 - Job: 作业 - Arguments: 参数 - Extras: 额外的 - Started: 已开始 - ShowAll: 显示全部 - CurrentMessagesInQueue: 目前在%{queue}的作业 - Delete: 删除 - AddToQueue: 添加至队列 - AreYouSureDeleteJob: 你确定要删除这个作业么? - AreYouSureDeleteQueue: 你确定要删除%{queue}这个队列? - Queues: 队列 - Size: 容量 - Actions: 动作 - NextRetry: 下次重试 - RetryCount: 重试次數 - RetryNow: 现在重试 - LastRetry: 最后一次重试 - OriginallyFailed: 原本已失败 - AreYouSure: 你确定? - DeleteAll: 删除全部 - RetryAll: 重试全部 - NoRetriesFound: 沒有发现可重试 - Error: 错误 - ErrorClass: 错误类别 - ErrorMessage: 错误消息 - ErrorBacktrace: 错误的回调追踪 - GoBack: ← 返回 - NoScheduledFound: 沒有发现计划作业 - When: 当 - ScheduledJobs: 计划作业 - idle: 闲置 - active: 活动中 - Version: 版本 - Connections: 连接 - MemoryUsage: 内存占用 - PeakMemoryUsage: 内存占用峰值 - Uptime: 上线时间 (天数) - OneWeek: 一周 - OneMonth: 一个月 - ThreeMonths: 三个月 - SixMonths: 六个月 - Failures: 失败 - DeadJobs: 已停滞作业 - NoDeadJobsFound: 沒有发现任何已停滞的作业 - Dead: 已停滞 - Processes: 处理中 - Thread: 线程 - Threads: 线程 - Jobs: 作业 diff --git a/web/locales/zh-tw.yml b/web/locales/zh-tw.yml deleted file mode 100644 index d56bfc97..00000000 --- a/web/locales/zh-tw.yml +++ /dev/null @@ -1,68 +0,0 @@ -# elements like %{queue} are variables and should not be translated -zh-tw: # <---- change this to your locale code - Dashboard: 資訊主頁 - Status: 狀態 - Time: 時間 - Namespace: 命名空間 - Realtime: 即時 - History: 歷史資料 - Busy: 忙碌 - Processed: 已處理 - Failed: 已失敗 - Scheduled: 已排程 - Retries: 重試 - Enqueued: 已佇列 - Worker: 工人 - LivePoll: 即時輪詢 - StopPolling: 停止輪詢 - Queue: 佇列 - Class: 類別 - Job: 工作 - Arguments: 參數 - Extras: 額外的 - Started: 已開始 - ShowAll: 顯示全部 - CurrentMessagesInQueue: 目前在%{queue}的工作 - Delete: 刪除 - AddToQueue: 增加至佇列 - AreYouSureDeleteJob: 你確定要刪除這個工作嗎? - AreYouSureDeleteQueue: 你確定要刪除%{queue}這個佇列? - Queues: 佇列 - Size: 容量 - Actions: 動作 - NextRetry: 下次重試 - RetryCount: 重試次數 - RetryNow: 馬上重試 - LastRetry: 最後一次重試 - OriginallyFailed: 原本已失敗 - AreYouSure: 你確定? - DeleteAll: 刪除全部 - RetryAll: 重試全部 - NoRetriesFound: 沒有發現可重試 - Error: 錯誤 - ErrorClass: 錯誤類別 - ErrorMessage: 錯誤訊息 - ErrorBacktrace: 錯誤的回調追踨 - GoBack: ← 返回 - NoScheduledFound: 沒有發現已排程的工作 - When: 當 - ScheduledJobs: 已排程的工作 - idle: 閒置 - active: 活動中 - Version: 版本 - Connections: 連線 - MemoryUsage: 記憶體使用量 - PeakMemoryUsage: 尖峰記憶體使用量 - Uptime: 上線時間 (天數) - OneWeek: 一週 - OneMonth: 一個月 - ThreeMonths: 三個月 - SixMonths: 六個月 - Failures: 失敗 - DeadJobs: 停滯工作 - NoDeadJobsFound: 沒有發現任何停滯的工作 - Dead: 停滯 - Processes: 處理中 - Thread: 執行緒 - Threads: 執行緒 - Jobs: 工作 diff --git a/web/views/_footer.erb b/web/views/_footer.erb deleted file mode 100644 index cb284f3c..00000000 --- a/web/views/_footer.erb +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/web/views/_job_info.erb b/web/views/_job_info.erb deleted file mode 100644 index 176e8cf1..00000000 --- a/web/views/_job_info.erb +++ /dev/null @@ -1,89 +0,0 @@ -
-

<%= t('Job') %>

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - <% unless retry_extra_items(job).empty? %> - - - - - <% end %> - <% if type == :retry %> - <% if job['retry_count'] && job['retry_count'] > 0 %> - - - - - - - - - <% else %> - - - - - <% end %> - - - - - <% end %> - <% if type == :scheduled %> - - - - - <% end %> - <% if type == :dead %> - - - - - <% end %> - -
<%= t('Queue') %> - <%= job.queue %> -
<%= t('Job') %> - <%= job.display_class %> - <%= display_tags(job) %> -
<%= t('Arguments') %> - - -
<%= display_args(job.display_args, nil) %>
-
-
JID - <%= job.jid %> -
<%= t('CreatedAt') %><%= relative_time(job.created_at) %>
<%= t('Enqueued') %><%= (enqueued_at = job.enqueued_at) ? relative_time(enqueued_at) : t('NotYetEnqueued') %>
<%= t('Extras') %> - - <%= retry_extra_items(job).inspect %> - -
<%= t('RetryCount') %><%= job['retry_count'] %>
<%= t('LastRetry') %><%= relative_time(Time.at(job['retried_at'])) %>
<%= t('OriginallyFailed') %><%= relative_time(Time.at(job['failed_at'])) %>
<%= t('NextRetry') %><%= relative_time(job.at) %>
<%= t('Scheduled') %><%= relative_time(job.at) %>
<%= t('LastRetry') %><%= relative_time(job.at) if job['retry_count'] %>
-
diff --git a/web/views/_nav.erb b/web/views/_nav.erb deleted file mode 100644 index d3b94659..00000000 --- a/web/views/_nav.erb +++ /dev/null @@ -1,52 +0,0 @@ - diff --git a/web/views/_paging.erb b/web/views/_paging.erb deleted file mode 100644 index cee57b7c..00000000 --- a/web/views/_paging.erb +++ /dev/null @@ -1,23 +0,0 @@ -<% if @total_size > @count %> - -<% end %> diff --git a/web/views/_poll_link.erb b/web/views/_poll_link.erb deleted file mode 100644 index 8bc57995..00000000 --- a/web/views/_poll_link.erb +++ /dev/null @@ -1,4 +0,0 @@ -<% if current_path != '' %> - <%= t('LivePoll') %> - <%= t('StopPolling') %> -<% end %> diff --git a/web/views/_status.erb b/web/views/_status.erb deleted file mode 100644 index 93528c9d..00000000 --- a/web/views/_status.erb +++ /dev/null @@ -1,4 +0,0 @@ - - - <%= t(current_status) %> - diff --git a/web/views/_summary.erb b/web/views/_summary.erb deleted file mode 100644 index 802162a7..00000000 --- a/web/views/_summary.erb +++ /dev/null @@ -1,40 +0,0 @@ - diff --git a/web/views/busy.erb b/web/views/busy.erb deleted file mode 100644 index c4f30925..00000000 --- a/web/views/busy.erb +++ /dev/null @@ -1,132 +0,0 @@ -
-
-

<%= t('Status') %>

-
-
- -
-
-
-

<%= s = processes.size; number_with_delimiter(s) %>

-

<%= t('Processes') %>

-
-
-

<%= x = processes.total_concurrency; number_with_delimiter(x) %>

-

<%= t('Threads') %>

-
-
-

<%= ws = workers.size; number_with_delimiter(ws) %>

-

<%= t('Busy') %>

-
-
-

<%= x == 0 ? 0 : ((ws / x.to_f) * 100).round(0) %>%

-

<%= t('Utilization') %>

-
-
-

<%= format_memory(processes.total_rss) %>

-

<%= t('RSS') %>

-
-
-
- -
-
-

<%= t('Processes') %>

-
-
-
- <%= csrf_tag %> -
- - -
-
-
-
-
- - - - - - - - - - <% lead = processes.leader %> - <% processes.each do |process| %> - - - - - - - - - <% end %> -
<%= t('Name') %><%= t('Started') %><%= t('RSS') %>?<%= t('Threads') %><%= t('Busy') %> 
- <%= "#{process['hostname']}:#{process['pid']}" %> - <%= process.tag %> - <% process.labels.each do |label| %> - <%= label %> - <% end %> - <% if process.stopping? %> - quiet - <% end %> - <% if process.identity == lead %> - leader - <% end %> -
- <%= "#{t('Queues')}: " %> - <%= process.queues.join(", ") %> -
<%= relative_time(Time.at(process['started_at'])) %><%= format_memory(process['rss'].to_i) %><%= process['concurrency'] %><%= process['busy'] %> -
- <%= csrf_tag %> - - -
- <% unless process.stopping? %><% end %> - -
-
-
-
- -
-
-

<%= t('Jobs') %>

-
-
- -
- - - - - - - - - - - <% workers.each do |process, thread, msg| %> - <% job = Sidekiq::JobRecord.new(msg['payload']) %> - - - - - - - - - - <% end %> -
<%= t('Process') %><%= t('TID') %><%= t('JID') %><%= t('Queue') %><%= t('Job') %><%= t('Arguments') %><%= t('Started') %>
<%= process %><%= thread %><%= job.jid %> - <%= msg['queue'] %> - - <%= job.display_class %> - <%= display_tags(job, nil) %> - -
<%= display_args(job.display_args) %>
-
<%= relative_time(Time.at(msg['run_at'])) %>
-
diff --git a/web/views/dashboard.erb b/web/views/dashboard.erb deleted file mode 100644 index ae51c9b6..00000000 --- a/web/views/dashboard.erb +++ /dev/null @@ -1,83 +0,0 @@ - -
-

- <%= t('Dashboard') %> - - - - -

-
- <%= t('PollingInterval') %>: - 5 sec -
- -
-
- -
-
-
-
- -
-
-

<%= t('History') %>

-
-
- - -
-
-
-

Redis

-
-
-
-
- <% if @redis_info.fetch("redis_version", nil) %> -
-

<%= @redis_info.fetch("redis_version") %>

-

<%= t('Version') %>

-
- <% end %> - - <% if @redis_info.fetch("uptime_in_days", nil) %> -
-

<%= @redis_info.fetch("uptime_in_days") %>

-

<%= t('Uptime') %>

-
- <% end %> - - <% if @redis_info.fetch("connected_clients", nil) %> -
-

<%= @redis_info.fetch("connected_clients") %>

-

<%= t('Connections') %>

-
- <% end %> - - <% if @redis_info.fetch("used_memory_human", nil) %> -
-

<%= @redis_info.fetch("used_memory_human") %>

-

<%= t('MemoryUsage') %>

-
- <% end %> - - <% if @redis_info.fetch("used_memory_peak_human", nil) %> -
-

<%= @redis_info.fetch("used_memory_peak_human") %>

-

<%= t('PeakMemoryUsage') %>

-
- <% end %> -
-
diff --git a/web/views/dead.erb b/web/views/dead.erb deleted file mode 100644 index 4f35ac93..00000000 --- a/web/views/dead.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%= erb :_job_info, locals: { job: @dead, type: :dead } %> - -

<%= t('Error') %>

-
- - - - - - - - - - - <% if @dead.error_backtrace %> - - - - - <% end %> - -
<%= t('ErrorClass') %> - <%= @dead['error_class'] %> -
<%= t('ErrorMessage') %><%= h(@dead['error_message']) %>
<%= t('ErrorBacktrace') %> - <%= @dead.error_backtrace.join("
") %>
-
-
- -
- <%= csrf_tag %> - <%= t('GoBack') %> - - -
diff --git a/web/views/layout.erb b/web/views/layout.erb deleted file mode 100644 index 3cbe9c2b..00000000 --- a/web/views/layout.erb +++ /dev/null @@ -1,42 +0,0 @@ - - - - <%= environment_title_prefix %><%= Sidekiq::NAME %> - - - - - <% if rtl? %> - - <% end %> - - - - <% if rtl? %> - - <% end %> - - - - - - <%= display_custom_head %> - - - <%= erb :_nav %> -
-
-
-
- <%= erb :_summary %> -
- -
- <%= yield %> -
-
-
-
- <%= erb :_footer %> - - diff --git a/web/views/morgue.erb b/web/views/morgue.erb deleted file mode 100644 index 411e5720..00000000 --- a/web/views/morgue.erb +++ /dev/null @@ -1,78 +0,0 @@ -
-
-

<%= t('DeadJobs') %>

-
- <% if @dead.size > 0 && @total_size > @count %> -
- <%= erb :_paging, locals: { url: "#{root_path}morgue" } %> -
- <% end %> - <%= filtering('dead') %> -
- -<% if @dead.size > 0 %> -
- <%= csrf_tag %> -
- - - - - - - - - - - - <% @dead.each do |entry| %> - - - - - - - - - <% end %> -
- - <%= t('LastRetry') %><%= t('Queue') %><%= t('Job') %><%= t('Arguments') %><%= t('Error') %>
- - - <%= relative_time(entry.at) %> - - <%= entry.queue %> - - <%= entry.display_class %> - <%= display_tags(entry, "dead") %> - -
<%= display_args(entry.display_args) %>
-
- <% if entry.error? %> -
<%= h truncate("#{entry['error_class']}: #{entry['error_message']}", 200) %>
- <% end %> -
-
- - -
- - <% unfiltered? do %> -
- <%= csrf_tag %> - -
-
- <%= csrf_tag %> - -
- <% end %> - -<% else %> -
<%= t('NoDeadJobsFound') %>
-<% end %> diff --git a/web/views/queue.erb b/web/views/queue.erb deleted file mode 100644 index 57585112..00000000 --- a/web/views/queue.erb +++ /dev/null @@ -1,55 +0,0 @@ -
-
-

- <%= t('CurrentMessagesInQueue', :queue => h(@name)) %> - <% if @queue.paused? %> - <%= t('Paused') %> - <% end %> - <%= number_with_delimiter(@total_size) %> -

-
-
- <%= erb :_paging, locals: { url: "#{root_path}queues/#{CGI.escape(@name)}" } %> -
-
-
- - - - - - - - <% @jobs.each_with_index do |job, index| %> - - <% if params[:direction] == 'asc' %> - - <% else %> - - <% end %> - - - - - <% end %> -
# <%= sort_direction_label %><%= t('Job') %><%= t('Arguments') %>
<%= @count * (@current_page - 1) + index + 1 %><%= @total_size - (@count * (@current_page - 1) + index) %> - <%= h(job.display_class) %> - <%= display_tags(job, nil) %> - - <% a = job.display_args %> - <% if a.inspect.size > 100 %> - <%= h(a.inspect[0..100]) + "... " %> - -
<%= display_args(a) %>
- <% else %> - <%= display_args(job.display_args) %> - <% end %> -
-
- <%= csrf_tag %> - - -
-
-
-<%= erb :_paging, locals: { url: "#{root_path}queues/#{CGI.escape(@name)}" } %> diff --git a/web/views/queues.erb b/web/views/queues.erb deleted file mode 100644 index 9d090739..00000000 --- a/web/views/queues.erb +++ /dev/null @@ -1,38 +0,0 @@ -

<%= t('Queues') %>

- -
- - - - - - - - <% @queues.each do |queue| %> - - - - - - - <% end %> -
<%= t('Queue') %><%= t('Size') %><%= t('Latency') %><%= t('Actions') %>
- <%= h queue.name %> - <% if queue.paused? %> - <%= t('Paused') %> - <% end %> - <%= number_with_delimiter(queue.size) %> <% queue_latency = queue.latency %><%= number_with_delimiter(queue_latency.round(2)) %><%= (queue_latency < 60) ? '' : " (#{relative_time(Time.at(Time.now.to_f - queue_latency))})" %> -
- <%= csrf_tag %> - - - <% if Sidekiq.pro? %> - <% if queue.paused? %> - - <% else %> - - <% end %> - <% end %> -
-
-
diff --git a/web/views/retries.erb b/web/views/retries.erb deleted file mode 100644 index a46ed1e8..00000000 --- a/web/views/retries.erb +++ /dev/null @@ -1,83 +0,0 @@ -
-
-

<%= t('Retries') %>

-
- <% if @retries.size > 0 && @total_size > @count %> -
- <%= erb :_paging, locals: { url: "#{root_path}retries" } %> -
- <% end %> - <%= filtering('retries') %> -
- -<% if @retries.size > 0 %> -
- <%= csrf_tag %> -
- - - - - - - - - - - - - <% @retries.each do |entry| %> - - - - - - - - - - <% end %> -
- - <%= t('NextRetry') %><%= t('RetryCount') %><%= t('Queue') %><%= t('Job') %><%= t('Arguments') %><%= t('Error') %>
- - - <%= relative_time(entry.at) %> - <%= entry['retry_count'] %> - <%= entry.queue %> - - <%= entry.display_class %> - <%= display_tags(entry, "retries") %> - -
<%= display_args(entry.display_args) %>
-
-
<%= h truncate("#{entry['error_class']}: #{entry['error_message']}", 200) %>
-
-
- - - -
- - <% unfiltered? do %> -
- <%= csrf_tag %> - -
-
- <%= csrf_tag %> - -
-
- <%= csrf_tag %> - -
- <% end %> - -<% else %> -
<%= t('NoRetriesFound') %>
-<% end %> diff --git a/web/views/retry.erb b/web/views/retry.erb deleted file mode 100644 index 52ad9ac4..00000000 --- a/web/views/retry.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%= erb :_job_info, locals: { job: @retry, type: :retry } %> - -

<%= t('Error') %>

-
- - - - - - - - - - - <% if @retry.error_backtrace %> - - - - - <% end %> - -
<%= t('ErrorClass') %> - <%= h @retry['error_class'] %> -
<%= t('ErrorMessage') %><%= h(@retry['error_message']) %>
<%= t('ErrorBacktrace') %> - <%= @retry.error_backtrace.join("
") %>
-
-
- -
- <%= csrf_tag %> - <%= t('GoBack') %> - - -
diff --git a/web/views/scheduled.erb b/web/views/scheduled.erb deleted file mode 100644 index 0ee8aee3..00000000 --- a/web/views/scheduled.erb +++ /dev/null @@ -1,57 +0,0 @@ -
-
-

<%= t('ScheduledJobs') %>

-
- <% if @scheduled.size > 0 && @total_size > @count %> -
- <%= erb :_paging, locals: { url: "#{root_path}scheduled" } %> -
- <% end %> - <%= filtering('scheduled') %> -
- -<% if @scheduled.size > 0 %> - -
- <%= csrf_tag %> -
- - - - - - - - - - - <% @scheduled.each do |entry| %> - - - - - - - - <% end %> -
- - <%= t('When') %><%= t('Queue') %><%= t('Job') %><%= t('Arguments') %>
- - - <%= relative_time(entry.at) %> - - <%= entry.queue %> - - <%= entry.display_class %> - <%= display_tags(entry, "scheduled") %> - -
<%= display_args(entry.display_args) %>
-
-
- - -
-<% else %> -
<%= t('NoScheduledFound') %>
-<% end %> diff --git a/web/views/scheduled_job_info.erb b/web/views/scheduled_job_info.erb deleted file mode 100644 index 4f532616..00000000 --- a/web/views/scheduled_job_info.erb +++ /dev/null @@ -1,8 +0,0 @@ -<%= erb :_job_info, locals: { job: @job, type: :scheduled } %> - -
- <%= csrf_tag %> - <%= t('GoBack') %> - - -