async-background 1.0.1 → 1.0.2
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 +86 -0
- data/lib/async/background/metrics.rb +1 -3
- data/lib/async/background/queue/socket_notifier.rb +44 -13
- data/lib/async/background/queue/socket_waker.rb +9 -6
- data/lib/async/background/queue/sql.rb +2 -1
- data/lib/async/background/queue/store.rb +85 -45
- data/lib/async/background/runner.rb +4 -14
- data/lib/async/background/version.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: 233f46d3617b40d9a8a27f83689050b0127603679342ddfa80a18eea0137651c
|
|
4
|
+
data.tar.gz: 5d70fe357702dff97b51734c1fddf64178f3ea0d9d9d88449fe3af9b32fc59d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 148770cb2522083b5944a5ee0f5830a1e1453f9c06647feaf0b92842e6008e2654aefd7e8c4a4a97509fc823702d64176a7bf4e30e8a08d409308f57792bbb1d
|
|
7
|
+
data.tar.gz: 99e64f1f1066528429617270d54ae64109896d1318634f5b4b001a9d10e4ed19b88239dc4b7a089c885d4f481d2799b22b70495b763d64df51702c82b15865f8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
Queue maintenance bug fix plus profiler-driven work on the hot paths that
|
|
6
|
+
actually showed up in StackProf: the sqlite3 statement wrapper, transaction
|
|
7
|
+
control, and the socket notifier.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- `Store#cleanup_finished_jobs` tested `@db.changes` after running *both*
|
|
12
|
+
DELETEs. `sqlite3_changes()` reports only the most recent statement, so the
|
|
13
|
+
incremental-vacuum decision saw the failed-job count alone and ignored every
|
|
14
|
+
deleted done-job. On a busy queue, where done-jobs vastly outnumber failed
|
|
15
|
+
ones, the vacuum effectively never ran and the database file grew without
|
|
16
|
+
bound. Both counts are now summed.
|
|
17
|
+
- `PRAGMA incremental_vacuum` ran without a page limit, releasing every free
|
|
18
|
+
page in a single blocking call — an unbounded reactor stall proportional to
|
|
19
|
+
the accumulated free list. Now capped at 64 pages per call
|
|
20
|
+
(`SQL::INCREMENTAL_VACUUM_PAGES`).
|
|
21
|
+
- `SocketWaker` signalled its notification only from the `ensure` block, so a
|
|
22
|
+
wake-up was really driven by the client disconnecting rather than by the
|
|
23
|
+
wake byte arriving. It happened to work because `SocketNotifier` closes
|
|
24
|
+
immediately after writing, but it made the protocol depend on a disconnect.
|
|
25
|
+
The signal now fires for every byte received; the `ensure` still signals so a
|
|
26
|
+
disconnect racing with a read cannot drop a wake-up.
|
|
27
|
+
|
|
28
|
+
### Performance
|
|
29
|
+
|
|
30
|
+
Measured on the `enqueue_stress` CI scenario (2.1M inserts / 30s, two producer
|
|
31
|
+
processes). `SQLite3::Statement#step` accounts for ~82% of producer wall time
|
|
32
|
+
and is irreducible; the changes below target what surrounded it.
|
|
33
|
+
|
|
34
|
+
- `Store#enqueue` and `Store#fetch` bind and step their prepared statements
|
|
35
|
+
directly instead of calling `Statement#execute`. The wrapper built a splat
|
|
36
|
+
array, ran `Array#flatten` over it and allocated a `ResultSet` — roughly 8%
|
|
37
|
+
of producer wall time, and `Array#flatten` alone was 5.8% of all object
|
|
38
|
+
allocations. Behaviour is unchanged: `execute` only steps when
|
|
39
|
+
`column_count == 0`, which is exactly what the INSERT path needed, and the
|
|
40
|
+
`UPDATE ... RETURNING` fetch still consumes a single row.
|
|
41
|
+
- `SocketNotifier` caches its socket paths at construction. It previously ran
|
|
42
|
+
`File.join` plus a string interpolation on every enqueue attempt —
|
|
43
|
+
13.3% of allocations in the normal scenario.
|
|
44
|
+
- `SocketNotifier` remembers unreachable workers for `DEAD_WORKER_TTL` (5s)
|
|
45
|
+
instead of reconnecting to them on every enqueue. Because the scan started at
|
|
46
|
+
a random index, a dead worker was re-probed indefinitely and each attempt
|
|
47
|
+
raised and swallowed an `Errno`; `SystemCallError#initialize` alone was 6.8%
|
|
48
|
+
of allocations. A worker that starts during the TTL window is still picked up
|
|
49
|
+
by the listener's own polling fallback.
|
|
50
|
+
- `SocketNotifier` rotates its scan start with a cursor rather than calling
|
|
51
|
+
`rand` per enqueue, which also spreads wake-ups deterministically.
|
|
52
|
+
- `Errno::EAGAIN` on the wake byte (`IO::WaitWritable`) is now treated as
|
|
53
|
+
success rather than falling through to the generic rescue and logging a
|
|
54
|
+
warning: a full send buffer means the worker already has unread wake-ups.
|
|
55
|
+
- `Store#transaction` runs `BEGIN IMMEDIATE`, `COMMIT` and `ROLLBACK` as
|
|
56
|
+
prepared statements. They previously went through `Database#execute`, which
|
|
57
|
+
compiles a fresh `Statement` and builds a `ResultSet` on every call: 26
|
|
58
|
+
allocations per BEGIN+COMMIT pair against 2 prepared, and 6x the wall time
|
|
59
|
+
over 30k pairs. Every `fetch` paid this twice, and it was 11.8% of worker
|
|
60
|
+
allocations in the object profile.
|
|
61
|
+
- `mark_started!`, `complete`, `fail`, `retry_job!`, `recover`,
|
|
62
|
+
`stored_options_for`, `lease_alive?`, `next_pending_run_at` and the two
|
|
63
|
+
cleanup DELETEs now bind and step directly, like `enqueue` and `fetch`
|
|
64
|
+
already did. `Statement#execute` is gone from the Store entirely;
|
|
65
|
+
`Database#execute` remains only on the cold pragma path. This is what was
|
|
66
|
+
still leaving `Array#flatten` at 4.9% of worker allocations and
|
|
67
|
+
`Statement#execute` at 10.8% of worker wall time.
|
|
68
|
+
- Combined effect on a full job cycle (enqueue omitted; fetch + mark_started +
|
|
69
|
+
complete, 8000 jobs, measured twice): **49 -> 18 allocations per job**, wall
|
|
70
|
+
time down roughly 10-20% depending on run.
|
|
71
|
+
- `@db.changes` and `@db.last_insert_row_id` are read after the statement is
|
|
72
|
+
reset; both were verified to survive `sqlite3_reset()`, so the claim-token
|
|
73
|
+
lease checks behave exactly as before (valid token true, stale token false,
|
|
74
|
+
repeat completion false).
|
|
75
|
+
|
|
76
|
+
### Notes
|
|
77
|
+
|
|
78
|
+
- `SocketNotifier` still opens a fresh connection per notification. This is
|
|
79
|
+
the largest remaining win and it is paid on both sides: 8.5% of producer wall
|
|
80
|
+
time in the scenario where workers are actually up, plus 16.4% of worker
|
|
81
|
+
allocations, because every connection makes `SocketWaker#handle_client` spawn
|
|
82
|
+
a fresh `Async::Task` and fiber. Persistent connections require the
|
|
83
|
+
`SocketWaker` fix above to be deployed on every worker first — a 1.0.2 producer holding a connection open
|
|
84
|
+
to a 1.0.1 worker would never wake it, silently degrading queue latency to
|
|
85
|
+
the 5s poll interval. Deferred until 1.0.2 is the deployed floor.
|
|
86
|
+
- No schema change; `Schema::VERSION` is untouched and 1.0.2 is a drop-in
|
|
87
|
+
replacement for 1.0.1 on an existing database.
|
|
88
|
+
|
|
3
89
|
## 1.0.1
|
|
4
90
|
|
|
5
91
|
Dashboard security headers and a fiber-native rewrite of the SSE stream.
|
|
@@ -127,9 +127,7 @@ module Async
|
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
def decode_workers(buffer, schema, segment, total_workers)
|
|
130
|
-
(1..total_workers).map
|
|
131
|
-
decode_worker(buffer, schema, segment, worker)
|
|
132
|
-
end.freeze
|
|
130
|
+
(1..total_workers).map { |worker| decode_worker(buffer, schema, segment, worker) }.freeze
|
|
133
131
|
end
|
|
134
132
|
|
|
135
133
|
def decode_worker(buffer, schema, segment, worker)
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require 'socket'
|
|
4
4
|
|
|
5
|
+
require_relative '../clock'
|
|
6
|
+
|
|
5
7
|
module Async
|
|
6
8
|
module Background
|
|
7
9
|
module Queue
|
|
8
10
|
class SocketNotifier
|
|
11
|
+
include Clock
|
|
12
|
+
|
|
9
13
|
# Errors that indicate a worker is unavailable - silently skip and try the next.
|
|
10
14
|
UNAVAILABLE = [
|
|
11
15
|
Errno::ENOENT, # Socket file doesn't exist (worker hasn't started yet)
|
|
@@ -14,41 +18,68 @@ module Async
|
|
|
14
18
|
Errno::ECONNRESET # Connection reset by peer
|
|
15
19
|
].freeze
|
|
16
20
|
|
|
21
|
+
DEAD_WORKER_TTL = 5.0
|
|
22
|
+
WAKE_BYTE = "\x01".freeze
|
|
23
|
+
|
|
17
24
|
def initialize(socket_dir:, total_workers:)
|
|
18
25
|
@socket_dir = socket_dir
|
|
19
26
|
@total_workers = total_workers
|
|
27
|
+
@paths = build_paths
|
|
28
|
+
@dead_until = Array.new([@total_workers, 0].max, 0.0)
|
|
29
|
+
@cursor = 0
|
|
20
30
|
end
|
|
21
31
|
|
|
22
32
|
def notify_all
|
|
23
|
-
return if @total_workers <= 0
|
|
33
|
+
return false if @total_workers <= 0
|
|
34
|
+
|
|
35
|
+
now = monotonic_now
|
|
36
|
+
start = advance_cursor
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return if notify_one(worker_index)
|
|
38
|
+
@total_workers.times do |offset|
|
|
39
|
+
index = (start + offset) % @total_workers
|
|
40
|
+
return true if notify_one(index, now)
|
|
29
41
|
end
|
|
42
|
+
|
|
43
|
+
false
|
|
30
44
|
end
|
|
31
45
|
|
|
32
46
|
private
|
|
33
47
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
def build_paths
|
|
49
|
+
return [].freeze if @total_workers <= 0
|
|
50
|
+
|
|
51
|
+
Array.new(@total_workers) do |index|
|
|
52
|
+
File.join(@socket_dir, "async_bg_worker_#{index + 1}.sock").freeze
|
|
53
|
+
end.freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def advance_cursor
|
|
57
|
+
@cursor = (@cursor + 1) % @total_workers
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def notify_one(index, now)
|
|
61
|
+
return false if @dead_until[index] > now
|
|
62
|
+
|
|
63
|
+
socket = UNIXSocket.new(@paths[index])
|
|
37
64
|
begin
|
|
38
|
-
|
|
65
|
+
socket.write_nonblock(WAKE_BYTE)
|
|
39
66
|
ensure
|
|
40
|
-
|
|
67
|
+
socket.close rescue nil
|
|
41
68
|
end
|
|
42
69
|
true
|
|
70
|
+
rescue IO::WaitWritable
|
|
71
|
+
true
|
|
43
72
|
rescue *UNAVAILABLE
|
|
73
|
+
mark_dead(index, now)
|
|
44
74
|
false
|
|
45
75
|
rescue => e
|
|
46
|
-
|
|
76
|
+
mark_dead(index, now)
|
|
77
|
+
Console.logger.warn(self) { "SocketNotifier#notify_one(#{index + 1}) failed: #{e.class} #{e.message}" } rescue nil
|
|
47
78
|
false
|
|
48
79
|
end
|
|
49
80
|
|
|
50
|
-
def
|
|
51
|
-
|
|
81
|
+
def mark_dead(index, now)
|
|
82
|
+
@dead_until[index] = now + DEAD_WORKER_TTL
|
|
52
83
|
end
|
|
53
84
|
end
|
|
54
85
|
end
|
|
@@ -80,12 +80,15 @@ module Async
|
|
|
80
80
|
parent_task.async do
|
|
81
81
|
begin
|
|
82
82
|
loop do
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
begin
|
|
84
|
+
client.read_nonblock(256)
|
|
85
|
+
@notification.signal
|
|
86
|
+
rescue IO::WaitReadable
|
|
87
|
+
client.wait_readable
|
|
88
|
+
retry
|
|
89
|
+
rescue EOFError, Errno::ECONNRESET
|
|
90
|
+
break
|
|
91
|
+
end
|
|
89
92
|
end
|
|
90
93
|
rescue => e
|
|
91
94
|
Console.logger.warn(self) { "SocketWaker client handler error: #{e.class} #{e.message}" }
|
|
@@ -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
|
|
@@ -346,6 +373,9 @@ module Async
|
|
|
346
373
|
@cleanup_done_stmt = @db.prepare(SQL::CLEANUP_DONE)
|
|
347
374
|
@cleanup_failed_stmt = @db.prepare(SQL::CLEANUP_FAILED)
|
|
348
375
|
@next_pending_stmt = @db.prepare(SQL::NEXT_PENDING_RUN_AT)
|
|
376
|
+
@begin_stmt = @db.prepare(SQL::BEGIN_IMMEDIATE)
|
|
377
|
+
@commit_stmt = @db.prepare(SQL::COMMIT)
|
|
378
|
+
@rollback_stmt = @db.prepare(SQL::ROLLBACK)
|
|
349
379
|
end
|
|
350
380
|
|
|
351
381
|
def finalize_statements
|
|
@@ -366,7 +396,10 @@ module Async
|
|
|
366
396
|
@requeue_stmt,
|
|
367
397
|
@cleanup_done_stmt,
|
|
368
398
|
@cleanup_failed_stmt,
|
|
369
|
-
@next_pending_stmt
|
|
399
|
+
@next_pending_stmt,
|
|
400
|
+
@begin_stmt,
|
|
401
|
+
@commit_stmt,
|
|
402
|
+
@rollback_stmt
|
|
370
403
|
]
|
|
371
404
|
end
|
|
372
405
|
|
|
@@ -376,6 +409,7 @@ module Async
|
|
|
376
409
|
@retry_stmt = @requeue_stmt = nil
|
|
377
410
|
@cleanup_done_stmt = @cleanup_failed_stmt = nil
|
|
378
411
|
@next_pending_stmt = nil
|
|
412
|
+
@begin_stmt = @commit_stmt = @rollback_stmt = nil
|
|
379
413
|
end
|
|
380
414
|
|
|
381
415
|
def maybe_cleanup
|
|
@@ -387,9 +421,15 @@ module Async
|
|
|
387
421
|
end
|
|
388
422
|
|
|
389
423
|
def cleanup_finished_jobs(now)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
@
|
|
424
|
+
deleted = 0
|
|
425
|
+
|
|
426
|
+
stepped(@cleanup_done_stmt) { |statement| statement.bind_param(1, now - CLEANUP_AGE) }
|
|
427
|
+
deleted += @db.changes
|
|
428
|
+
stepped(@cleanup_failed_stmt) { |statement| statement.bind_param(1, now - FAILED_RETENTION_AGE) }
|
|
429
|
+
deleted += @db.changes
|
|
430
|
+
|
|
431
|
+
@db.execute(SQL::INCREMENTAL_VACUUM) if deleted > CLEANUP_VACUUM_THRESHOLD
|
|
432
|
+
deleted
|
|
393
433
|
end
|
|
394
434
|
end
|
|
395
435
|
end
|
|
@@ -47,11 +47,7 @@ module Async
|
|
|
47
47
|
@total_workers = total_workers
|
|
48
48
|
@running = true
|
|
49
49
|
@shutdown = ::Async::Condition.new
|
|
50
|
-
@metrics = Metrics.new(
|
|
51
|
-
worker_index: worker_index,
|
|
52
|
-
total_workers: total_workers,
|
|
53
|
-
shm_path: metrics_shm_path
|
|
54
|
-
)
|
|
50
|
+
@metrics = Metrics.new(worker_index: worker_index, total_workers: total_workers, shm_path: metrics_shm_path)
|
|
55
51
|
logger.info { "Async::Background worker_index=#{worker_index}/#{total_workers}, job_count=#{job_count}" }
|
|
56
52
|
|
|
57
53
|
@drain_barrier = ::Async::Barrier.new
|
|
@@ -91,7 +87,7 @@ module Async
|
|
|
91
87
|
# continues independently in its own Async task.
|
|
92
88
|
return shutdown.wait if heap.empty? && @listen_queue
|
|
93
89
|
|
|
94
|
-
|
|
90
|
+
while running?
|
|
95
91
|
entry = heap.peek
|
|
96
92
|
break unless entry
|
|
97
93
|
|
|
@@ -123,12 +119,7 @@ module Async
|
|
|
123
119
|
end
|
|
124
120
|
|
|
125
121
|
def dispatch_entry(entry)
|
|
126
|
-
|
|
127
|
-
skip_entry(entry)
|
|
128
|
-
else
|
|
129
|
-
execute_entry(entry)
|
|
130
|
-
end
|
|
131
|
-
|
|
122
|
+
entry.running ? skip_entry(entry) : execute_entry(entry)
|
|
132
123
|
entry.reschedule(monotonic_now)
|
|
133
124
|
heap.replace_top(entry)
|
|
134
125
|
end
|
|
@@ -182,12 +173,11 @@ module Async
|
|
|
182
173
|
|
|
183
174
|
def start_signal_watcher(task)
|
|
184
175
|
task.async(transient: true) do
|
|
185
|
-
|
|
176
|
+
while running?
|
|
186
177
|
@signal_r.wait_readable
|
|
187
178
|
@signal_r.read_nonblock(256) rescue nil
|
|
188
179
|
shutdown.signal
|
|
189
180
|
@queue_waker&.signal
|
|
190
|
-
break unless running?
|
|
191
181
|
end
|
|
192
182
|
end
|
|
193
183
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-background
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Hajdarov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -198,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
198
198
|
- !ruby/object:Gem::Version
|
|
199
199
|
version: '0'
|
|
200
200
|
requirements: []
|
|
201
|
-
rubygems_version: 3.
|
|
201
|
+
rubygems_version: 3.4.22
|
|
202
202
|
signing_key:
|
|
203
203
|
specification_version: 4
|
|
204
204
|
summary: Lightweight heap-based cron/interval scheduler for Async.
|