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 +4 -4
- data/README.md +15 -2
- data/lib/disqualified/cli.rb +7 -3
- data/lib/disqualified/logging.rb +16 -2
- data/lib/disqualified/main.rb +8 -20
- data/lib/disqualified/pool.rb +57 -30
- data/lib/disqualified/version.rb +1 -1
- data/lib/generators/disqualified/install/templates/20220703062536_create_disqualified_jobs.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 34ac6d57608468421c25cb83e842ce8cadc028b922e4437e39580ba8f8009106
|
4
|
+
data.tar.gz: 8bf4b146c66caaeae5a322c013b9be859436e17940f6747643289f76edb84abc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
data/lib/disqualified/cli.rb
CHANGED
@@ -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 => {
|
36
|
-
logger.debug { format_log("
|
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
|
data/lib/disqualified/logging.rb
CHANGED
@@ -1,7 +1,21 @@
|
|
1
1
|
module Disqualified::Logging
|
2
2
|
module_function
|
3
3
|
|
4
|
-
def format_log(
|
5
|
-
|
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
|
data/lib/disqualified/main.rb
CHANGED
@@ -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("
|
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
|
-
|
34
|
-
|
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
|
-
|
48
|
-
|
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
|
56
|
-
|
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.
|
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
|
data/lib/disqualified/pool.rb
CHANGED
@@ -1,58 +1,91 @@
|
|
1
1
|
class Disqualified::Pool
|
2
2
|
include Disqualified::Logging
|
3
3
|
|
4
|
-
|
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
|
-
|
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
|
54
|
+
def repeat(promise_index:)
|
34
55
|
if @running.false?
|
35
56
|
return
|
36
57
|
end
|
37
58
|
|
38
|
-
|
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
|
-
.
|
54
|
-
|
55
|
-
|
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
|
data/lib/disqualified/version.rb
CHANGED
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.
|
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:
|
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.
|
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
|