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 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