fleiss 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.editorconfig +9 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +10 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +82 -0
- data/LICENSE +13 -0
- data/README.md +62 -0
- data/Rakefile +9 -0
- data/bin/fleiss +21 -0
- data/fleiss.gemspec +26 -0
- data/lib/active_job/queue_adapters/fleiss_adapter.rb +17 -0
- data/lib/fleiss/backend/active_record/concern.rb +88 -0
- data/lib/fleiss/backend/active_record/migration.rb +29 -0
- data/lib/fleiss/backend/active_record.rb +12 -0
- data/lib/fleiss/backend.rb +5 -0
- data/lib/fleiss/cli.rb +95 -0
- data/lib/fleiss/worker.rb +107 -0
- data/lib/fleiss.rb +9 -0
- data/spec/fleiss/backend/active_record_spec.rb +109 -0
- data/spec/fleiss/worker_spec.rb +47 -0
- data/spec/fleiss_spec.rb +13 -0
- data/spec/spec_helper.rb +48 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '0786905f08fa028fa92f0d422db3d26acc173b57dbf403b5fe1b20dc2bfb8e73'
|
4
|
+
data.tar.gz: f6f13c6d6c86e456a7779bc06119b2d4e7ed61daa1bbd9438014a0e356daedd0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 67ae6d5b939e7c83cfa69bba4d0ecbacabbed12a0c474289a9162a82928a2aea78aaaa6d96bf66700f10e06c548ac10fbe3008b7562be1133ed09bfdc33b3713
|
7
|
+
data.tar.gz: 609b536863e02bc91eb093b839f64ec518090bd722eaffa163162cccff537d3cd87f5991c0a366abd403a5a3fb701d1a581830d3d820439d0d74d80e30638812
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
fleiss (0.1.0)
|
5
|
+
activejob (>= 5.0)
|
6
|
+
activerecord (>= 5.0)
|
7
|
+
concurrent-ruby
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activejob (5.2.1)
|
13
|
+
activesupport (= 5.2.1)
|
14
|
+
globalid (>= 0.3.6)
|
15
|
+
activemodel (5.2.1)
|
16
|
+
activesupport (= 5.2.1)
|
17
|
+
activerecord (5.2.1)
|
18
|
+
activemodel (= 5.2.1)
|
19
|
+
activesupport (= 5.2.1)
|
20
|
+
arel (>= 9.0)
|
21
|
+
activesupport (5.2.1)
|
22
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
23
|
+
i18n (>= 0.7, < 2)
|
24
|
+
minitest (~> 5.1)
|
25
|
+
tzinfo (~> 1.1)
|
26
|
+
arel (9.0.0)
|
27
|
+
ast (2.4.0)
|
28
|
+
concurrent-ruby (1.1.3)
|
29
|
+
diff-lcs (1.3)
|
30
|
+
globalid (0.4.1)
|
31
|
+
activesupport (>= 4.2.0)
|
32
|
+
i18n (1.1.1)
|
33
|
+
concurrent-ruby (~> 1.0)
|
34
|
+
jaro_winkler (1.5.1)
|
35
|
+
minitest (5.11.3)
|
36
|
+
parallel (1.12.1)
|
37
|
+
parser (2.5.3.0)
|
38
|
+
ast (~> 2.4.0)
|
39
|
+
powerpack (0.1.2)
|
40
|
+
rainbow (3.0.0)
|
41
|
+
rake (12.3.1)
|
42
|
+
rspec (3.8.0)
|
43
|
+
rspec-core (~> 3.8.0)
|
44
|
+
rspec-expectations (~> 3.8.0)
|
45
|
+
rspec-mocks (~> 3.8.0)
|
46
|
+
rspec-core (3.8.0)
|
47
|
+
rspec-support (~> 3.8.0)
|
48
|
+
rspec-expectations (3.8.2)
|
49
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
50
|
+
rspec-support (~> 3.8.0)
|
51
|
+
rspec-mocks (3.8.0)
|
52
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
53
|
+
rspec-support (~> 3.8.0)
|
54
|
+
rspec-support (3.8.0)
|
55
|
+
rubocop (0.60.0)
|
56
|
+
jaro_winkler (~> 1.5.1)
|
57
|
+
parallel (~> 1.10)
|
58
|
+
parser (>= 2.5, != 2.5.1.1)
|
59
|
+
powerpack (~> 0.1)
|
60
|
+
rainbow (>= 2.2.2, < 4.0)
|
61
|
+
ruby-progressbar (~> 1.7)
|
62
|
+
unicode-display_width (~> 1.4.0)
|
63
|
+
ruby-progressbar (1.10.0)
|
64
|
+
sqlite3 (1.3.13)
|
65
|
+
thread_safe (0.3.6)
|
66
|
+
tzinfo (1.2.5)
|
67
|
+
thread_safe (~> 0.1)
|
68
|
+
unicode-display_width (1.4.0)
|
69
|
+
|
70
|
+
PLATFORMS
|
71
|
+
ruby
|
72
|
+
|
73
|
+
DEPENDENCIES
|
74
|
+
bundler
|
75
|
+
fleiss!
|
76
|
+
rake
|
77
|
+
rspec
|
78
|
+
rubocop
|
79
|
+
sqlite3
|
80
|
+
|
81
|
+
BUNDLED WITH
|
82
|
+
1.16.4
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2018 Black Square Media Ltd
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Fleiss
|
2
|
+
|
3
|
+
[](https://travis-ci.org/bsm/fleiss)
|
4
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
5
|
+
|
6
|
+
Minimialist background jobs backed by ActiveJob and ActiveRecord.
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
Define your active jobs, as usual:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
class SimpleJob < ActiveJob::Base
|
14
|
+
queue_as :default
|
15
|
+
|
16
|
+
def perform(*args)
|
17
|
+
# Perform Job
|
18
|
+
end
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
Allow jobs to expire job by specifying an optional TTL:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class ExpringJob < ActiveJob::Base
|
26
|
+
queue_as :default
|
27
|
+
retry_on SomeError, attempts: 1_000_000
|
28
|
+
|
29
|
+
def perform(*args)
|
30
|
+
# Perform Job
|
31
|
+
end
|
32
|
+
|
33
|
+
# This will cause the job to retry up-to 1M times
|
34
|
+
# until the 72h TTL is reached.
|
35
|
+
def ttl
|
36
|
+
72.hours
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
Include the data migration:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# db/migrate/20182412102030_create_fleiss_jobs.rb
|
45
|
+
require 'fleiss/backend/active_record/migration'
|
46
|
+
|
47
|
+
class CreateFleissJobs < ActiveRecord::Migration[5.2]
|
48
|
+
def up
|
49
|
+
Fleiss::Backend::ActiveRecord::Migration.migrate(:up)
|
50
|
+
end
|
51
|
+
|
52
|
+
def down
|
53
|
+
Fleiss::Backend::ActiveRecord::Migration.migrate(:down)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
Run the worker:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
bundle exec fleiss -I . -r config/environment
|
62
|
+
```
|
data/Rakefile
ADDED
data/bin/fleiss
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib_dir = File.expand_path('../lib', __dir__)
|
4
|
+
$LOAD_PATH.push(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
5
|
+
|
6
|
+
require 'fleiss/cli'
|
7
|
+
|
8
|
+
cli = Fleiss::CLI.instance
|
9
|
+
begin
|
10
|
+
cli.parse!
|
11
|
+
cli.run!
|
12
|
+
rescue ArgumentError => e
|
13
|
+
STDERR.puts " ! #{e.message}\n"
|
14
|
+
STDERR.puts
|
15
|
+
STDERR.puts cli.parser
|
16
|
+
exit 1
|
17
|
+
rescue StandardError => e
|
18
|
+
STDERR.puts e.message
|
19
|
+
STDERR.puts e.backtrace.join("\n")
|
20
|
+
exit 1
|
21
|
+
end
|
data/fleiss.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'fleiss'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.authors = ['Black Square Media Ltd']
|
5
|
+
s.email = ['info@blacksquaremedia.com']
|
6
|
+
s.summary = %(Minimialist background jobs backed by ActiveJob and ActiveRecord.)
|
7
|
+
s.description = %(Run background jobs with your favourite stack.)
|
8
|
+
s.homepage = 'https://github.com/bsm/fleiss'
|
9
|
+
s.license = 'Apache-2.0'
|
10
|
+
|
11
|
+
s.executables = ['fleiss']
|
12
|
+
s.files = `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^spec/}) }
|
13
|
+
s.test_files = `git ls-files -z -- spec/*`.split("\x0")
|
14
|
+
s.require_paths = ['lib']
|
15
|
+
s.required_ruby_version = '>= 2.2'
|
16
|
+
|
17
|
+
s.add_dependency 'activejob', '>= 5.0'
|
18
|
+
s.add_dependency 'activerecord', '>= 5.0'
|
19
|
+
s.add_dependency 'concurrent-ruby'
|
20
|
+
|
21
|
+
s.add_development_dependency 'bundler'
|
22
|
+
s.add_development_dependency 'rake'
|
23
|
+
s.add_development_dependency 'rspec'
|
24
|
+
s.add_development_dependency 'rubocop'
|
25
|
+
s.add_development_dependency 'sqlite3'
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'fleiss'
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module QueueAdapters
|
5
|
+
class FleissAdapter
|
6
|
+
def enqueue(job) #:nodoc:
|
7
|
+
enqueue_at(job, nil)
|
8
|
+
end
|
9
|
+
|
10
|
+
def enqueue_at(job, scheduled_at) #:nodoc:
|
11
|
+
job_id = Fleiss.backend.enqueue(job, scheduled_at: scheduled_at)
|
12
|
+
job.provider_job_id = job_id
|
13
|
+
job_id
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Fleiss
|
2
|
+
module Backend
|
3
|
+
class ActiveRecord
|
4
|
+
module Concern
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
scope :in_queue, ->(qs) { where(queue_name: Array.wrap(qs)) }
|
9
|
+
scope :not_finished, -> { where(finished_at: nil) }
|
10
|
+
scope :not_expired, ->(now=Time.zone.now) { where(arel_table[:expires_at].eq(nil).or(arel_table[:expires_at].gt(now))) }
|
11
|
+
scope :started, -> { where(arel_table[:started_at].not_eq(nil)) }
|
12
|
+
scope :not_started, -> { where(arel_table[:started_at].eq(nil)) }
|
13
|
+
scope :scheduled, ->(now=Time.zone.now) { where(arel_table[:scheduled_at].gt(now)) }
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# @return [ActiveRecord::Relation] pending scope
|
18
|
+
def pending(now=Time.zone.now)
|
19
|
+
not_finished
|
20
|
+
.not_expired(now)
|
21
|
+
.not_started
|
22
|
+
.where(arel_table[:scheduled_at].lteq(now))
|
23
|
+
.order(priority: :desc)
|
24
|
+
.order(scheduled_at: :asc)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [ActiveRecord::Relation] in-progress scope
|
28
|
+
def in_progress(owner)
|
29
|
+
started.not_finished.where(owner: owner)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Reschedules jobs to run again.
|
33
|
+
def reschedule_all(at=Time.zone.now)
|
34
|
+
update_all(
|
35
|
+
started_at: nil,
|
36
|
+
owner: nil,
|
37
|
+
scheduled_at: at,
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param [ActiveJob::Base] job the job instance
|
42
|
+
# @option [Time] :scheduled_at schedule job at
|
43
|
+
def enqueue(job, scheduled_at: nil)
|
44
|
+
scheduled_at = scheduled_at ? Time.zone.at(scheduled_at) : Time.zone.now
|
45
|
+
expires_at = scheduled_at + job.ttl.seconds if job.respond_to?(:ttl)
|
46
|
+
|
47
|
+
create!(
|
48
|
+
payload: JSON.dump(job.serialize),
|
49
|
+
queue_name: job.queue_name,
|
50
|
+
priority: job.priority.to_i,
|
51
|
+
scheduled_at: scheduled_at,
|
52
|
+
expires_at: expires_at,
|
53
|
+
).id
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Hash] serialized job data
|
58
|
+
def job_data
|
59
|
+
@job_data ||= JSON.parse(payload)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [String] the ActiveJob ID
|
63
|
+
def job_id
|
64
|
+
job_data['job_id']
|
65
|
+
end
|
66
|
+
|
67
|
+
# Acquires a lock and starts the job.
|
68
|
+
# @param [String] owner
|
69
|
+
# @return [Boolean] true if job was started.
|
70
|
+
def start(owner, now: Time.zone.now)
|
71
|
+
self.class.pending(now)
|
72
|
+
.where(id: id)
|
73
|
+
.update_all(started_at: now, owner: owner) == 1
|
74
|
+
end
|
75
|
+
|
76
|
+
# Marks a job as finished.
|
77
|
+
# @param [String] owner
|
78
|
+
# @return [Boolean] true if successful.
|
79
|
+
def finish(owner, now: Time.zone.now)
|
80
|
+
self.class
|
81
|
+
.in_progress(owner)
|
82
|
+
.where(id: id)
|
83
|
+
.update_all(finished_at: now) == 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'fleiss/backend/active_record'
|
2
|
+
|
3
|
+
module Fleiss
|
4
|
+
module Backend
|
5
|
+
class ActiveRecord
|
6
|
+
class Migration < ::ActiveRecord::Migration[5.0]
|
7
|
+
def change
|
8
|
+
create_table :fleiss_jobs do |t|
|
9
|
+
t.string :queue_name, limit: 50, null: false
|
10
|
+
t.integer :priority, limit: 2, null: false, default: 10
|
11
|
+
t.text :payload, null: false
|
12
|
+
t.timestamp :scheduled_at, null: false
|
13
|
+
t.timestamp :started_at
|
14
|
+
t.timestamp :finished_at
|
15
|
+
t.timestamp :expires_at
|
16
|
+
t.string :owner, limit: 100
|
17
|
+
|
18
|
+
t.index :queue_name
|
19
|
+
t.index :priority
|
20
|
+
t.index :scheduled_at
|
21
|
+
t.index :finished_at
|
22
|
+
t.index :expires_at
|
23
|
+
t.index :owner
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/fleiss/cli.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'optparse'
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
module Fleiss
|
8
|
+
class CLI
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
DEFAULT_OPTIONS = {
|
12
|
+
config: nil,
|
13
|
+
queues: ['default'],
|
14
|
+
include: [],
|
15
|
+
require: [],
|
16
|
+
concurrency: 10,
|
17
|
+
wait_time: 1.0,
|
18
|
+
logfile: STDOUT,
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
attr_reader :opts
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@opts = DEFAULT_OPTIONS.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse!(argv=ARGV)
|
28
|
+
parser.parse!(argv)
|
29
|
+
|
30
|
+
# Check config file
|
31
|
+
raise ArgumentError, "Unable to find config file in #{opts[:config]}" if opts[:config] && !File.exist?(opts[:config])
|
32
|
+
|
33
|
+
return unless opts[:config]
|
34
|
+
|
35
|
+
# Load config file
|
36
|
+
conf = YAML.safe_load(ERB.new(IO.read(opts[:config])).result)
|
37
|
+
raise ArgumentError, "File in #{opts[:config]} does not contain a valid configuration" unless conf.is_a?(Hash)
|
38
|
+
|
39
|
+
conf.each do |key, value|
|
40
|
+
opts[key.to_sym] = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def run!
|
45
|
+
return if @worker
|
46
|
+
|
47
|
+
$LOAD_PATH.concat opts[:include]
|
48
|
+
opts[:require].each {|n| require n }
|
49
|
+
require 'fleiss/worker'
|
50
|
+
|
51
|
+
Signal.trap('TERM') { shutdown }
|
52
|
+
Signal.trap('INT') { shutdown }
|
53
|
+
|
54
|
+
@worker = Fleiss::Worker.new \
|
55
|
+
queues: opts[:queues],
|
56
|
+
concurrency: opts[:concurrency],
|
57
|
+
wait_time: opts[:wait_time],
|
58
|
+
logger: Logger.new(opts[:logfile])
|
59
|
+
@worker.run
|
60
|
+
@worker.wait
|
61
|
+
end
|
62
|
+
|
63
|
+
def shutdown
|
64
|
+
@worker.shutdown if @worker
|
65
|
+
end
|
66
|
+
|
67
|
+
def parser # rubocop:disable Metrics/MethodLength
|
68
|
+
@parser ||= begin
|
69
|
+
op = OptionParser.new do |o|
|
70
|
+
o.on '-C', '--config FILE', 'YAML config file to load' do |v|
|
71
|
+
@opts[:config] = v
|
72
|
+
end
|
73
|
+
|
74
|
+
o.on '-I [DIR]', 'Specify an additional $LOAD_PATH directory' do |v|
|
75
|
+
@opts[:include].push(v)
|
76
|
+
end
|
77
|
+
|
78
|
+
o.on '-r', '--require [PATH|DIR]', 'File to require' do |v|
|
79
|
+
@opts[:require].push(v)
|
80
|
+
end
|
81
|
+
|
82
|
+
o.on '-L', '--logfile PATH', 'path to writable logfile' do |v|
|
83
|
+
@opts[:logfile] = v
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
op.banner = 'fleiss [options]'
|
88
|
+
op.on_tail '-h', '--help', 'Show help' do
|
89
|
+
$stdout.puts parser
|
90
|
+
exit 1
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'fleiss'
|
2
|
+
require 'concurrent/executor/fixed_thread_pool'
|
3
|
+
require 'concurrent/atomic/atomic_fixnum'
|
4
|
+
require 'logger'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
class Fleiss::Worker
|
8
|
+
attr_reader :queues, :uuid, :wait_time, :logger
|
9
|
+
|
10
|
+
# Init a new worker instance
|
11
|
+
# @param [ConnectionPool] disque client connection pool
|
12
|
+
# @param [Hash] options
|
13
|
+
# @option [Array<String>] :queues queues to watch. Default: ["default"]
|
14
|
+
# @option [Integer] :concurrency the number of concurrent pool. Default: 10
|
15
|
+
# @option [Numeric] :wait_time maximum time (in seconds) to wait for jobs when retrieving next batch. Default: 1s.
|
16
|
+
# @option [Logger] :logger optional logger.
|
17
|
+
def initialize(queues: [Fleiss::DEFAULT_QUEUE], concurrency: 10, wait_time: 1, logger: nil)
|
18
|
+
@uuid = SecureRandom.uuid
|
19
|
+
@queues = Array(queues)
|
20
|
+
@pool = Concurrent::FixedThreadPool.new(concurrency, fallback_policy: :discard)
|
21
|
+
@wait_time = wait_time
|
22
|
+
@logger = logger || Logger.new(nil)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Run starts the worker
|
26
|
+
def run
|
27
|
+
logger.info "Worker #{uuid} starting - queues: #{queues.inspect}, concurrency: #{@pool.max_length}"
|
28
|
+
loop do
|
29
|
+
run_cycle
|
30
|
+
break if @stopped
|
31
|
+
|
32
|
+
sleep @wait_time
|
33
|
+
end
|
34
|
+
logger.info "Worker #{uuid} shutting down"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Blocks until worker until it's stopped.
|
38
|
+
def wait
|
39
|
+
@pool.shutdown
|
40
|
+
@pool.wait_for_termination(1)
|
41
|
+
|
42
|
+
Fleiss.backend
|
43
|
+
.in_queue(queues)
|
44
|
+
.in_progress(uuid)
|
45
|
+
.reschedule_all(10.seconds.from_now)
|
46
|
+
@pool.wait_for_termination
|
47
|
+
logger.info "Worker #{uuid} shutdown complete"
|
48
|
+
rescue StandardError => e
|
49
|
+
handle_exception e, 'shutdown'
|
50
|
+
end
|
51
|
+
|
52
|
+
# Initiates the shutdown process
|
53
|
+
def shutdown
|
54
|
+
@stopped = true
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def run_cycle
|
60
|
+
jobs = next_batch
|
61
|
+
while !@stopped && capacity.positive? && !jobs.empty?
|
62
|
+
job = jobs.shift
|
63
|
+
@pool.post { perform(job) }
|
64
|
+
end
|
65
|
+
rescue StandardError => e
|
66
|
+
handle_exception e, 'running cycle'
|
67
|
+
end
|
68
|
+
|
69
|
+
def capacity
|
70
|
+
num = @pool.max_length - @pool.scheduled_task_count + @pool.completed_task_count
|
71
|
+
num.positive? ? num : 0
|
72
|
+
end
|
73
|
+
|
74
|
+
def next_batch
|
75
|
+
slots = capacity
|
76
|
+
return [] if slots.zero?
|
77
|
+
|
78
|
+
Fleiss.backend
|
79
|
+
.in_queue(queues)
|
80
|
+
.pending
|
81
|
+
.limit(slots)
|
82
|
+
.to_a
|
83
|
+
end
|
84
|
+
|
85
|
+
def perform(job)
|
86
|
+
return unless job.start(uuid)
|
87
|
+
|
88
|
+
thread_id = Thread.current.object_id.to_s(36)
|
89
|
+
logger.info { "Worker #{uuid} execute job ##{job.id} by thread #{thread_id}" }
|
90
|
+
|
91
|
+
ActiveJob::Base.execute job.job_data
|
92
|
+
rescue StandardError => e
|
93
|
+
handle_exception e, "processing job ##{job.id} (by thread #{thread_id})"
|
94
|
+
ensure
|
95
|
+
job.finish(uuid)
|
96
|
+
end
|
97
|
+
|
98
|
+
def handle_exception(err, intro)
|
99
|
+
lines = [
|
100
|
+
"Worker #{uuid} error on #{intro}:",
|
101
|
+
"#{err.class.name}: #{err.message}",
|
102
|
+
err.backtrace,
|
103
|
+
].compact.flatten
|
104
|
+
|
105
|
+
logger.error lines.join("\n")
|
106
|
+
end
|
107
|
+
end
|
data/lib/fleiss.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Fleiss::Backend::ActiveRecord do
|
4
|
+
def retrieve(job)
|
5
|
+
described_class.find(job.provider_job_id)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should persist jobs' do
|
9
|
+
job = TestJob.perform_later
|
10
|
+
rec = retrieve(job)
|
11
|
+
|
12
|
+
expect(rec.attributes).to include(
|
13
|
+
'queue_name' => 'test-queue',
|
14
|
+
'owner' => nil,
|
15
|
+
'started_at' => nil,
|
16
|
+
'finished_at' => nil,
|
17
|
+
)
|
18
|
+
expect(rec.scheduled_at).to be_within(2.seconds).of(Time.zone.now)
|
19
|
+
expect(rec.expires_at).to be_within(2.seconds).of(3.days.from_now)
|
20
|
+
expect(rec.job_data).to include(
|
21
|
+
'job_class' => 'TestJob',
|
22
|
+
'arguments' => [],
|
23
|
+
'executions' => 0,
|
24
|
+
'locale' => 'en',
|
25
|
+
'priority' => nil,
|
26
|
+
'provider_job_id' => nil,
|
27
|
+
'queue_name' => 'test-queue',
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should enqueue with delay' do
|
32
|
+
job = TestJob.set(wait: 1.day).perform_later
|
33
|
+
rec = retrieve(job)
|
34
|
+
expect(rec.scheduled_at).to be_within(2.seconds).of(1.day.from_now)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should enqueue with priority' do
|
38
|
+
job = TestJob.set(priority: 8).perform_later
|
39
|
+
rec = retrieve(job)
|
40
|
+
expect(rec.priority).to eq(8)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should expose active job ID' do
|
44
|
+
job = TestJob.perform_later
|
45
|
+
rec = retrieve(job)
|
46
|
+
expect(rec.job_id.size).to eq(36)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should reschedule' do
|
50
|
+
job = TestJob.perform_later
|
51
|
+
described_class.reschedule_all(1.hour.from_now)
|
52
|
+
rec = retrieve(job)
|
53
|
+
expect(rec.scheduled_at).to be_within(2.seconds).of(1.hour.from_now)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should scope pending' do
|
57
|
+
j1 = TestJob.perform_later
|
58
|
+
expect(retrieve(j1).start('owner')).to be_truthy
|
59
|
+
expect(retrieve(j1).finish('owner')).to be_truthy
|
60
|
+
|
61
|
+
j2 = TestJob.perform_later
|
62
|
+
_j3 = TestJob.set(wait: 1.hour).perform_later
|
63
|
+
j4 = TestJob.set(priority: 2).perform_later
|
64
|
+
|
65
|
+
expect(described_class.pending.ids).to eq [j4.provider_job_id, j2.provider_job_id]
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should scope in_progress' do
|
69
|
+
_j1 = TestJob.perform_later
|
70
|
+
j2 = TestJob.perform_later
|
71
|
+
expect(retrieve(j2).start('owner')).to be_truthy
|
72
|
+
|
73
|
+
j3 = TestJob.perform_later
|
74
|
+
expect(retrieve(j3).start('owner')).to be_truthy
|
75
|
+
expect(described_class.in_progress('owner').ids).to match_array [j2.provider_job_id, j3.provider_job_id]
|
76
|
+
expect(described_class.in_progress('other').ids).to be_empty
|
77
|
+
|
78
|
+
expect(retrieve(j3).finish('owner')).to be_truthy
|
79
|
+
expect(described_class.in_progress('owner').ids).to eq [j2.provider_job_id]
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should scope by queue' do
|
83
|
+
j1 = TestJob.perform_later
|
84
|
+
j2 = TestJob.set(queue: 'other').perform_later
|
85
|
+
expect(described_class.in_queue('test-queue').ids).to eq [j1.provider_job_id]
|
86
|
+
expect(described_class.in_queue('other').ids).to eq [j2.provider_job_id]
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should start' do
|
90
|
+
job = TestJob.perform_later
|
91
|
+
rec = retrieve(job)
|
92
|
+
expect(rec.start('owner')).to be_truthy
|
93
|
+
expect(rec.start('other')).to be_falsey
|
94
|
+
expect(rec.reload.owner).to eq('owner')
|
95
|
+
expect(rec.started_at).to be_within(2.seconds).of(Time.zone.now)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should finish' do
|
99
|
+
job = TestJob.perform_later
|
100
|
+
rec = retrieve(job)
|
101
|
+
expect(rec.finish('owner')).to be_falsey
|
102
|
+
expect(rec.start('owner')).to be_truthy
|
103
|
+
expect(rec.finish('other')).to be_falsey
|
104
|
+
expect(rec.finish('owner')).to be_truthy
|
105
|
+
expect(rec.reload.owner).to eq('owner')
|
106
|
+
expect(rec.started_at).to be_within(2.seconds).of(Time.zone.now)
|
107
|
+
expect(rec.finished_at).to be_within(2.seconds).of(Time.zone.now)
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fleiss/worker'
|
3
|
+
|
4
|
+
RSpec.describe Fleiss::Worker do
|
5
|
+
subject do
|
6
|
+
described_class.new queues: TestJob.queue_name, wait_time: 0.01
|
7
|
+
end
|
8
|
+
|
9
|
+
let! :runner do
|
10
|
+
t = Thread.new { subject.run }
|
11
|
+
t.abort_on_exception = true
|
12
|
+
t
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
subject.shutdown
|
17
|
+
subject.wait
|
18
|
+
end
|
19
|
+
|
20
|
+
def wait_for
|
21
|
+
200.times do
|
22
|
+
break if yield
|
23
|
+
|
24
|
+
sleep(0.1)
|
25
|
+
end
|
26
|
+
expect(yield).to be_truthy
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should run/process/shutdown' do
|
30
|
+
# seed 50 jobs
|
31
|
+
50.times {|n| TestJob.perform_later(n) }
|
32
|
+
wait_for { Fleiss.backend.not_finished.count > 0 }
|
33
|
+
|
34
|
+
# ensure runner processes them all
|
35
|
+
wait_for { Fleiss.backend.not_finished.count.zero? }
|
36
|
+
|
37
|
+
# check what's been performed
|
38
|
+
expect(TestJob.performed.size).to eq(50)
|
39
|
+
expect(TestJob.performed).to match_array(0..49)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should handle failing jobs' do
|
43
|
+
TestJob.perform_later('raise')
|
44
|
+
wait_for { Fleiss.backend.not_finished.count.zero? }
|
45
|
+
expect(Fleiss.backend.count).to eq(1)
|
46
|
+
end
|
47
|
+
end
|
data/spec/fleiss_spec.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Fleiss do
|
4
|
+
it 'should have a backend' do
|
5
|
+
expect(described_class.backend).to eq(Fleiss::Backend::ActiveRecord)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should enqueue' do
|
9
|
+
expect do
|
10
|
+
TestJob.set(wait: 1.week).perform_later
|
11
|
+
end.to change { described_class.backend.count }.by(1)
|
12
|
+
end
|
13
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
ENV['RACK_ENV'] ||= 'test'
|
2
|
+
|
3
|
+
require 'rspec'
|
4
|
+
require 'fleiss'
|
5
|
+
require 'fleiss/backend/active_record/migration'
|
6
|
+
require 'active_job'
|
7
|
+
require 'active_job/queue_adapters/fleiss_adapter'
|
8
|
+
require 'tempfile'
|
9
|
+
|
10
|
+
ActiveJob::Base.queue_adapter = :fleiss
|
11
|
+
ActiveJob::Base.logger = Logger.new(nil)
|
12
|
+
|
13
|
+
Time.zone_default = Time.find_zone!('UTC')
|
14
|
+
|
15
|
+
ActiveRecord::Base.configurations['test'] = {
|
16
|
+
'adapter' => 'sqlite3',
|
17
|
+
'database' => Tempfile.new(['fleiss-test', '.sqlite3']).path,
|
18
|
+
'pool' => 20,
|
19
|
+
}
|
20
|
+
ActiveRecord::Base.establish_connection :test
|
21
|
+
ActiveRecord::Migration.suppress_messages do
|
22
|
+
Fleiss::Backend::ActiveRecord::Migration.migrate(:up)
|
23
|
+
end
|
24
|
+
|
25
|
+
class TestJob < ActiveJob::Base
|
26
|
+
queue_as 'test-queue'
|
27
|
+
|
28
|
+
def self.performed
|
29
|
+
@performed ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
def ttl
|
33
|
+
72.hours
|
34
|
+
end
|
35
|
+
|
36
|
+
def perform(msg=nil)
|
37
|
+
raise 'Failing' if msg == 'raise'
|
38
|
+
|
39
|
+
self.class.performed.push(msg)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
RSpec.configure do |c|
|
44
|
+
c.after :each do
|
45
|
+
TestJob.performed.clear
|
46
|
+
Fleiss.backend.delete_all
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fleiss
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Black Square Media Ltd
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-11-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: concurrent-ruby
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Run background jobs with your favourite stack.
|
126
|
+
email:
|
127
|
+
- info@blacksquaremedia.com
|
128
|
+
executables:
|
129
|
+
- fleiss
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- ".editorconfig"
|
134
|
+
- ".gitignore"
|
135
|
+
- ".rubocop.yml"
|
136
|
+
- Gemfile
|
137
|
+
- Gemfile.lock
|
138
|
+
- LICENSE
|
139
|
+
- README.md
|
140
|
+
- Rakefile
|
141
|
+
- bin/fleiss
|
142
|
+
- fleiss.gemspec
|
143
|
+
- lib/active_job/queue_adapters/fleiss_adapter.rb
|
144
|
+
- lib/fleiss.rb
|
145
|
+
- lib/fleiss/backend.rb
|
146
|
+
- lib/fleiss/backend/active_record.rb
|
147
|
+
- lib/fleiss/backend/active_record/concern.rb
|
148
|
+
- lib/fleiss/backend/active_record/migration.rb
|
149
|
+
- lib/fleiss/cli.rb
|
150
|
+
- lib/fleiss/worker.rb
|
151
|
+
- spec/fleiss/backend/active_record_spec.rb
|
152
|
+
- spec/fleiss/worker_spec.rb
|
153
|
+
- spec/fleiss_spec.rb
|
154
|
+
- spec/spec_helper.rb
|
155
|
+
homepage: https://github.com/bsm/fleiss
|
156
|
+
licenses:
|
157
|
+
- Apache-2.0
|
158
|
+
metadata: {}
|
159
|
+
post_install_message:
|
160
|
+
rdoc_options: []
|
161
|
+
require_paths:
|
162
|
+
- lib
|
163
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '2.2'
|
168
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
requirements: []
|
174
|
+
rubyforge_project:
|
175
|
+
rubygems_version: 2.7.7
|
176
|
+
signing_key:
|
177
|
+
specification_version: 4
|
178
|
+
summary: Minimialist background jobs backed by ActiveJob and ActiveRecord.
|
179
|
+
test_files:
|
180
|
+
- spec/fleiss/backend/active_record_spec.rb
|
181
|
+
- spec/fleiss/worker_spec.rb
|
182
|
+
- spec/fleiss_spec.rb
|
183
|
+
- spec/spec_helper.rb
|