disqualified 0.1.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec05ac0a0d2eadb072a6cfddd164ae66d3dc14642234663917ebb02c824cecec
4
- data.tar.gz: 05f192048112ba00432dc7a1e9844e106f0515e7a7f835e65b2a646f9909b8b6
3
+ metadata.gz: 34ac6d57608468421c25cb83e842ce8cadc028b922e4437e39580ba8f8009106
4
+ data.tar.gz: 8bf4b146c66caaeae5a322c013b9be859436e17940f6747643289f76edb84abc
5
5
  SHA512:
6
- metadata.gz: 23ac21c0d08bd70540746321ba5f6cf427d75027acf49f2f4a4295ad6a52fbcaa821f35b40369d99108e4d6dd7d0ad5c04100cb4d7b6a9a10419cd8129fb8c2f
7
- data.tar.gz: eb6495ae699c5a1a9414f54924af2a90b583b5ab249e6437f067696416f2807a3871d164b8ac8ee2c4139ae7342e644658b64fa207f4a8d6c7c664c37a42842e
6
+ metadata.gz: 2c5c1472c10779a1f55aa88b946801e3ef7ba725538bda4c415572ca8f103c6df81e709399455cc901b419dec15534714fde445d09503f495d9660c7c4bf7f16
7
+ data.tar.gz: 6eda9531c46d5002efd09c6784551cd2e1cefc7b912143010a92f97ce87db1d478bbcc05c507d22dcb1272f63d97afca57f9d4a758cd67185e86f4588fb7e876
data/README.md CHANGED
@@ -6,12 +6,48 @@ Since SQLite doesn't have any features like Postgres' `LISTEN`/`NOTIFY`,
6
6
  Disqualified resorts to polling the database. This might _disqualify_ it as an
7
7
  option for you, but it works well enough for my workload.
8
8
 
9
- Disqualified only works with Rails. It does not work with ActiveJob.
9
+ Note that:
10
+
11
+ * Disqualified only works with Rails.
12
+ * Disqualified does not support multiple queues.
13
+ * Each Disqualified process assumes it's the only process running. Running
14
+ multiple instances of Disqualified should not hurt, but it is not supported.
10
15
 
11
16
 
12
17
  ## Usage
13
18
 
14
- Run `bundle exec disqualified --help`
19
+ Run `bundle exec disqualified --help` for more information on how to run the
20
+ Disqualified server. This is what I use in production:
21
+
22
+ ```
23
+ env RAILS_ENV=production bundle exec disqualified
24
+ ```
25
+
26
+ You can use Disqualified with ActiveJob, or you can use it by itself.
27
+ The examples below detail how to use it by by itself. See Installation
28
+ instructions for information on how to set up integration with ActiveJob.
29
+
30
+
31
+ ### Defining a job
32
+
33
+ ```ruby
34
+ class ComplicatedJob
35
+ include Disqualified::Job
36
+
37
+ def perform(arg1, arg2)
38
+ # ...
39
+ end
40
+ end
41
+ ```
42
+
43
+
44
+ ### Queuing
45
+
46
+ ```ruby
47
+ ComplicatedJob.perform_async(1, 2)
48
+ ComplicatedJob.perform_in(1.minute, 1, 2)
49
+ ComplicatedJob.perform_at(3.days.from_now, 1, 2)
50
+ ```
15
51
 
16
52
 
17
53
  ## Installation
