async-background 1.0.1 → 1.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 +4 -4
- data/CHANGELOG.md +153 -0
- data/README.md +36 -15
- data/async-background.gemspec +13 -7
- data/lib/async/background/metrics.rb +1 -3
- data/lib/async/background/queue/schema.rb +14 -14
- data/lib/async/background/queue/socket_notifier.rb +44 -13
- data/lib/async/background/queue/socket_waker.rb +115 -49
- data/lib/async/background/queue/sql.rb +2 -1
- data/lib/async/background/queue/store.rb +98 -75
- data/lib/async/background/runner/queue_execution.rb +32 -8
- data/lib/async/background/runner/schedule.rb +16 -13
- data/lib/async/background/runner.rb +132 -52
- data/lib/async/background/runtime/notification.rb +41 -0
- data/lib/async/background/runtime/semaphore.rb +147 -0
- data/lib/async/background/runtime/task.rb +145 -0
- data/lib/async/background/runtime/task_group.rb +81 -0
- data/lib/async/background/runtime.rb +243 -0
- data/lib/async/background/scheduler.rb +145 -0
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +22 -26
- data/lib/async/background/web/configuration.rb +40 -93
- data/lib/async/background/web/cursor.rb +10 -21
- data/lib/async/background/web/event_hub.rb +12 -7
- data/lib/async/background/web/metrics_reader.rb +29 -35
- data/lib/async/background/web/response.rb +39 -51
- data/lib/async/background/web/serializer.rb +23 -63
- data/lib/async/background/web/snapshot.rb +32 -78
- data/lib/async/background/web/sql.rb +58 -64
- data/lib/async/background.rb +1 -2
- metadata +26 -22
|
@@ -1,101 +1,167 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'socket'
|
|
4
|
-
require 'async/notification'
|
|
5
4
|
require 'fileutils'
|
|
5
|
+
require_relative '../runtime'
|
|
6
6
|
|
|
7
7
|
module Async
|
|
8
8
|
module Background
|
|
9
9
|
module Queue
|
|
10
10
|
class SocketWaker
|
|
11
|
+
CLOSE_GRACE = 2
|
|
12
|
+
|
|
11
13
|
attr_reader :path
|
|
12
14
|
|
|
13
15
|
def initialize(path)
|
|
14
16
|
@path = path
|
|
15
17
|
@server = nil
|
|
16
|
-
@notification = ::
|
|
18
|
+
@notification = Runtime::Notification.new
|
|
17
19
|
@running = false
|
|
18
20
|
@accept_task = nil
|
|
21
|
+
@clients = Runtime::TaskGroup.new
|
|
22
|
+
@sockets = {}
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def open!
|
|
22
|
-
cleanup_stale_socket
|
|
23
26
|
ensure_directory
|
|
27
|
+
cleanup_stale_socket
|
|
24
28
|
@server = UNIXServer.new(@path)
|
|
25
|
-
File.chmod(0600, @path)
|
|
26
29
|
@running = true
|
|
30
|
+
self
|
|
27
31
|
rescue Errno::EADDRINUSE
|
|
28
32
|
raise "Socket #{@path} is already in use by another process"
|
|
29
33
|
end
|
|
30
34
|
|
|
31
|
-
def start_accept_loop(
|
|
32
|
-
@accept_task =
|
|
33
|
-
while @running
|
|
34
|
-
begin
|
|
35
|
-
client = @server.accept_nonblock
|
|
36
|
-
handle_client(task, client)
|
|
37
|
-
rescue IO::WaitReadable
|
|
38
|
-
@server.wait_readable
|
|
39
|
-
rescue Errno::EBADF, IOError
|
|
40
|
-
break
|
|
41
|
-
rescue => e
|
|
42
|
-
Console.logger.error(self) { "SocketWaker accept error: #{e.class} #{e.message}" }
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
rescue => e
|
|
46
|
-
Console.logger.error(self) { "SocketWaker loop crashed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}" }
|
|
47
|
-
ensure
|
|
48
|
-
@accept_task = nil
|
|
49
|
-
end
|
|
35
|
+
def start_accept_loop(_parent_task = nil)
|
|
36
|
+
@accept_task = Runtime.spawn(name: 'socket-waker-accept') { accept_loop }
|
|
50
37
|
end
|
|
51
38
|
|
|
52
39
|
def wait(timeout: nil)
|
|
53
|
-
|
|
54
|
-
::Async::Task.current.with_timeout(timeout) { @notification.wait }
|
|
55
|
-
else
|
|
56
|
-
@notification.wait
|
|
57
|
-
end
|
|
58
|
-
rescue ::Async::TimeoutError
|
|
59
|
-
# Timeout is normal - listener will fall back to polling
|
|
40
|
+
@notification.wait(timeout)
|
|
60
41
|
end
|
|
61
42
|
|
|
62
43
|
def signal
|
|
63
|
-
@notification.
|
|
44
|
+
@notification.signal_all
|
|
45
|
+
true
|
|
64
46
|
end
|
|
65
47
|
|
|
66
48
|
def close
|
|
49
|
+
return unless @running || @server
|
|
50
|
+
|
|
67
51
|
@running = false
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
stop_accept_loop
|
|
53
|
+
@notification.signal_all
|
|
54
|
+
hang_up_clients
|
|
55
|
+
stop_clients
|
|
71
56
|
|
|
72
57
|
@server&.close rescue nil
|
|
73
58
|
@server = nil
|
|
59
|
+
@accept_task = nil
|
|
74
60
|
File.unlink(@path) rescue nil
|
|
75
61
|
end
|
|
76
62
|
|
|
77
63
|
private
|
|
78
64
|
|
|
79
|
-
def
|
|
80
|
-
|
|
65
|
+
def accept_loop
|
|
66
|
+
while @running
|
|
81
67
|
begin
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
Console.logger.warn(self) { "SocketWaker client handler error: #{e.class} #{e.message}" }
|
|
92
|
-
ensure
|
|
93
|
-
client.close rescue nil
|
|
94
|
-
@notification.signal
|
|
68
|
+
client = @server.accept_nonblock
|
|
69
|
+
rescue IO::WaitReadable
|
|
70
|
+
@server.wait_readable
|
|
71
|
+
next
|
|
72
|
+
rescue Errno::EBADF, IOError
|
|
73
|
+
break
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
Console.logger.error(self) { "SocketWaker accept error: #{e.class} #{e.message}" }
|
|
76
|
+
next
|
|
95
77
|
end
|
|
78
|
+
|
|
79
|
+
break unless @running
|
|
80
|
+
|
|
81
|
+
handle_client(client)
|
|
82
|
+
end
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
Console.logger.error(self) { "SocketWaker loop crashed: #{e.class} #{e.message}\n#{e.backtrace.join("\n")}" }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def handle_client(client)
|
|
88
|
+
@sockets[client] = true
|
|
89
|
+
|
|
90
|
+
@clients.spawn(name: 'socket-waker-client') do
|
|
91
|
+
loop do
|
|
92
|
+
client.read_nonblock(256)
|
|
93
|
+
@notification.signal_all
|
|
94
|
+
rescue IO::WaitReadable
|
|
95
|
+
client.wait_readable
|
|
96
|
+
retry
|
|
97
|
+
rescue EOFError, Errno::ECONNRESET, Errno::EBADF, IOError
|
|
98
|
+
break
|
|
99
|
+
end
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
Console.logger.warn(self) { "SocketWaker client handler error: #{e.class} #{e.message}" }
|
|
102
|
+
ensure
|
|
103
|
+
@sockets.delete(client)
|
|
104
|
+
client.close rescue nil
|
|
105
|
+
@notification.signal_all
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def stop_accept_loop
|
|
110
|
+
task = @accept_task or return
|
|
111
|
+
|
|
112
|
+
wake_accept_loop
|
|
113
|
+
return if await(task, CLOSE_GRACE)
|
|
114
|
+
|
|
115
|
+
task.stop
|
|
116
|
+
await(task, CLOSE_GRACE)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def wake_accept_loop
|
|
120
|
+
return unless @server
|
|
121
|
+
|
|
122
|
+
UNIXSocket.open(@path) { |s| s.write_nonblock("\x00") rescue nil }
|
|
123
|
+
rescue StandardError
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def hang_up_clients
|
|
128
|
+
@sockets.keys.each do |socket|
|
|
129
|
+
socket.close
|
|
130
|
+
rescue StandardError
|
|
131
|
+
nil
|
|
96
132
|
end
|
|
97
133
|
end
|
|
98
134
|
|
|
135
|
+
def stop_clients
|
|
136
|
+
return if @clients.empty?
|
|
137
|
+
|
|
138
|
+
return if await_group(@clients, CLOSE_GRACE)
|
|
139
|
+
|
|
140
|
+
@clients.stop_all(CLOSE_GRACE)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def await(task, grace)
|
|
144
|
+
return true unless Runtime.scheduler
|
|
145
|
+
|
|
146
|
+
task.wait(grace)
|
|
147
|
+
true
|
|
148
|
+
rescue Runtime::TimeoutError
|
|
149
|
+
false
|
|
150
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
151
|
+
true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def await_group(group, grace)
|
|
155
|
+
return true unless Runtime.scheduler
|
|
156
|
+
|
|
157
|
+
group.wait(grace)
|
|
158
|
+
true
|
|
159
|
+
rescue Runtime::TimeoutError
|
|
160
|
+
false
|
|
161
|
+
rescue StandardError
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
|
|
99
165
|
def cleanup_stale_socket
|
|
100
166
|
return unless File.exist?(@path)
|
|
101
167
|
|
|
@@ -9,7 +9,8 @@ module Async
|
|
|
9
9
|
BUSY_TIMEOUT = 'PRAGMA busy_timeout'.freeze
|
|
10
10
|
TABLE_INFO = 'PRAGMA table_info(jobs)'.freeze
|
|
11
11
|
OPTIMIZE = 'PRAGMA optimize'.freeze
|
|
12
|
-
|
|
12
|
+
INCREMENTAL_VACUUM_PAGES = 64
|
|
13
|
+
INCREMENTAL_VACUUM = "PRAGMA incremental_vacuum(#{INCREMENTAL_VACUUM_PAGES})".freeze
|
|
13
14
|
AUTO_VACUUM_INCREMENTAL = 'PRAGMA auto_vacuum = INCREMENTAL'.freeze
|
|
14
15
|
BEGIN_IMMEDIATE = 'BEGIN IMMEDIATE'.freeze
|
|
15
16
|
COMMIT = 'COMMIT'.freeze
|
|
@@ -22,6 +22,7 @@ module Async
|
|
|
22
22
|
CLEANUP_INTERVAL = 300
|
|
23
23
|
CLEANUP_AGE = 3600
|
|
24
24
|
FAILED_RETENTION_AGE = 7 * 24 * 3600
|
|
25
|
+
CLEANUP_VACUUM_THRESHOLD = 100
|
|
25
26
|
ERROR_MESSAGE_MAX_LEN = 2_000
|
|
26
27
|
EMPTY_ARGS_JSON = '[]'.freeze
|
|
27
28
|
|
|
@@ -80,7 +81,13 @@ module Async
|
|
|
80
81
|
def enqueue(class_name, args = EMPTY_ARGS, run_at = nil, options: EMPTY_OPTIONS)
|
|
81
82
|
ensure_connection
|
|
82
83
|
now = realtime_now
|
|
83
|
-
@enqueue_stmt
|
|
84
|
+
stepped(@enqueue_stmt) do |statement|
|
|
85
|
+
statement.bind_param(1, class_name)
|
|
86
|
+
statement.bind_param(2, dump_args(args))
|
|
87
|
+
statement.bind_param(3, dump_options(options))
|
|
88
|
+
statement.bind_param(4, now)
|
|
89
|
+
statement.bind_param(5, run_at || now)
|
|
90
|
+
end
|
|
84
91
|
@db.last_insert_row_id
|
|
85
92
|
end
|
|
86
93
|
|
|
@@ -90,9 +97,14 @@ module Async
|
|
|
90
97
|
now = realtime_now
|
|
91
98
|
|
|
92
99
|
row = transaction do
|
|
93
|
-
|
|
100
|
+
stepped(@fetch_stmt) do |statement|
|
|
101
|
+
statement.bind_param(1, worker_id)
|
|
102
|
+
statement.bind_param(2, now)
|
|
103
|
+
statement.bind_param(3, token)
|
|
104
|
+
statement.bind_param(4, now)
|
|
105
|
+
end
|
|
94
106
|
end
|
|
95
|
-
return
|
|
107
|
+
return if row.nil? || row.empty?
|
|
96
108
|
|
|
97
109
|
maybe_cleanup
|
|
98
110
|
job_from_row(row, token)
|
|
@@ -100,26 +112,28 @@ module Async
|
|
|
100
112
|
|
|
101
113
|
def mark_started!(job_id, claim_token:, started_at: realtime_now)
|
|
102
114
|
ensure_connection
|
|
103
|
-
@mark_started_stmt
|
|
115
|
+
stepped(@mark_started_stmt) do |statement|
|
|
116
|
+
statement.bind_param(1, started_at)
|
|
117
|
+
statement.bind_param(2, job_id)
|
|
118
|
+
statement.bind_param(3, claim_token)
|
|
119
|
+
end
|
|
104
120
|
@db.changes.positive?
|
|
105
121
|
end
|
|
106
122
|
|
|
107
123
|
def complete(job_id, claim_token:, finished_at: realtime_now, duration_ms: nil)
|
|
108
124
|
ensure_connection
|
|
109
|
-
@complete_stmt
|
|
125
|
+
stepped(@complete_stmt) do |statement|
|
|
126
|
+
statement.bind_param(1, finished_at)
|
|
127
|
+
statement.bind_param(2, duration_ms)
|
|
128
|
+
statement.bind_param(3, job_id)
|
|
129
|
+
statement.bind_param(4, claim_token)
|
|
130
|
+
end
|
|
110
131
|
@db.changes.positive?
|
|
111
132
|
end
|
|
112
133
|
|
|
113
134
|
def fail(job_id, claim_token:, error_class: nil, error_message: nil, finished_at: realtime_now, duration_ms: nil)
|
|
114
135
|
ensure_connection
|
|
115
|
-
@fail_stmt
|
|
116
|
-
finished_at,
|
|
117
|
-
duration_ms,
|
|
118
|
-
error_class&.to_s,
|
|
119
|
-
truncate_message(error_message),
|
|
120
|
-
job_id,
|
|
121
|
-
claim_token
|
|
122
|
-
)
|
|
136
|
+
bind_failure(@fail_stmt, finished_at, duration_ms, error_class, error_message, job_id, claim_token)
|
|
123
137
|
@db.changes.positive?
|
|
124
138
|
end
|
|
125
139
|
|
|
@@ -146,13 +160,13 @@ module Async
|
|
|
146
160
|
|
|
147
161
|
def recover(worker_id)
|
|
148
162
|
ensure_connection
|
|
149
|
-
@requeue_stmt.
|
|
163
|
+
stepped(@requeue_stmt) { |statement| statement.bind_param(1, worker_id) }
|
|
150
164
|
@db.changes
|
|
151
165
|
end
|
|
152
166
|
|
|
153
167
|
def next_pending_run_at
|
|
154
168
|
ensure_connection
|
|
155
|
-
|
|
169
|
+
stepped(@next_pending_stmt)&.first
|
|
156
170
|
end
|
|
157
171
|
|
|
158
172
|
def data_version
|
|
@@ -243,9 +257,11 @@ module Async
|
|
|
243
257
|
end
|
|
244
258
|
|
|
245
259
|
def stored_options_for(job_id, claim_token)
|
|
246
|
-
|
|
247
|
-
|
|
260
|
+
row = stepped(@retry_state_stmt) do |statement|
|
|
261
|
+
statement.bind_param(1, job_id)
|
|
262
|
+
statement.bind_param(2, claim_token)
|
|
248
263
|
end
|
|
264
|
+
load_options(row&.first)
|
|
249
265
|
end
|
|
250
266
|
|
|
251
267
|
def retry_policy(stored_options, fallback_options)
|
|
@@ -260,29 +276,33 @@ module Async
|
|
|
260
276
|
|
|
261
277
|
def retry_job!(job_id, claim_token, policy, error_class, error_message)
|
|
262
278
|
advanced = policy.with_attempt(policy.next_attempt)
|
|
263
|
-
@retry_stmt
|
|
264
|
-
realtime_now + advanced.next_retry_delay(advanced.attempt)
|
|
265
|
-
dump_options(advanced.to_h.compact)
|
|
266
|
-
error_class&.to_s
|
|
267
|
-
truncate_message(error_message)
|
|
268
|
-
job_id
|
|
269
|
-
claim_token
|
|
270
|
-
|
|
279
|
+
stepped(@retry_stmt) do |statement|
|
|
280
|
+
statement.bind_param(1, realtime_now + advanced.next_retry_delay(advanced.attempt))
|
|
281
|
+
statement.bind_param(2, dump_options(advanced.to_h.compact))
|
|
282
|
+
statement.bind_param(3, error_class&.to_s)
|
|
283
|
+
statement.bind_param(4, truncate_message(error_message))
|
|
284
|
+
statement.bind_param(5, job_id)
|
|
285
|
+
statement.bind_param(6, claim_token)
|
|
286
|
+
end
|
|
271
287
|
@db.changes.positive? ? :retried : nil
|
|
272
288
|
end
|
|
273
289
|
|
|
274
290
|
def fail_job!(job_id, claim_token, error_class, error_message, finished_at, duration_ms)
|
|
275
|
-
@fail_stmt
|
|
276
|
-
finished_at,
|
|
277
|
-
duration_ms,
|
|
278
|
-
error_class&.to_s,
|
|
279
|
-
truncate_message(error_message),
|
|
280
|
-
job_id,
|
|
281
|
-
claim_token
|
|
282
|
-
)
|
|
291
|
+
bind_failure(@fail_stmt, finished_at, duration_ms, error_class, error_message, job_id, claim_token)
|
|
283
292
|
@db.changes.positive? ? :failed : nil
|
|
284
293
|
end
|
|
285
294
|
|
|
295
|
+
def bind_failure(statement, finished_at, duration_ms, error_class, error_message, job_id, claim_token)
|
|
296
|
+
stepped(statement) do |s|
|
|
297
|
+
s.bind_param(1, finished_at)
|
|
298
|
+
s.bind_param(2, duration_ms)
|
|
299
|
+
s.bind_param(3, error_class&.to_s)
|
|
300
|
+
s.bind_param(4, truncate_message(error_message))
|
|
301
|
+
s.bind_param(5, job_id)
|
|
302
|
+
s.bind_param(6, claim_token)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
286
306
|
def generate_claim_token = SecureRandom.hex(16)
|
|
287
307
|
|
|
288
308
|
def truncate_message(message)
|
|
@@ -293,23 +313,30 @@ module Async
|
|
|
293
313
|
end
|
|
294
314
|
|
|
295
315
|
def lease_alive?(job_id, claim_token)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
316
|
+
!stepped(@lease_check_stmt) do |statement|
|
|
317
|
+
statement.bind_param(1, job_id)
|
|
318
|
+
statement.bind_param(2, claim_token)
|
|
319
|
+
end.nil?
|
|
299
320
|
end
|
|
300
321
|
|
|
301
322
|
def transaction
|
|
302
|
-
@
|
|
323
|
+
stepped(@begin_stmt)
|
|
303
324
|
result = yield
|
|
304
|
-
@
|
|
325
|
+
stepped(@commit_stmt)
|
|
305
326
|
result
|
|
306
327
|
rescue StandardError
|
|
307
|
-
|
|
328
|
+
begin
|
|
329
|
+
stepped(@rollback_stmt)
|
|
330
|
+
rescue StandardError
|
|
331
|
+
nil
|
|
332
|
+
end
|
|
308
333
|
raise
|
|
309
334
|
end
|
|
310
335
|
|
|
311
|
-
def
|
|
312
|
-
|
|
336
|
+
def stepped(statement)
|
|
337
|
+
statement.reset!
|
|
338
|
+
yield statement if block_given?
|
|
339
|
+
statement.step
|
|
313
340
|
ensure
|
|
314
341
|
statement.reset! rescue nil
|
|
315
342
|
end
|
|
@@ -333,19 +360,26 @@ module Async
|
|
|
333
360
|
Job::Options.new(**options)
|
|
334
361
|
end
|
|
335
362
|
|
|
363
|
+
STATEMENTS = {
|
|
364
|
+
:@enqueue_stmt => SQL::INSERT_JOB,
|
|
365
|
+
:@fetch_stmt => SQL::FETCH_NEXT_JOB,
|
|
366
|
+
:@mark_started_stmt => SQL::MARK_STARTED,
|
|
367
|
+
:@complete_stmt => SQL::COMPLETE_JOB,
|
|
368
|
+
:@fail_stmt => SQL::FAIL_JOB,
|
|
369
|
+
:@retry_state_stmt => SQL::RETRY_STATE,
|
|
370
|
+
:@lease_check_stmt => SQL::LEASE_ALIVE,
|
|
371
|
+
:@retry_stmt => SQL::RETRY_JOB,
|
|
372
|
+
:@requeue_stmt => SQL::RECOVER_WORKER,
|
|
373
|
+
:@cleanup_done_stmt => SQL::CLEANUP_DONE,
|
|
374
|
+
:@cleanup_failed_stmt => SQL::CLEANUP_FAILED,
|
|
375
|
+
:@next_pending_stmt => SQL::NEXT_PENDING_RUN_AT,
|
|
376
|
+
:@begin_stmt => SQL::BEGIN_IMMEDIATE,
|
|
377
|
+
:@commit_stmt => SQL::COMMIT,
|
|
378
|
+
:@rollback_stmt => SQL::ROLLBACK
|
|
379
|
+
}.freeze
|
|
380
|
+
|
|
336
381
|
def prepare_statements
|
|
337
|
-
|
|
338
|
-
@fetch_stmt = @db.prepare(SQL::FETCH_NEXT_JOB)
|
|
339
|
-
@mark_started_stmt = @db.prepare(SQL::MARK_STARTED)
|
|
340
|
-
@complete_stmt = @db.prepare(SQL::COMPLETE_JOB)
|
|
341
|
-
@fail_stmt = @db.prepare(SQL::FAIL_JOB)
|
|
342
|
-
@retry_state_stmt = @db.prepare(SQL::RETRY_STATE)
|
|
343
|
-
@lease_check_stmt = @db.prepare(SQL::LEASE_ALIVE)
|
|
344
|
-
@retry_stmt = @db.prepare(SQL::RETRY_JOB)
|
|
345
|
-
@requeue_stmt = @db.prepare(SQL::RECOVER_WORKER)
|
|
346
|
-
@cleanup_done_stmt = @db.prepare(SQL::CLEANUP_DONE)
|
|
347
|
-
@cleanup_failed_stmt = @db.prepare(SQL::CLEANUP_FAILED)
|
|
348
|
-
@next_pending_stmt = @db.prepare(SQL::NEXT_PENDING_RUN_AT)
|
|
382
|
+
STATEMENTS.each { |name, sql| instance_variable_set(name, @db.prepare(sql)) }
|
|
349
383
|
end
|
|
350
384
|
|
|
351
385
|
def finalize_statements
|
|
@@ -354,28 +388,11 @@ module Async
|
|
|
354
388
|
end
|
|
355
389
|
|
|
356
390
|
def statements
|
|
357
|
-
|
|
358
|
-
@enqueue_stmt,
|
|
359
|
-
@fetch_stmt,
|
|
360
|
-
@mark_started_stmt,
|
|
361
|
-
@complete_stmt,
|
|
362
|
-
@fail_stmt,
|
|
363
|
-
@retry_state_stmt,
|
|
364
|
-
@lease_check_stmt,
|
|
365
|
-
@retry_stmt,
|
|
366
|
-
@requeue_stmt,
|
|
367
|
-
@cleanup_done_stmt,
|
|
368
|
-
@cleanup_failed_stmt,
|
|
369
|
-
@next_pending_stmt
|
|
370
|
-
]
|
|
391
|
+
STATEMENTS.keys.map { |name| instance_variable_get(name) }
|
|
371
392
|
end
|
|
372
393
|
|
|
373
394
|
def clear_statements
|
|
374
|
-
|
|
375
|
-
@complete_stmt = @fail_stmt = @retry_state_stmt = @lease_check_stmt = nil
|
|
376
|
-
@retry_stmt = @requeue_stmt = nil
|
|
377
|
-
@cleanup_done_stmt = @cleanup_failed_stmt = nil
|
|
378
|
-
@next_pending_stmt = nil
|
|
395
|
+
STATEMENTS.each_key { |name| instance_variable_set(name, nil) }
|
|
379
396
|
end
|
|
380
397
|
|
|
381
398
|
def maybe_cleanup
|
|
@@ -387,9 +404,15 @@ module Async
|
|
|
387
404
|
end
|
|
388
405
|
|
|
389
406
|
def cleanup_finished_jobs(now)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
@
|
|
407
|
+
deleted = 0
|
|
408
|
+
|
|
409
|
+
stepped(@cleanup_done_stmt) { |statement| statement.bind_param(1, now - CLEANUP_AGE) }
|
|
410
|
+
deleted += @db.changes
|
|
411
|
+
stepped(@cleanup_failed_stmt) { |statement| statement.bind_param(1, now - FAILED_RETENTION_AGE) }
|
|
412
|
+
deleted += @db.changes
|
|
413
|
+
|
|
414
|
+
@db.execute(SQL::INCREMENTAL_VACUUM) if deleted > CLEANUP_VACUUM_THRESHOLD
|
|
415
|
+
deleted
|
|
393
416
|
end
|
|
394
417
|
end
|
|
395
418
|
end
|
|
@@ -9,6 +9,7 @@ module Async
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
11
|
def setup_queue(queue_socket_dir, queue_db_path, queue_mmap)
|
|
12
|
+
@queue_saturated = false
|
|
12
13
|
@listen_queue = !!queue_socket_dir && !isolated_worker?
|
|
13
14
|
return unless @listen_queue
|
|
14
15
|
|
|
@@ -26,20 +27,36 @@ module Async
|
|
|
26
27
|
recover_queue_jobs
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
def start_queue_listener
|
|
30
|
-
@queue_waker.start_accept_loop
|
|
30
|
+
def start_queue_listener
|
|
31
|
+
@queue_waker.start_accept_loop
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
@services.spawn(name: 'queue-listener') do
|
|
33
34
|
logger.info { "Async::Background queue: listening on worker #{worker_index}" }
|
|
34
35
|
|
|
36
|
+
failures = 0
|
|
37
|
+
|
|
35
38
|
while running?
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
begin
|
|
40
|
+
@queue_waker.wait(timeout: next_wait_timeout)
|
|
41
|
+
break unless running?
|
|
42
|
+
|
|
43
|
+
dispatch_available_queue_jobs
|
|
44
|
+
failures = 0
|
|
45
|
+
rescue StandardError => error
|
|
46
|
+
failures += 1
|
|
47
|
+
backoff = [QUEUE_ERROR_BACKOFF * failures, QUEUE_POLL_INTERVAL].min
|
|
48
|
+
logger.error('Async::Background') do
|
|
49
|
+
"queue listener: #{error.class} #{error.message}; retrying in #{backoff}s"
|
|
50
|
+
end
|
|
51
|
+
shutdown.wait(backoff)
|
|
52
|
+
end
|
|
38
53
|
end
|
|
39
54
|
end
|
|
40
55
|
end
|
|
41
56
|
|
|
42
57
|
def next_wait_timeout
|
|
58
|
+
return QUEUE_POLL_INTERVAL if @queue_saturated
|
|
59
|
+
|
|
43
60
|
next_due = @queue_store.next_pending_run_at
|
|
44
61
|
return QUEUE_POLL_INTERVAL unless next_due
|
|
45
62
|
|
|
@@ -68,7 +85,7 @@ module Async
|
|
|
68
85
|
complete_queue_job!(job, class_name, claim_token, started_at)
|
|
69
86
|
rescue ConfigError => error
|
|
70
87
|
record_invalid_queue_job!(job, class_name, claim_token, error)
|
|
71
|
-
rescue ::
|
|
88
|
+
rescue Runtime::TimeoutError => error
|
|
72
89
|
handle_queue_failure(
|
|
73
90
|
job,
|
|
74
91
|
options,
|
|
@@ -129,11 +146,18 @@ module Async
|
|
|
129
146
|
end
|
|
130
147
|
|
|
131
148
|
def dispatch_available_queue_jobs
|
|
149
|
+
@queue_saturated = false
|
|
150
|
+
|
|
132
151
|
while running?
|
|
152
|
+
if @jobs.size >= @semaphore.limit
|
|
153
|
+
@queue_saturated = true
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
|
|
133
157
|
job = @queue_store.fetch(worker_index)
|
|
134
|
-
|
|
158
|
+
return if job.nil?
|
|
135
159
|
|
|
136
|
-
|
|
160
|
+
spawn_job { |job_task| run_queue_job(job_task, job) }
|
|
137
161
|
end
|
|
138
162
|
end
|
|
139
163
|
|
|
@@ -59,12 +59,15 @@ module Async
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def build_entries(schedule, now)
|
|
62
|
-
schedule
|
|
63
|
-
|
|
62
|
+
schedule
|
|
63
|
+
.filter_map { |name, config| entry_for(name, config, now) }
|
|
64
|
+
.each_with_object(MinHeap.new) { |entry, heap| heap.push(entry) }
|
|
65
|
+
end
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
def entry_for(name, config, now)
|
|
68
|
+
return unless assigned_worker(config, name) == worker_index
|
|
69
|
+
|
|
70
|
+
build_entry(name, build_task_config(name, config), now)
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def assigned_worker(config, name)
|
|
@@ -98,18 +101,18 @@ module Async
|
|
|
98
101
|
end
|
|
99
102
|
|
|
100
103
|
def parse_interval(name, value)
|
|
101
|
-
|
|
102
|
-
interval = interval.to_i
|
|
103
|
-
raise ConfigError, "[#{name}] 'every' must be > 0" unless interval.positive?
|
|
104
|
+
return if value.nil?
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
interval = value.to_i
|
|
107
|
+
raise ConfigError, "[#{name}] 'every' must be > 0" unless interval.positive?
|
|
108
|
+
|
|
109
|
+
interval
|
|
107
110
|
end
|
|
108
111
|
|
|
109
112
|
def parse_cron(name, value)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
return if value.nil?
|
|
114
|
+
|
|
115
|
+
Fugit::Cron.new(value) || raise(ConfigError, "[#{name}] invalid cron: #{value}")
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
def parse_timeout(name, config)
|