disqualified 0.1.1 → 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: 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