@@ -25,6 +61,24 @@ bundle binstub disqualified
25
61
  ```
26
62
 
27
63
 
64
+ ### ActiveJob
65
+
66
+ You can optionally set up Disqualified as ActiveJob's default backend.
67
+
68
+ Usually, you'll just need to update your `config/environments/production.rb`
69
+ file to include something like this.
70
+
71
+ ```ruby
72
+ require "disqualified/active_job"
73
+
74
+ Rails.application.configure do
75
+ # ...
76
+ config.active_job.queue_adapter = :disqualified
77
+ # ...
78
+ end
79
+ ```
80
+
81
+
28
82
  ## Contributing
29
83
 
30
84
  PRs are welcome! Please confirm the change with me before you start working;
@@ -0,0 +1,25 @@
1
+ require "active_job"
2
+ require "disqualified"
3
+
4
+ class Disqualified::ActiveJobAdapter
5
+ include Disqualified::Job
6
+
7
+ def perform(serialized_job_data)
8
+ ::ActiveJob::Base.execute(serialized_job_data)
9
+ end
10
+ end
11
+
12
+ module ActiveJob
13
+ module QueueAdapters
14
+ class DisqualifiedAdapter
15
+ def enqueue(job_data)
16
+ Disqualified::ActiveJobAdapter.perform_async(job_data.serialize)
17
+ end
18
+
19
+ def enqueue_at(job_data, timestamp)
20
+ timestamp = Time.at(timestamp)
21
+ Disqualified::ActiveJobAdapter.perform_at(timestamp, job_data.serialize)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -29,14 +29,18 @@ class Disqualified::CLI
29
29
  logger.info { ' / /_/ / (__ ) /_/ / /_/ / /_/ / / / __/ / __/ /_/ /' }
30
30
  logger.info { '/_____/_/____/\__, /\__,_/\__,_/_/_/_/ /_/\___/\__,_/' }
31
31
  logger.info { ' /_/' + "v#{Disqualified::VERSION}".rjust(32, " ") }
32
- logger.info { "#{Disqualified.server_options.to_s}" }
32
+ logger.info { Disqualified.server_options.to_s }
33
33
 
34
- pool = Disqualified::Pool.new(delay_range:, pool_size:, logger:) do |args|
35
- args => { promise_index:, running: }
36
- logger.debug { format_log("[Disqualified::CLI#run <block>] ##{promise_index}") }
34
+ pool = Disqualified::Pool.new(delay_range:, pool_size:, error_hooks:, logger:) do |args|
35
+ args => {promise_index:}
36
+ logger.debug { format_log("Disqualified::CLI#run <block>", "##{promise_index}") }
37
37
  Disqualified::Main.new(error_hooks:, logger:).call
38
38
  end
39
39
  pool.run!
40
+ rescue Interrupt
41
+ pool.shutdown
42
+ puts
43
+ puts "Gracefully quitting..."
40
44
  end
41
45
 
42
46
  private
@@ -6,7 +6,7 @@ module Disqualified::Job
6
6
  module ClassMethods
7
7
  def perform_at(the_time, *args)
8
8
  Disqualified::Record.create(
9
- handler: self.name,
9
+ handler: name,
10
10
  arguments: JSON.dump(args),
11
11
  queue: "default",
12
12
  run_at: the_time
@@ -1,7 +1,21 @@
1
1
  module Disqualified::Logging
2
2
  module_function
3
3
 
4
- def format_log(message)
5
- "[#{Time.now.iso8601(3)}] #{message}"
4
+ def format_log(*parts)
5
+ *extras, message = parts
6
+
7
+ if extras.empty?
8
+ message
9
+ else
10
+ "#{extras.map { |x| "[#{x}]" }.join(" ")} #{message}"
11
+ end
12
+ end
13
+
14
+ def handle_error(error_hooks, error, context)
15
+ error_hooks.each do |hook|
16
+ hook.call(error, context)
17
+ rescue
18
+ nil
19
+ end
6
20
  end
7
21
  end
@@ -9,7 +9,7 @@ class Disqualified::Main
9
9
  def call
10
10
  run_id = SecureRandom.uuid
11
11
 
12
- Rails.application.executor.wrap do
12
+ Rails.application.reloader.wrap do
13
13
  # Claim a job
14
14
  claimed_count =
15
15
  Disqualified::Record
@@ -18,7 +18,7 @@ class Disqualified::Main
18
18
  .limit(1)
19
19
  .update_all(locked_by: run_id, locked_at: Time.now, updated_at: Time.now, attempts: Arel.sql("attempts + 1"))
20
20
 
21
- @logger.debug { format_log("[Disqualified::Main#run] [Runner #{run_id}] Claimed #{claimed_count}") }
21
+ @logger.debug { format_log("Disqualified::Main#call", "Runner #{run_id}", "Claimed #{claimed_count}") }
22
22
 
23
23
  next if claimed_count == 0
24
24
 
@@ -30,12 +30,8 @@ class Disqualified::Main
30
30
  handler_class = job.handler.constantize
31
31
  arguments = JSON.parse(job.arguments)
32
32
 
33
- begin
34
- @logger.info do
35
- format_log("[#{run_id}] Running `#{job.handler}' with #{arguments.size} argument(s)")
36
- end
37
- rescue
38
- nil
33
+ @logger.info do
34
+ format_log("Disqualified::Main#call", "Runner #{run_id}" "Running `#{job.handler}'")
39
35
  end
