fleiss 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .rubocop-*
2
+ pkg/
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ inherit_from:
2
+ - https://storage.googleapis.com/bsm-misc/rubocop.yml
3
+
4
+ Security/Open:
5
+ Exclude:
6
+ - "core/lib/bfs/bucket/abstract.rb"
7
+ - "core/lib/bfs/blob.rb"
8
+ Metrics/BlockLength:
9
+ Exclude:
10
+ - "**/spec/**/*"
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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
+ [![Build Status](https://travis-ci.org/bsm/fleiss.png?branch=master)](https://travis-ci.org/bsm/fleiss)
4
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'rubocop/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ RuboCop::RakeTask.new(:rubocop)
8
+
9
+ task default: %i[spec rubocop]
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
@@ -0,0 +1,12 @@
1
+ require 'active_record'
2
+
3
+ module Fleiss
4
+ module Backend
5
+ class ActiveRecord < ::ActiveRecord::Base
6
+ self.table_name = 'fleiss_jobs'
7
+
8
+ require 'fleiss/backend/active_record/concern'
9
+ include self::Concern
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module Fleiss
2
+ module Backend
3
+ end
4
+ end
5
+ require 'fleiss/backend/active_record'
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,9 @@
1
+ module Fleiss
2
+ DEFAULT_QUEUE = 'default'.freeze
3
+
4
+ def self.backend
5
+ self::Backend::ActiveRecord
6
+ end
7
+ end
8
+
9
+ require 'fleiss/backend'
@@ -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
@@ -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
@@ -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