disqualified 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc97aaa11362ee9b3b2de020309c6ad083dce7c9c0dfa8337efdac9dc6194f81
4
- data.tar.gz: d78e23de661b6e78cffa4a46253ad3abd7a00715593307fe043d5d24776df820
3
+ metadata.gz: 34ac6d57608468421c25cb83e842ce8cadc028b922e4437e39580ba8f8009106
4
+ data.tar.gz: 8bf4b146c66caaeae5a322c013b9be859436e17940f6747643289f76edb84abc
5
5
  SHA512:
6
- metadata.gz: ddca0e4fa03d30f0c974003baa82be69d56e17128eb8a4a3d9643fda4a021562eb805b128b02f1d797213d23ef4be5c9d93e053f7a15abb52c9725a8fd51d2a6
7
- data.tar.gz: 79aa2ca84d392afb379c9a3ad9987b0df49ce0189deb1bde5d0ba3fc325e43b14da03e394296feacc785d32ff01955dabbcd7422930f442e53197d851011af50
6
+ metadata.gz: 2c5c1472c10779a1f55aa88b946801e3ef7ba725538bda4c415572ca8f103c6df81e709399455cc901b419dec15534714fde445d09503f495d9660c7c4bf7f16
7
+ data.tar.gz: 6eda9531c46d5002efd09c6784551cd2e1cefc7b912143010a92f97ce87db1d478bbcc05c507d22dcb1272f63d97afca57f9d4a758cd67185e86f4588fb7e876
data/README.md CHANGED
@@ -6,13 +6,26 @@ 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. You can use it with ActiveJob or by itself.
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
19
  Run `bundle exec disqualified --help` for more information on how to run the
15
- Disqualified server.
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.
16
29
 
17
30
 
18
31
  ### Defining a job
@@ -31,12 +31,16 @@ class Disqualified::CLI
31
31
  logger.info { ' /_/' + "v#{Disqualified::VERSION}".rjust(32, " ") }
32
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
@@ -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
@@ -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.2.0"
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.2.0
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-08-17 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
@@ -115,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
115
  - !ruby/object:Gem::Version
116
116
  version: '0'
117
117
  requirements: []
118
- rubygems_version: 3.3.7
118
+ rubygems_version: 3.4.7
119
119
  signing_key:
120
120
  specification_version: 4
121
121
  summary: A background job processor tuned for SQLite