40
36
 
41
37
  # Run the job
@@ -44,20 +40,12 @@ class Disqualified::Main
44
40
 
45
41
  finish(job)
46
42
 
47
- begin
48
- @logger.info do
49
- format_log("[#{run_id}] Done")
50
- end
51
- rescue
52
- nil
43
+ @logger.info do
44
+ format_log("Disqualified::Main#call", "Runner #{run_id}", "Done")
53
45
  end
54
46
  rescue => e
55
- @error_hooks.each do |hook|
56
- hook.call(e)
57
- rescue
58
- nil
59
- end
60
- @logger.error { format_log("[Disqualified::Main#run] [Runner #{run_id}] Rescued Record ##{job&.id}") }
47
+ handle_error(@error_hooks, e, {record: job.attributes})
48
+ @logger.error { format_log("Disqualified::Main#run", "Runner #{run_id}", "Rescued Record ##{job&.id}") }
61
49
  requeue(job)
62
50
  end
63
51
  end
@@ -73,7 +61,7 @@ class Disqualified::Main
73
61
  # Formula from the Sidekiq wiki
74
62
  retry_count = job.attempts - 1
75
63
  sleep = (retry_count**4) + 15 + (rand(10) * (retry_count + 1))
76
- @logger.error { format_log("[Disqualified::Main#requeue] Sleeping job for ##{sleep} seconds") }
64
+ @logger.debug { format_log("Disqualified::Main#requeue", "Sleeping job for #{sleep} seconds") }
77
65
  job.update!(locked_by: nil, locked_at: nil, run_at: Time.now + sleep)
78
66
  end
79
67
  end
@@ -1,58 +1,91 @@
1
1
  class Disqualified::Pool
2
2
  include Disqualified::Logging
3
3
 
4
- def initialize(delay_range:, logger:, pool_size:, &task)
4
+ CHECK = :check
5
+ QUIT = :quit
6
+ RUN = :run
7
+
8
+ def initialize(delay_range:, logger:, pool_size:, error_hooks:, &task)
5
9
  @delay_range = delay_range
6
10
  @logger = logger
7
11
  @pool_size = pool_size
12
+ @error_hooks = error_hooks
8
13
  @task = task
9
14
  @running = Concurrent::AtomicBoolean.new(true)
15
+ @command_queue = Thread::Queue.new
10
16
  end
11
17
 
12
18
  def run!
19
+ clock.execute
13
20
  Concurrent::Promises
14
21
  .zip(*pool)
15
- .rescue { |error| handle_error(error) }
22
+ .rescue { |error| handle_error(@error_hooks, error, {}) }
16
23
  .run
17
24
  .value!
18
25
  end
19
26
 
27
+ def shutdown
28
+ @running.make_false
29
+ clock.shutdown
30
+ @pool_size.times do
31
+ @command_queue.push(Disqualified::Pool::QUIT)
32
+ end
33
+ end
34
+
35
+ def clock
36
+ @clock ||= Concurrent::TimerTask.new(run_now: true) do |clock_task|
37
+ @logger.debug { format_log("Disqualified::Pool#clock", "Starting") }
38
+ clock_task.execution_interval = random_interval
39
+ @command_queue.push(Disqualified::Pool::CHECK)
40
+ @logger.debug { format_log("Disqualified::Pool#clock", "Next run in #{clock_task.execution_interval}") }
41
+ rescue => e
42
+ handle_error(@error_hooks, e, {})
43
+ end
44
+ end
45
+
20
46
  def pool
21
47
  @pool ||=
22
48
  @pool_size.times.map do |promise_index|
23
- initial_delay = random_interval * promise_index / @pool_size
24
- Concurrent::Promises
25
- .schedule(initial_delay) do
26
- repeat(promise_index:, schedule: false, previous_delay: initial_delay)
27
- end
28
- .rescue { |error| handle_error(error) }
49
+ repeat(promise_index:)
29
50
  .run
30
51
  end
31
52
  end
32
53
 
