delayed_job_heartbeat_plugin 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +87 -0
- data/Rakefile +6 -0
- data/bin/setup +7 -0
- data/delayed_job_heartbeat_plugin.gemspec +39 -0
- data/lib/delayed/heartbeat.rb +46 -0
- data/lib/delayed/heartbeat/compatibility.rb +14 -0
- data/lib/delayed/heartbeat/configuration.rb +29 -0
- data/lib/delayed/heartbeat/delete_worker_results.rb +43 -0
- data/lib/delayed/heartbeat/plugin.rb +22 -0
- data/lib/delayed/heartbeat/railtie.rb +12 -0
- data/lib/delayed/heartbeat/tasks.rb +24 -0
- data/lib/delayed/heartbeat/version.rb +7 -0
- data/lib/delayed/heartbeat/worker.rb +69 -0
- data/lib/delayed/heartbeat/worker_heartbeat.rb +91 -0
- data/lib/delayed_job_heartbeat_plugin.rb +7 -0
- data/lib/generators/delayed_job_heartbeat_plugin/install_generator.rb +20 -0
- data/lib/generators/delayed_job_heartbeat_plugin/templates/migration.rb +13 -0
- data/spec/db/database.yml +5 -0
- data/spec/db/schema.rb +23 -0
- data/spec/delayed/heartbeat/delete_worker_results_spec.rb +47 -0
- data/spec/delayed/heartbeat/worker_heartbeat_spec.rb +81 -0
- data/spec/delayed/heartbeat_spec.rb +82 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/destroyed_model.rb +15 -0
- data/spec/support/test_job.rb +5 -0
- data/spec/support/test_job_with_callbacks.rb +16 -0
- data/spec/support/wait.rb +13 -0
- metadata +232 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d23c84e35c7097a52dd7c4ff8431ec634adc72b4
|
4
|
+
data.tar.gz: 626b0561368160ea52dd2b7d4cf65240768cd3c7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fa8a6ff6c94fef094132d2625eb534a2f9b68730fae7696e57001ea513aa28ec7966bfff1b8c9f3e41ac10209ee817c3f495e5288aa4c445999def8c83fa2692
|
7
|
+
data.tar.gz: 5a13041ce00bf26a2dfccd022831f32c1273587e701c9e759423519ae3bbaf4ffe35677bb11ff4c81f2d9bfc6e66114d37bb979d22c33d322a393e03a7b46156
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
before_install: gem install bundler -v 1.10.6
|
4
|
+
env:
|
5
|
+
matrix:
|
6
|
+
- RAILS_VERSION="~> 3.2.22"
|
7
|
+
- RAILS_VERSION="~> 4.0.13"
|
8
|
+
- RAILS_VERSION="~> 4.1.13"
|
9
|
+
- RAILS_VERSION="~> 4.2.4"
|
10
|
+
rvm:
|
11
|
+
- 2.0.0
|
12
|
+
- 2.1.7
|
13
|
+
- 2.2.3
|
14
|
+
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Joel Turkel
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Delayed Job Heartbeat Plugin
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/delayed_job_heartbeat_plugin.png)][gem]
|
4
|
+
[![Build Status](https://secure.travis-ci.org/salsify/delayed_job_heartbeat_plugin.png?branch=master)][travis]
|
5
|
+
[![Code Climate](https://codeclimate.com/github/salsify/delayed_job_heartbeat_plugin.png)][codeclimate]
|
6
|
+
[![Coverage Status](https://coveralls.io/repos/salsify/delayed_job_heartbeat_plugin/badge.png)][coveralls]
|
7
|
+
|
8
|
+
[gem]: https://rubygems.org/gems/delayed_job_heartbeat_plugin
|
9
|
+
[travis]: http://travis-ci.org/salsify/delayed_job_heartbeat_plugin
|
10
|
+
[codeclimate]: https://codeclimate.com/github/salsify/delayed_job_heartbeat_plugin
|
11
|
+
[coveralls]: https://coveralls.io/r/salsify/delayed_job_heartbeat_plugin
|
12
|
+
|
13
|
+
By default [Delayed Job](https://github.com/collectiveidea/delayed_job) uses the [ClearLocks](https://github.com/collectiveidea/delayed_job/blob/master/lib/delayed/plugins/clear_locks.rb) plugin to unlock jobs when a worker shuts down. Unfortunately this only works if a worker shuts down cleanly. If the worker crashes, the job won't be unlocked until `max_run_time` elapses which can cause unacceptable delays processing background jobs. Enter the Delayed Job Heartbeat Plugin...
|
14
|
+
|
15
|
+
The Delayed Job Heartbeat Plugin adds a heartbeat to all Delayed Job workers. After a configurable timeout jobs from unresponsive workers can be unlocked either via a Ruby API or a rake task.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'delayed_job_heartbeat_plugin'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install delayed_job_heartbeat_plugin
|
32
|
+
|
33
|
+
Run the required database migrations:
|
34
|
+
|
35
|
+
$ rails generate delayed_job_heartbeat_plugin:install
|
36
|
+
$ rake db:migrate
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
There are two parts to using the Delayed Job Heartbeat Plugin:
|
41
|
+
|
42
|
+
* Configuring the worker's heartbeat options.
|
43
|
+
* Periodically unlocking jobs from unresponsive workers.
|
44
|
+
|
45
|
+
### Worker Heartbeat Options
|
46
|
+
|
47
|
+
The worker heartbeat can be configured in an initializer (e.g. `config/initializers/delayed_job_heartbeat.rb`) follows:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
Delayed::Heartbeat.configure do |configuration|
|
51
|
+
configuration.enabled = Rails.env.production?
|
52
|
+
configuration.heartbeat_interval_seconds = 60
|
53
|
+
configuration.heartbeat_timeout_seconds = 180
|
54
|
+
configuration.worker_termination_enabled = true
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
The plugin supports the following options (all of which are optional):
|
59
|
+
|
60
|
+
* `enabled` - enables/disables the plugin entirely (defaults to true in production and false in other environments)
|
61
|
+
* `worker_label` - a label for the worker. Consider setting this to `ENV['DYNO']` if running in Heroku to get the dyno's friendly name (defaults to the Delayed Job worker's name)
|
62
|
+
* `heartbeat_interval_seconds` - how often workers should send a heartbeat (defaults to 60 seconds)
|
63
|
+
* `heartbeat_timeout_seconds` - theshold after which workers are considered dead if they haven't heartbeated (defaults to 180 seconds)
|
64
|
+
* `worker_termination_enabled` - controls whether a worker that detects it has not heartbeated within the timeout period (e.g. due to severe memory swapping) should shut itself down (defaults to true in production and false in other environments)
|
65
|
+
* `on_worker_termination` - a callback proc that accepts a `Delayed::Heartbeat::Worker` and an `Exception` if the heartbeat fails. This can be useful for reporting to an error monitoring system.
|
66
|
+
* `worker_version` - a version number of the worker's source code that is only used if you want to cleanup workers from old source code versions (defaults to `nil`)
|
67
|
+
|
68
|
+
### Unlocking Unresponsive Workers
|
69
|
+
|
70
|
+
Jobs from unresponsive workers can be unlocked by calling `Delayed::Heartbeat.delete_timed_out_workers` or running the `delayed:heartbeat:delete_timed_out_workers` rake task. This should be integrated with a scheduler like cron, [clockwork](https://github.com/tomykaira/clockwork) or the [Heroku Scheduler](https://devcenter.heroku.com/articles/scheduler) (if running in Heroku).
|
71
|
+
|
72
|
+
Jobs from workers running an old version of the source code can be unlocked by calling `Delayed::Heartbeat.delete_workers_with_different_version` or running the `delayed:heartbeat:delete_workers_with_different_version` rake task.
|
73
|
+
|
74
|
+
## Development
|
75
|
+
|
76
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
77
|
+
|
78
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/salsify/delayed_job_heartbeat_plugin.
|
83
|
+
|
84
|
+
|
85
|
+
## License
|
86
|
+
|
87
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/setup
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'delayed/heartbeat/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'delayed_job_heartbeat_plugin'
|
8
|
+
spec.version = Delayed::Heartbeat::VERSION
|
9
|
+
spec.authors = ['Joel Turkel']
|
10
|
+
spec.email = ['jturkel@salsify.com']
|
11
|
+
|
12
|
+
spec.summary = 'Delayed::Job plugin to unlock jobs from dead workers'
|
13
|
+
spec.homepage = 'https://github.com/salsify/delayed_job_heartbeat_plugin'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.test_files = Dir.glob('spec/**/*')
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.required_ruby_version = '>= 2.0'
|
21
|
+
|
22
|
+
spec.add_dependency 'delayed_job', '>= 4.1.0'
|
23
|
+
spec.add_dependency 'delayed_job_active_record', '>= 4.1.0'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'activerecord', ENV.fetch('RAILS_VERSION', ['>= 3.2', '< 4.3'])
|
26
|
+
spec.add_development_dependency 'coveralls'
|
27
|
+
spec.add_development_dependency 'database_cleaner', '>= 1.2'
|
28
|
+
spec.add_development_dependency 'rake'
|
29
|
+
spec.add_development_dependency 'rspec', '3.3.0'
|
30
|
+
spec.add_development_dependency 'simplecov', '~> 0.7.1'
|
31
|
+
spec.add_development_dependency 'timecop'
|
32
|
+
|
33
|
+
if RUBY_PLATFORM == 'java'
|
34
|
+
spec.add_development_dependency 'jdbc-sqlite3'
|
35
|
+
spec.add_development_dependency 'activerecord-jdbcsqlite3-adapter'
|
36
|
+
else
|
37
|
+
spec.add_development_dependency 'sqlite3'
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'delayed/heartbeat/compatibility'
|
2
|
+
require 'delayed/heartbeat/configuration'
|
3
|
+
require 'delayed/heartbeat/delete_worker_results'
|
4
|
+
require 'delayed/heartbeat/plugin'
|
5
|
+
require 'delayed/heartbeat/version'
|
6
|
+
require 'delayed/heartbeat/worker'
|
7
|
+
require 'delayed/heartbeat/worker_heartbeat'
|
8
|
+
require 'delayed/heartbeat/railtie' if defined?(Rails::Railtie)
|
9
|
+
|
10
|
+
module Delayed
|
11
|
+
module Heartbeat
|
12
|
+
@configuration = Delayed::Heartbeat::Configuration.new
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configure
|
16
|
+
yield(configuration) if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
def configuration
|
20
|
+
@configuration
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete_workers_with_different_version(current_version = configuration.worker_version)
|
24
|
+
old_workers = Delayed::Heartbeat::Worker.workers_with_different_version(current_version)
|
25
|
+
cleanup_workers(old_workers, mark_attempt_failed: false)
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_timed_out_workers(timeout_seconds = configuration.heartbeat_timeout_seconds)
|
29
|
+
dead_workers = Delayed::Heartbeat::Worker.dead_workers(timeout_seconds)
|
30
|
+
cleanup_workers(dead_workers, mark_attempt_failed: true)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def cleanup_workers(workers, mark_attempt_failed: true)
|
36
|
+
Delayed::Heartbeat::Worker.transaction do
|
37
|
+
orphaned_jobs = workers.flat_map do |worker|
|
38
|
+
worker.unlock_jobs(mark_attempt_failed: mark_attempt_failed)
|
39
|
+
end
|
40
|
+
Delayed::Heartbeat::Worker.delete_workers(workers)
|
41
|
+
Delayed::Heartbeat::DeleteWorkerResults.new(workers, orphaned_jobs)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'active_support/version'
|
2
|
+
require 'active_record/version'
|
3
|
+
|
4
|
+
module Delayed
|
5
|
+
module Heartbeat
|
6
|
+
module Compatibility
|
7
|
+
|
8
|
+
def self.mass_assignment_security_enabled?
|
9
|
+
::ActiveRecord::VERSION::MAJOR < 4 || defined?(::ActiveRecord::MassAssignmentSecurity)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Delayed
|
2
|
+
module Heartbeat
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :enabled, :worker_label, :worker_version,
|
5
|
+
:heartbeat_timeout_seconds, :heartbeat_interval_seconds,
|
6
|
+
:worker_termination_enabled, :on_worker_termination
|
7
|
+
alias_method :enabled?, :enabled
|
8
|
+
alias_method :worker_termination_enabled?, :worker_termination_enabled
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
options.each do |key, value|
|
12
|
+
send("#{key}=", value)
|
13
|
+
end
|
14
|
+
|
15
|
+
if enabled.nil?
|
16
|
+
self.enabled = defined?(Rails) ? Rails.env.production? : true
|
17
|
+
end
|
18
|
+
|
19
|
+
if worker_termination_enabled.nil?
|
20
|
+
self.worker_termination_enabled = defined?(Rails) ? Rails.env.production? : true
|
21
|
+
end
|
22
|
+
|
23
|
+
self.heartbeat_timeout_seconds ||= 180
|
24
|
+
self.heartbeat_interval_seconds ||= 60
|
25
|
+
self.on_worker_termination ||= Proc.new { }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Delayed
|
2
|
+
module Heartbeat
|
3
|
+
class DeleteWorkerResults
|
4
|
+
attr_reader :workers, :unlocked_jobs
|
5
|
+
|
6
|
+
def initialize(workers, unlocked_jobs)
|
7
|
+
@workers = workers
|
8
|
+
@unlocked_jobs = unlocked_jobs
|
9
|
+
end
|
10
|
+
|
11
|
+
def empty?
|
12
|
+
workers.empty? && unlocked_jobs.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
io = StringIO.new
|
17
|
+
workers.each do |worker|
|
18
|
+
io.puts("Deleted worker #{worker_description(worker)}")
|
19
|
+
end
|
20
|
+
|
21
|
+
unlocked_jobs.each do |unlocked_job|
|
22
|
+
worker = worker_map[unlocked_job.locked_by]
|
23
|
+
worker_string = worker ? worker_description(worker) : unlocked_job.locked_by
|
24
|
+
io.puts("Unlocked orphaned job #{unlocked_job.id} from worker #{worker_string}")
|
25
|
+
end
|
26
|
+
|
27
|
+
io.string
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def worker_map
|
33
|
+
@worker_map ||= workers.each_with_object(Hash.new) do |worker, worker_map|
|
34
|
+
worker_map[worker.name] = worker
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def worker_description(worker)
|
39
|
+
"#{worker.label}(#{worker.name})"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Delayed
|
4
|
+
module Heartbeat
|
5
|
+
class Plugin < Delayed::Plugin
|
6
|
+
|
7
|
+
callbacks do |lifecycle|
|
8
|
+
lifecycle.before(:execute) do |worker|
|
9
|
+
if Delayed::Heartbeat.configuration.enabled?
|
10
|
+
@heartbeat = Delayed::Heartbeat::WorkerHeartbeat.new(worker.name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
lifecycle.after(:execute) do |worker|
|
15
|
+
@heartbeat.stop if @heartbeat
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
namespace :delayed do
|
2
|
+
namespace :heartbeat do
|
3
|
+
desc 'Cleans up workers that have not heartbeated recently.'
|
4
|
+
task delete_timed_out_workers: :environment do
|
5
|
+
results = Delayed::Heartbeat.delete_timed_out_workers
|
6
|
+
print_results(results)
|
7
|
+
end
|
8
|
+
|
9
|
+
desc 'Cleans up workers running a different version.'
|
10
|
+
task delete_workers_with_different_version: :environment do
|
11
|
+
results = Delayed::Heartbeat.delete_workers_with_different_version
|
12
|
+
print_results(results)
|
13
|
+
end
|
14
|
+
|
15
|
+
def print_results(results)
|
16
|
+
puts "Deleted #{results.workers.size} and unlocked #{results.unlocked_jobs.size} orphaned jobs"
|
17
|
+
puts results.to_s if verbose? && results.present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def verbose?
|
21
|
+
ENV['VERBOSE'].to_s.downcase == 'true'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'delayed/heartbeat/compatibility'
|
2
|
+
|
3
|
+
module Delayed
|
4
|
+
module Heartbeat
|
5
|
+
class Worker < ActiveRecord::Base
|
6
|
+
self.table_name = 'delayed_workers'
|
7
|
+
|
8
|
+
if Delayed::Heartbeat::Compatibility.mass_assignment_security_enabled?
|
9
|
+
attr_accessible :name, :version, :last_heartbeat_at, :host_name, :label
|
10
|
+
end
|
11
|
+
|
12
|
+
before_create do |model|
|
13
|
+
model.last_heartbeat_at ||= Time.now.utc
|
14
|
+
model.host_name ||= Socket.gethostname
|
15
|
+
model.label ||= Delayed::Heartbeat.configuration.worker_label || name
|
16
|
+
model.version ||= Delayed::Heartbeat.configuration.worker_version
|
17
|
+
end
|
18
|
+
|
19
|
+
def jobs
|
20
|
+
Delayed::Job.where(locked_by: name, failed_at: nil)
|
21
|
+
end
|
22
|
+
|
23
|
+
def job
|
24
|
+
jobs.first
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the jobs that were unlocked
|
28
|
+
def unlock_jobs(mark_attempt_failed: true)
|
29
|
+
orphaned_jobs = jobs.to_a
|
30
|
+
return orphaned_jobs unless orphaned_jobs.present?
|
31
|
+
|
32
|
+
if mark_attempt_failed
|
33
|
+
mark_job_attempts_failed(orphaned_jobs)
|
34
|
+
else
|
35
|
+
Delayed::Job.where(id: orphaned_jobs.map(&:id)).update_all(locked_at: nil, locked_by: nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
orphaned_jobs
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.dead_workers(timeout_seconds)
|
42
|
+
where('last_heartbeat_at < ?', Time.now.utc - timeout_seconds)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.workers_with_different_version(current_version)
|
46
|
+
where('version != ?', current_version)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.delete_workers(workers)
|
50
|
+
where(id: workers.map(&:id)).delete_all if workers.present?
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def mark_job_attempts_failed(jobs)
|
56
|
+
dj_worker = Delayed::Worker.new
|
57
|
+
jobs.each do |job|
|
58
|
+
mark_job_attempt_failed(dj_worker, job)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def mark_job_attempt_failed(dj_worker, job)
|
63
|
+
# If there are more attempts this reschedules the job otherwise marks it as failed
|
64
|
+
# and runs appropriate callbacks
|
65
|
+
dj_worker.reschedule(job)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Delayed
|
2
|
+
module Heartbeat
|
3
|
+
class WorkerHeartbeat
|
4
|
+
|
5
|
+
def initialize(worker_name)
|
6
|
+
@worker_model = create_worker_model(worker_name)
|
7
|
+
|
8
|
+
# Use a self-pipe to safely shutdown the heartbeat thread
|
9
|
+
@stop_reader, @stop_writer = IO.pipe
|
10
|
+
|
11
|
+
yield(self) if block_given?
|
12
|
+
|
13
|
+
@heartbeat_thread = Thread.new { run_heartbeat_loop }
|
14
|
+
# Make this a high priority thread to try to ensure it runs
|
15
|
+
@heartbeat_thread.priority = 100
|
16
|
+
end
|
17
|
+
|
18
|
+
def alive?
|
19
|
+
@heartbeat_thread.alive?
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
# Use the self-pipe to tell the heartbeat thread to cleanly
|
24
|
+
# shutdown
|
25
|
+
if @stop_writer
|
26
|
+
@stop_writer.close
|
27
|
+
@stop_writer = nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def create_worker_model(worker_name)
|
34
|
+
Delayed::Heartbeat::Worker.transaction do
|
35
|
+
Delayed::Heartbeat::Worker.where(name: worker_name).delete_all
|
36
|
+
Delayed::Heartbeat::Worker.create!(name: worker_name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_heartbeat_loop
|
41
|
+
loop do
|
42
|
+
break if sleep_interruptibly(heartbeat_interval)
|
43
|
+
update_heartbeat
|
44
|
+
# Return the connection back to the pool since we won't be needing
|
45
|
+
# it again for a while.
|
46
|
+
Delayed::Backend::ActiveRecord::Job.clear_active_connections!
|
47
|
+
end
|
48
|
+
rescue => e
|
49
|
+
# We don't want the worker to continue running if the heartbeat can't be written.
|
50
|
+
# Don't use Thread.abort_on_exception because that will give Delayed::Job a chance
|
51
|
+
# to mark the job as failed which will unlock it even though the clock
|
52
|
+
# process has likely already unlocked it and another worker may have picked it up.
|
53
|
+
Delayed::Heartbeat.configuration.on_worker_termination.call(@worker_model, e)
|
54
|
+
exit(false)
|
55
|
+
ensure
|
56
|
+
@stop_reader.close
|
57
|
+
@worker_model.delete
|
58
|
+
# Note: The built-in Delayed::Plugins::ClearLocks will unlock the jobs for us
|
59
|
+
Delayed::Backend::ActiveRecord::Job.clear_active_connections!
|
60
|
+
end
|
61
|
+
|
62
|
+
def update_heartbeat
|
63
|
+
now = Time.now.utc
|
64
|
+
heartbeat_delta_seconds = now - @worker_model.last_heartbeat_at
|
65
|
+
if heartbeat_delta_seconds < heartbeat_timeout_seconds || self_termination_disabled?
|
66
|
+
@worker_model.update_column(:last_heartbeat_at, now)
|
67
|
+
else
|
68
|
+
raise Timeout::Error, "Worker heartbeat not updated for #{heartbeat_delta_seconds} seconds which " \
|
69
|
+
"exceeds timeout\n. Current job: #{ @worker_model.job.inspect}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self_termination_disabled?
|
74
|
+
!Delayed::Heartbeat.configuration.worker_termination_enabled?
|
75
|
+
end
|
76
|
+
|
77
|
+
def heartbeat_timeout_seconds
|
78
|
+
Delayed::Heartbeat.configuration.heartbeat_timeout_seconds
|
79
|
+
end
|
80
|
+
|
81
|
+
def heartbeat_interval
|
82
|
+
Delayed::Heartbeat.configuration.heartbeat_interval_seconds
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a truthy if the sleep was interrupted
|
86
|
+
def sleep_interruptibly(secs)
|
87
|
+
IO.select([@stop_reader], nil, nil, secs)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
require 'rails/generators/active_record'
|
4
|
+
|
5
|
+
module DelayedJobHeartbeatPlugin
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
|
9
|
+
self.source_paths << File.join(File.dirname(__FILE__), 'templates')
|
10
|
+
|
11
|
+
def create_migration_file
|
12
|
+
migration_template('migration.rb', 'db/migrate/create_delayed_workers.rb')
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.next_migration_number(dirname)
|
16
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/spec/db/schema.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
ActiveRecord::Schema.define(version: 0) do
|
2
|
+
|
3
|
+
create_table(:delayed_jobs, force: true) do |t|
|
4
|
+
t.integer :priority, default: 0
|
5
|
+
t.integer :attempts, default: 0
|
6
|
+
t.text :handler
|
7
|
+
t.text :last_error
|
8
|
+
t.datetime :run_at
|
9
|
+
t.datetime :locked_at
|
10
|
+
t.datetime :failed_at
|
11
|
+
t.string :locked_by
|
12
|
+
t.string :queue
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
|
16
|
+
create_table(:delayed_workers, force: true) do |t|
|
17
|
+
t.string :name
|
18
|
+
t.string :version
|
19
|
+
t.datetime :last_heartbeat_at
|
20
|
+
t.string :host_name
|
21
|
+
t.string :label
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Delayed::Heartbeat::DeleteWorkerResults do
|
4
|
+
let(:worker) { create_worker(name: 'my-worker') }
|
5
|
+
let(:job) { create_job(locked_by: worker.name) }
|
6
|
+
let(:results) { create_results([worker], [job]) }
|
7
|
+
|
8
|
+
describe "#workers" do
|
9
|
+
specify { expect(results.workers).to eq [worker] }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#unlocked_jobs" do
|
13
|
+
specify { expect(results.unlocked_jobs).to eq [job] }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#to_s" do
|
17
|
+
it "includes workers" do
|
18
|
+
results = create_results([worker], [])
|
19
|
+
expect(results.to_s).to include(worker.name)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "includes jobs for known workers" do
|
23
|
+
results = create_results([worker], [job])
|
24
|
+
expect(results.to_s).to include(job.id.to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "includes jobs locked by an unknown worker" do
|
28
|
+
job = create_job(locked_by: 'unknown-worker')
|
29
|
+
results = create_results([], [job])
|
30
|
+
expect(results.to_s).to include(job.id.to_s)
|
31
|
+
expect(results.to_s).to include(job.locked_by)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_worker(attributes = {})
|
36
|
+
Delayed::Heartbeat::Worker.create!(attributes)
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_job(attributes = {})
|
40
|
+
attributes = attributes.reverse_merge(payload_object: TestJob.new)
|
41
|
+
Delayed::Job.create!(attributes)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_results(workers, unlocked_jobs)
|
45
|
+
Delayed::Heartbeat::DeleteWorkerResults.new(workers, unlocked_jobs)
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Delayed::Heartbeat::WorkerHeartbeat, cleaner_strategy: :truncation do
|
4
|
+
let(:worker_name) { 'Test Worker' }
|
5
|
+
let(:worker_model) { find_worker_model(worker_name) }
|
6
|
+
let(:heartbeat_config) do
|
7
|
+
Delayed::Heartbeat::Configuration.new(heartbeat_interval_seconds: 0.001)
|
8
|
+
end
|
9
|
+
|
10
|
+
before do
|
11
|
+
allow(Delayed::Heartbeat).to receive(:configuration).and_return(heartbeat_config)
|
12
|
+
|
13
|
+
Delayed::Job.create!(locked_by: worker_name, payload_object: TestJob.new)
|
14
|
+
@worker_heartbeat = start_heartbeat
|
15
|
+
end
|
16
|
+
|
17
|
+
after do
|
18
|
+
stop_heartbeat
|
19
|
+
end
|
20
|
+
|
21
|
+
it "creates a worker model" do
|
22
|
+
expect(worker_model).to be_present
|
23
|
+
end
|
24
|
+
|
25
|
+
it "updates the worker heartbeat" do
|
26
|
+
original_heartbeat = worker_model.last_heartbeat_at
|
27
|
+
Wait.for('worker heartbeat updated') do
|
28
|
+
worker_model.reload.last_heartbeat_at != original_heartbeat
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when the heartbeat is stopped" do
|
33
|
+
let!(:job) { Delayed::Backend::ActiveRecord::Job.create!(locked_by: worker_model.name, locked_at: Time.now) }
|
34
|
+
|
35
|
+
before do
|
36
|
+
stop_heartbeat
|
37
|
+
end
|
38
|
+
|
39
|
+
it "destroys the worker model" do
|
40
|
+
expect(find_worker_model(worker_name)).not_to be_present
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when the heartbeat times out" do
|
45
|
+
let(:heartbeat_config) do
|
46
|
+
# Create a configuration where heartbeat is updated less frequently than the timeout
|
47
|
+
Delayed::Heartbeat::Configuration.new(heartbeat_interval_seconds: 0.001,
|
48
|
+
heartbeat_timeout_seconds: 0.00000001,
|
49
|
+
worker_termination_enabled: true)
|
50
|
+
end
|
51
|
+
|
52
|
+
before do
|
53
|
+
wait_for_heartbeat_thread_stopped
|
54
|
+
end
|
55
|
+
|
56
|
+
it "aborts the process" do
|
57
|
+
expect(@worker_heartbeat).to have_received(:exit).with(false)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def start_heartbeat
|
62
|
+
Delayed::Heartbeat::WorkerHeartbeat.new(worker_name) do |heartbeat|
|
63
|
+
allow(heartbeat).to receive(:exit)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def stop_heartbeat
|
68
|
+
@worker_heartbeat.stop
|
69
|
+
wait_for_heartbeat_thread_stopped
|
70
|
+
end
|
71
|
+
|
72
|
+
def wait_for_heartbeat_thread_stopped
|
73
|
+
Wait.for('worker thread stopped') do
|
74
|
+
!@worker_heartbeat.alive?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_worker_model(worker_name)
|
79
|
+
Delayed::Heartbeat::Worker.where(name: worker_name).first
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Delayed::Heartbeat do
|
4
|
+
let(:current_version) { 'current version' }
|
5
|
+
let!(:active_worker) { create_worker_model(name: 'active_worker', version: current_version) }
|
6
|
+
|
7
|
+
let!(:active_job) do
|
8
|
+
Delayed::Job.create!(locked_by: active_worker.name,
|
9
|
+
locked_at: active_worker.last_heartbeat_at,
|
10
|
+
payload_object: TestJob.new)
|
11
|
+
end
|
12
|
+
|
13
|
+
let!(:orphaned_job) do
|
14
|
+
Delayed::Job.create!(locked_by: dead_worker.name,
|
15
|
+
locked_at: dead_worker.last_heartbeat_at,
|
16
|
+
payload_object: TestJob.new)
|
17
|
+
end
|
18
|
+
|
19
|
+
shared_examples "it destroys a worker and unlocks its jobs" do |expected_orphaned_job_attempts: nil|
|
20
|
+
specify { expect(active_worker).not_to have_been_destroyed }
|
21
|
+
specify { expect(active_job.reload.locked_by).not_to be_nil }
|
22
|
+
specify { expect(active_job.reload.locked_at).not_to be_nil }
|
23
|
+
specify { expect(active_job.reload.attempts).to eq(0) }
|
24
|
+
specify { expect(active_job.reload.failed_at).to be_nil }
|
25
|
+
|
26
|
+
specify { expect(dead_worker).to have_been_destroyed }
|
27
|
+
specify { expect(orphaned_job.reload.locked_by).to be_nil }
|
28
|
+
specify { expect(orphaned_job.reload.locked_at).to be_nil }
|
29
|
+
specify { expect(orphaned_job.reload.attempts).to eq(expected_orphaned_job_attempts) }
|
30
|
+
specify { expect(orphaned_job.reload.failed_at).to be_nil }
|
31
|
+
end
|
32
|
+
|
33
|
+
describe ".delete_timed_out_workers" do
|
34
|
+
let!(:dead_worker) do
|
35
|
+
create_worker_model(name: 'dead_worker',
|
36
|
+
last_heartbeat_at: Time.now - Delayed::Heartbeat.configuration.heartbeat_timeout_seconds - 1)
|
37
|
+
end
|
38
|
+
|
39
|
+
let(:max_attempts) { 5 }
|
40
|
+
|
41
|
+
let!(:failed_orphaned_job) do
|
42
|
+
Delayed::Job.create!(locked_by: dead_worker.name, locked_at: dead_worker.last_heartbeat_at,
|
43
|
+
payload_object: TestJobWithCallbacks.new) do |job|
|
44
|
+
job.attempts = max_attempts - 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
before do
|
49
|
+
TestJobWithCallbacks.clear
|
50
|
+
@old_attempts = Delayed::Worker.max_attempts
|
51
|
+
Delayed::Worker.max_attempts = max_attempts
|
52
|
+
|
53
|
+
Delayed::Heartbeat.delete_timed_out_workers
|
54
|
+
end
|
55
|
+
|
56
|
+
after do
|
57
|
+
Delayed::Worker.max_attempts = @old_attempts
|
58
|
+
end
|
59
|
+
|
60
|
+
specify { expect(failed_orphaned_job.reload.failed_at).to be_present }
|
61
|
+
specify { expect(TestJobWithCallbacks.called_callbacks).to eq [:failure] }
|
62
|
+
|
63
|
+
it_behaves_like "it destroys a worker and unlocks its jobs", expected_orphaned_job_attempts: 1
|
64
|
+
end
|
65
|
+
|
66
|
+
describe ".delete_workers_with_different_version" do
|
67
|
+
let!(:dead_worker) { create_worker_model(name: 'old_worker', version: 'old version') }
|
68
|
+
|
69
|
+
before do
|
70
|
+
Delayed::Heartbeat.delete_workers_with_different_version(current_version)
|
71
|
+
end
|
72
|
+
|
73
|
+
it_behaves_like "it destroys a worker and unlocks its jobs", expected_orphaned_job_attempts: 0
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_worker_model(attributes)
|
77
|
+
Delayed::Heartbeat::Worker.create!(attributes)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'simplecov'
|
4
|
+
require 'coveralls'
|
5
|
+
|
6
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
7
|
+
SimpleCov::Formatter::HTMLFormatter,
|
8
|
+
Coveralls::SimpleCov::Formatter
|
9
|
+
]
|
10
|
+
SimpleCov.start do
|
11
|
+
add_filter 'spec'
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'database_cleaner'
|
15
|
+
require 'delayed_job_heartbeat_plugin'
|
16
|
+
require 'yaml'
|
17
|
+
require 'timecop'
|
18
|
+
|
19
|
+
spec_dir = File.dirname(__FILE__)
|
20
|
+
Dir["#{spec_dir}/support/**/*.rb"].sort.each { |f| require f }
|
21
|
+
|
22
|
+
FileUtils.makedirs('log')
|
23
|
+
FileUtils.makedirs('tmp')
|
24
|
+
|
25
|
+
Delayed::Worker.read_ahead = 1
|
26
|
+
Delayed::Worker.destroy_failed_jobs = false
|
27
|
+
|
28
|
+
Delayed::Worker.logger = Logger.new('log/test.log')
|
29
|
+
Delayed::Worker.logger.level = Logger::DEBUG
|
30
|
+
ActiveRecord::Base.logger = Delayed::Worker.logger
|
31
|
+
ActiveRecord::Migration.verbose = false
|
32
|
+
|
33
|
+
db_adapter = ENV.fetch('ADAPTER', 'sqlite3')
|
34
|
+
config = YAML.load(File.read('spec/db/database.yml'))
|
35
|
+
ActiveRecord::Base.establish_connection(config[db_adapter])
|
36
|
+
require 'db/schema'
|
37
|
+
|
38
|
+
RSpec.configure do |config|
|
39
|
+
config.order = 'random'
|
40
|
+
|
41
|
+
config.before(:suite) do
|
42
|
+
DatabaseCleaner.clean_with(:truncation)
|
43
|
+
end
|
44
|
+
|
45
|
+
config.before(:each) do |example|
|
46
|
+
DatabaseCleaner.strategy = example.metadata.fetch(:cleaner_strategy, :transaction)
|
47
|
+
end
|
48
|
+
|
49
|
+
config.before(:each) do
|
50
|
+
DatabaseCleaner.start
|
51
|
+
end
|
52
|
+
|
53
|
+
config.after(:each) do
|
54
|
+
DatabaseCleaner.clean
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
RSpec::Matchers.define :have_been_destroyed do
|
4
|
+
match do |actual|
|
5
|
+
!actual.class.where(id: actual.id).exists?
|
6
|
+
end
|
7
|
+
|
8
|
+
description do
|
9
|
+
"model should have been destroyed"
|
10
|
+
end
|
11
|
+
|
12
|
+
failure_message do |actual|
|
13
|
+
"expected #{actual.class}(id: #{actual.id}) to have been destroyed"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class TestJobWithCallbacks
|
2
|
+
cattr_accessor :called_callbacks
|
3
|
+
self.called_callbacks = []
|
4
|
+
|
5
|
+
def self.clear
|
6
|
+
called_callbacks.clear
|
7
|
+
end
|
8
|
+
|
9
|
+
def failure(*)
|
10
|
+
self.class.called_callbacks << :failure
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
self.class.called_callbacks << :perform
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Wait
|
2
|
+
def self.for(condition_name, max_wait_time: 5, polling_interval: 0.001)
|
3
|
+
wait_until = Time.now + max_wait_time.seconds
|
4
|
+
loop do
|
5
|
+
return if yield
|
6
|
+
if Time.now > wait_until
|
7
|
+
raise "Condition not met: #{condition_name}"
|
8
|
+
else
|
9
|
+
sleep(polling_interval)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: delayed_job_heartbeat_plugin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joel Turkel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: delayed_job
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: delayed_job_active_record
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.1.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.2'
|
48
|
+
- - <
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '4.3'
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '3.2'
|
58
|
+
- - <
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '4.3'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: coveralls
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: database_cleaner
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.2'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.2'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rake
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rspec
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - '='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 3.3.0
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - '='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 3.3.0
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: simplecov
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ~>
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 0.7.1
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ~>
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 0.7.1
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: timecop
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - '>='
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: sqlite3
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - '>='
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
description:
|
160
|
+
email:
|
161
|
+
- jturkel@salsify.com
|
162
|
+
executables: []
|
163
|
+
extensions: []
|
164
|
+
extra_rdoc_files: []
|
165
|
+
files:
|
166
|
+
- .gitignore
|
167
|
+
- .rspec
|
168
|
+
- .travis.yml
|
169
|
+
- Gemfile
|
170
|
+
- LICENSE.txt
|
171
|
+
- README.md
|
172
|
+
- Rakefile
|
173
|
+
- bin/setup
|
174
|
+
- delayed_job_heartbeat_plugin.gemspec
|
175
|
+
- lib/delayed/heartbeat.rb
|
176
|
+
- lib/delayed/heartbeat/compatibility.rb
|
177
|
+
- lib/delayed/heartbeat/configuration.rb
|
178
|
+
- lib/delayed/heartbeat/delete_worker_results.rb
|
179
|
+
- lib/delayed/heartbeat/plugin.rb
|
180
|
+
- lib/delayed/heartbeat/railtie.rb
|
181
|
+
- lib/delayed/heartbeat/tasks.rb
|
182
|
+
- lib/delayed/heartbeat/version.rb
|
183
|
+
- lib/delayed/heartbeat/worker.rb
|
184
|
+
- lib/delayed/heartbeat/worker_heartbeat.rb
|
185
|
+
- lib/delayed_job_heartbeat_plugin.rb
|
186
|
+
- lib/generators/delayed_job_heartbeat_plugin/install_generator.rb
|
187
|
+
- lib/generators/delayed_job_heartbeat_plugin/templates/migration.rb
|
188
|
+
- spec/db/database.yml
|
189
|
+
- spec/db/schema.rb
|
190
|
+
- spec/delayed/heartbeat/delete_worker_results_spec.rb
|
191
|
+
- spec/delayed/heartbeat/worker_heartbeat_spec.rb
|
192
|
+
- spec/delayed/heartbeat_spec.rb
|
193
|
+
- spec/spec_helper.rb
|
194
|
+
- spec/support/destroyed_model.rb
|
195
|
+
- spec/support/test_job.rb
|
196
|
+
- spec/support/test_job_with_callbacks.rb
|
197
|
+
- spec/support/wait.rb
|
198
|
+
homepage: https://github.com/salsify/delayed_job_heartbeat_plugin
|
199
|
+
licenses:
|
200
|
+
- MIT
|
201
|
+
metadata: {}
|
202
|
+
post_install_message:
|
203
|
+
rdoc_options: []
|
204
|
+
require_paths:
|
205
|
+
- lib
|
206
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
207
|
+
requirements:
|
208
|
+
- - '>='
|
209
|
+
- !ruby/object:Gem::Version
|
210
|
+
version: '2.0'
|
211
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - '>='
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
requirements: []
|
217
|
+
rubyforge_project:
|
218
|
+
rubygems_version: 2.2.2
|
219
|
+
signing_key:
|
220
|
+
specification_version: 4
|
221
|
+
summary: Delayed::Job plugin to unlock jobs from dead workers
|
222
|
+
test_files:
|
223
|
+
- spec/db/database.yml
|
224
|
+
- spec/db/schema.rb
|
225
|
+
- spec/delayed/heartbeat/delete_worker_results_spec.rb
|
226
|
+
- spec/delayed/heartbeat/worker_heartbeat_spec.rb
|
227
|
+
- spec/delayed/heartbeat_spec.rb
|
228
|
+
- spec/spec_helper.rb
|
229
|
+
- spec/support/destroyed_model.rb
|
230
|
+
- spec/support/test_job.rb
|
231
|
+
- spec/support/test_job_with_callbacks.rb
|
232
|
+
- spec/support/wait.rb
|