33
- def repeat(promise_index:, schedule:, previous_delay:)
54
+ def repeat(promise_index:)
34
55
  if @running.false?
35
56
  return
36
57
  end
37
58
 
38
- interval =
39
- if schedule
40
- random_interval
41
- else
42
- 0
43
- end
44
-
45
- @logger.debug { format_log("[Disqualified::Pool#repeat(#{promise_index})] Interval #{interval}") }
59
+ @logger.debug { format_log("Disqualified::Pool#repeat(#{promise_index})", "Started") }
46
60
 
47
- args = {
48
- promise_index:,
49
- running: @running
50
- }
61
+ args = {promise_index:}
51
62
 
52
63
  Concurrent::Promises
53
- .schedule(interval, args, &@task)
54
- .then { repeat(promise_index:, schedule: true, previous_delay: interval) }
55
- .rescue { |error| handle_error(error) }
64
+ .future(args) do |args|
65
+ @logger.debug { format_log("Disqualified::Pool#repeat(#{promise_index}) <pre-exec>", "Waiting for command") }
66
+ command = @command_queue.pop
67
+ @logger.debug { format_log("Disqualified::Pool#repeat(#{promise_index}) <pre-exec>", "Command: #{command}") }
68
+
69
+ case command
70
+ when Disqualified::Pool::QUIT
71
+ nil
72
+ when Disqualified::Pool::CHECK
73
+ Rails.application.reloader.wrap do
74
+ pending_job_count = Disqualified::Record
75
+ .where(finished_at: nil, run_at: (..Time.now), locked_by: nil)
76
+ .count
77
+
78
+ pending_job_count.times do
79
+ @command_queue.push(Disqualified::Pool::RUN)
80
+ end
81
+ end
82
+ when Disqualified::Pool::RUN
83
+ @task.call(args)
84
+ end
85
+ rescue => e
86
+ handle_error(@error_hooks, e, {})
87
+ end
88
+ .then { repeat(promise_index:) }
56
89
  end
57
90
 
58
91
  private
@@ -60,10 +93,4 @@ class Disqualified::Pool
60
93
  def random_interval
61
94
  rand(@delay_range)
62
95
  end
63
-
64
- def handle_error(error)
65
- pp error
66
- puts "Gracefully quitting..."
67
- @running.make_false
68
- end
69
96
  end
@@ -1,3 +1,3 @@
1
1
  module Disqualified
2
- VERSION = "0.1.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,4 +1,4 @@
1
- class CreateDisqualifiedJobs < ActiveRecord::Migration[5.0]
1
+ class CreateDisqualifiedJobs < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  create_table :disqualified_jobs do |t|
4
4
  t.string :handler, null: false
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: disqualified
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Ahn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-18 00:00:00.000000000 Z
11
+ date: 2023-04-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 7.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: concurrent-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: mocktail
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -62,12 +76,12 @@ extra_rdoc_files: []
62
76
  files:
63
77
  - LICENSE
64
78
  - README.md
65
- - app/assets/config/disqualified_manifest.js
66
79
  - app/models/disqualified/base_record.rb
67
80
  - app/models/disqualified/record.rb
68
81
  - config/routes.rb
69
82
  - exe/disqualified
70
83
  - lib/disqualified.rb
84
+ - lib/disqualified/active_job.rb
71
85
  - lib/disqualified/cli.rb
72
86
  - lib/disqualified/engine.rb
73
87
  - lib/disqualified/job.rb
@@ -79,7 +93,6 @@ files:
79
93
  - lib/generators/disqualified/install/USAGE
80
94
  - lib/generators/disqualified/install/install_generator.rb
81
95
  - lib/generators/disqualified/install/templates/20220703062536_create_disqualified_jobs.rb
82
- - lib/tasks/disqualified_tasks.rake
83
96
  homepage: https://github.com/zachahn/disqualified
84
97
  licenses:
85
98
  - LGPL-3.0-only
@@ -102,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
115
  - !ruby/object:Gem::Version
103
116
  version: '0'
104
117
  requirements: []
105
- rubygems_version: 3.3.7
118
+ rubygems_version: 3.4.7
106
119
  signing_key:
107
120
  specification_version: 4
108
121
  summary: A background job processor tuned for SQLite
File without changes
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :disqualified do
3
- # # Task goes here
4
- # end