async-background 1.0.0 → 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 +405 -276
- data/README.md +91 -109
- 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
- data/lib/async/background/web/app.rb +39 -18
- data/lib/async/background/web/auth.rb +7 -2
- data/lib/async/background/web/configuration.rb +17 -3
- data/lib/async/background/web/event_hub.rb +25 -148
- data/lib/async/background/web/response.rb +31 -9
- data/lib/async/background/web/router.rb +3 -1
- data/lib/async/background/web/stream.rb +54 -15
- metadata +3 -3
data/CHANGELOG.md
CHANGED
|
@@ -1,358 +1,487 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
+
|
|
89
|
+
## 1.0.1
|
|
90
|
+
|
|
91
|
+
Dashboard security headers and a fiber-native rewrite of the SSE stream.
|
|
92
|
+
|
|
93
|
+
### Security
|
|
94
|
+
|
|
95
|
+
- HTML shell now ships a strict CSP and `X-Frame-Options: DENY`. The CSP is
|
|
96
|
+
`default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'
|
|
97
|
+
data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none';
|
|
98
|
+
form-action 'none'`.
|
|
99
|
+
- Every response carries `X-Content-Type-Options: nosniff`,
|
|
100
|
+
`Referrer-Policy: no-referrer`, and `Cross-Origin-Resource-Policy:
|
|
101
|
+
same-origin`.
|
|
102
|
+
- `401` and `404` responses are now JSON like every other error — minor
|
|
103
|
+
breaking change for clients that parsed the previous `text/plain` body.
|
|
104
|
+
- `HEAD` is accepted on every route that accepts `GET` (RFC 9110 §9.3.2).
|
|
105
|
+
`HEAD /api/stream` returns the SSE headers without opening a stream.
|
|
106
|
+
- `mount_path` validation is stricter: must start with `/`, no trailing
|
|
107
|
+
slash, no control characters, no whitespace.
|
|
108
|
+
- New `Configuration#logger` (any object responding to `#warn` / `#error`).
|
|
109
|
+
When set, auth-callable exceptions, internal `rescue StandardError`, and
|
|
110
|
+
SSE stream errors are surfaced instead of being silently swallowed.
|
|
111
|
+
|
|
112
|
+
### Architecture: fiber-native SSE
|
|
113
|
+
|
|
114
|
+
- Removed the per-process monitor `Thread.new` and per-subscription
|
|
115
|
+
`Mutex` + `ConditionVariable`. The dashboard web subsystem spawns **zero**
|
|
116
|
+
native threads of its own.
|
|
117
|
+
- `EventHub` is now a tiny mutex-guarded frame cache keyed by `data_version`.
|
|
118
|
+
No subscriptions, no monitor.
|
|
119
|
+
- `Stream#each` runs the entire poll-and-yield loop inside the per-request
|
|
120
|
+
Falcon fiber: `sleep` (fiber-aware), read `PRAGMA data_version`, yield the
|
|
121
|
+
`overview` frame when the version moves, yield a heartbeat otherwise.
|
|
122
|
+
- Each tab now polls `data_version` independently. At default 0.5 s and
|
|
123
|
+
realistic operator fan-out that's well under 50 SQLite header reads /
|
|
124
|
+
second per process. JSON rendering still happens at most once per version
|
|
125
|
+
thanks to the hub cache.
|
|
126
|
+
- Shutdown is clean: `App#close` marks the hub closed, the next poll raises
|
|
127
|
+
`ClosedError`, the loop exits. No thread to join, no `Subscription` to
|
|
128
|
+
unsubscribe.
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
4
133
|
|
|
5
|
-
|
|
134
|
+
## 1.0.0
|
|
6
135
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
136
|
+
First stable release. The queue execution contract from 0.7.2 (claim-token
|
|
137
|
+
CAS, lifecycle columns, barrier-based shutdown drain, per-status partial
|
|
138
|
+
indexes, versioned migrations) is now considered the public API.
|
|
139
|
+
|
|
140
|
+
### Dashboard
|
|
141
|
+
|
|
142
|
+
A read-only Rack-mountable UI under `require 'async/background/web'`:
|
|
143
|
+
vanilla HTML / CSS / JS, no framework, no npm.
|
|
144
|
+
|
|
145
|
+
- Endpoints: `GET /`, `GET /assets/{app.css,app.js}`,
|
|
146
|
+
`GET /api/{overview,executing,claimed,pending,done,failed,metrics,config,stream}`.
|
|
147
|
+
- The read path runs through `Async::Background::Web::Snapshot`, which
|
|
148
|
+
opens SQLite with `file:?mode=ro`, wraps a `Mutex` around a single shared
|
|
149
|
+
connection, and uses one read transaction per endpoint plus a TTL'd
|
|
150
|
+
overview cache (`counts_cache_ttl`, default 3 s).
|
|
151
|
+
- Distinguishes **executing** (`status='running' AND started_at IS NOT
|
|
152
|
+
NULL`) from **claimed** (`status='running' AND started_at IS NULL`).
|
|
153
|
+
- Cursor pagination for `done` / `failed` / `pending` using
|
|
154
|
+
`(finished_at, id)` / `(run_at, id)` tuples. Stable on ties.
|
|
155
|
+
- Args hidden by default (`expose_args: false`); when enabled, content
|
|
156
|
+
runs through `redact_args`. All user content rendered through
|
|
157
|
+
`textContent`, never `innerHTML`.
|
|
158
|
+
- `auth` is **mandatory**. `Configuration#validate!` rejects an
|
|
159
|
+
unconfigured `auth`. There is no permissive default — a falsey result
|
|
160
|
+
returns `401`.
|
|
161
|
+
|
|
162
|
+
### SSE transport
|
|
163
|
+
|
|
164
|
+
The dashboard uses a single long-lived `text/event-stream` connection per
|
|
165
|
+
browser tab instead of polling `/api/overview` every 2 seconds. One HTTP
|
|
166
|
+
connection per tab regardless of how long it stays open.
|
|
167
|
+
|
|
168
|
+
- `Configuration#transport` accepts `:sse` (default) or `:polling`. Anything
|
|
169
|
+
else raises `ConfigurationError`. The chosen transport is exposed at
|
|
170
|
+
`/api/config` so the client knows which path to take.
|
|
171
|
+
- Client opens `EventSource(mount_path + '/api/stream')` once; the server
|
|
172
|
+
pushes an `overview` event when `PRAGMA data_version` changes and a
|
|
173
|
+
`:keepalive` comment frame every 25 s.
|
|
174
|
+
- Server-supplied 5 s reconnect delay; each reconnect begins from a full
|
|
175
|
+
current snapshot (no event log).
|
|
176
|
+
- Asset URLs are fingerprinted and cached immutably by digest, so a
|
|
177
|
+
dashboard deploy can't leave a browser on incompatible HTML / JS / CSS.
|
|
178
|
+
|
|
179
|
+
### Server compatibility for SSE
|
|
180
|
+
|
|
181
|
+
SSE holds the response open for the lifetime of the dashboard tab.
|
|
182
|
+
|
|
183
|
+
- **Falcon** — recommended. Handles long-lived connections via fibers.
|
|
184
|
+
- **Puma** — works. Each open tab holds one worker thread for its lifetime;
|
|
185
|
+
fine for a handful of operators, problematic if many concurrent operators
|
|
186
|
+
would starve the worker pool.
|
|
187
|
+
- **Unicorn** — doesn't work. Blocking worker model can't hold long-lived
|
|
188
|
+
connections without timeouts. Stay on `:polling`.
|
|
189
|
+
|
|
190
|
+
See the picture in the README for what each server is actually holding.
|
|
14
191
|
|
|
15
|
-
|
|
192
|
+
### Configuration
|
|
16
193
|
|
|
17
|
-
|
|
194
|
+
```ruby
|
|
195
|
+
require 'async/background/web'
|
|
18
196
|
|
|
19
|
-
|
|
197
|
+
Async::Background::Queue::Store.prepare_dashboard!(path: '/var/lib/app/queue.db')
|
|
20
198
|
|
|
21
|
-
|
|
199
|
+
Async::Background::Web.configure do |c|
|
|
200
|
+
c.queue_path = '/var/lib/app/queue.db'
|
|
201
|
+
c.auth = ->(env) { env['warden'].user&.admin? }
|
|
202
|
+
c.expose_args = false
|
|
203
|
+
c.metrics_path = '/run/app/async-background.shm'
|
|
204
|
+
c.total_workers = 4
|
|
205
|
+
c.counts_cache_ttl = 3.0
|
|
206
|
+
c.poll_interval_ms = 2000
|
|
207
|
+
c.list_limit = 50
|
|
208
|
+
c.mount_path = '/admin/background'
|
|
209
|
+
c.title = 'My App background jobs'
|
|
210
|
+
end
|
|
22
211
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- New `Response.sse(body)` helper sets the correct headers including `x-accel-buffering: no` (disables nginx buffering for the streaming response).
|
|
26
|
-
- JS client (`assets.rb`) detects `state.config.transport === 'sse'` at boot and chooses `EventSource` over `setInterval(tick, ...)`. Both transports share the same `applyOverview()` and `refreshActiveList()` handlers, so the UI behaves identically.
|
|
212
|
+
run Async::Background::Web.app
|
|
213
|
+
```
|
|
27
214
|
|
|
28
|
-
|
|
215
|
+
### Dependencies
|
|
29
216
|
|
|
30
|
-
|
|
217
|
+
`rack` is optional. Required only when `require 'async/background/web'` is
|
|
218
|
+
loaded. Core gem and worker processes don't require it.
|
|
31
219
|
|
|
32
|
-
|
|
220
|
+
### Breaking changes from 0.7.x
|
|
33
221
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
c.queue_path = ...
|
|
37
|
-
c.auth = ->(env) { ... }
|
|
38
|
-
c.transport = :sse
|
|
39
|
-
end
|
|
40
|
-
```
|
|
222
|
+
None beyond what 0.7.2 already shipped. The 1.0 line locks the existing
|
|
223
|
+
contract:
|
|
41
224
|
|
|
42
|
-
|
|
225
|
+
- `Queue::Store#fetch` returns `claim_token` in the result hash.
|
|
226
|
+
- All terminal `Queue::Store` methods (`complete`, `fail`, `retry_or_fail`)
|
|
227
|
+
require the `claim_token:` kwarg and return CAS success boolean /
|
|
228
|
+
`:retried` / `:failed` / `nil`.
|
|
229
|
+
- Schema is versioned via `PRAGMA user_version`. Use
|
|
230
|
+
`Queue::Store.migrate!(path:)` to upgrade. Use
|
|
231
|
+
`Queue::Store.prepare_dashboard!(path:)` from the dashboard process to
|
|
232
|
+
add dashboard-only indexes.
|
|
43
233
|
|
|
44
|
-
SSE holds the request thread/fiber open for the lifetime of the dashboard tab. **Recommended for Falcon**, which handles long-lived connections natively via fibers. **Puma works** but each open dashboard tab holds one worker thread for its lifetime — fine for an admin dashboard with a handful of operators, problematic if many concurrent operators would starve the worker pool. **Unicorn does not work** for SSE since its blocking worker model can't hold long-lived connections without timeouts; stay on `:polling` there.
|
|
45
234
|
|
|
46
|
-
### Backend-side polling
|
|
47
235
|
|
|
48
|
-
|
|
236
|
+
|
|
49
237
|
|
|
50
|
-
|
|
238
|
+
## 0.7.2
|
|
51
239
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- Extended `spec/async/background/web/configuration_spec.rb` — accepts `:sse`, rejects unknown transports.
|
|
240
|
+
Harden queue execution, retries, shutdown, and metrics. Adds schema v1,
|
|
241
|
+
optional dashboard indexes, and a faster enqueue path.
|
|
55
242
|
|
|
56
|
-
## 1.0.0
|
|
57
243
|
|
|
58
|
-
First stable release. The queue execution contract from 0.7.2 (claim-token CAS, lifecycle columns, barrier-based shutdown drain, per-status partial indexes, versioned migrations) is now considered the public API.
|
|
59
244
|
|
|
60
|
-
|
|
245
|
+
|
|
61
246
|
|
|
62
|
-
|
|
63
|
-
- Endpoints: `GET /`, `GET /assets/app.css`, `GET /assets/app.js`, `GET /api/overview`, `GET /api/executing`, `GET /api/claimed`, `GET /api/pending`, `GET /api/done`, `GET /api/failed`, `GET /api/metrics`, `GET /api/config`.
|
|
64
|
-
- Default transport is JSON polling (`poll_interval_ms`, default 2000). SSE adapter for Falcon is intentionally deferred to a later release; the dashboard already coalesces work via a shared overview cache, so adding SSE later is a backward-compatible change.
|
|
65
|
-
- Read path runs through `Async::Background::Web::Snapshot`, which opens SQLite with `file:?mode=ro`, wraps a `Mutex` around a single shared connection, and uses one read transaction per endpoint and caches each overview as one consistent snapshot.
|
|
66
|
-
- Distinguishes `Executing` (`status='running' AND started_at IS NOT NULL`) from `Claimed` (`status='running' AND started_at IS NULL`).
|
|
67
|
-
- Overview snapshot cache for `counts_cache_ttl` seconds (default 3.0) so a busy queue does not turn the dashboard into a hot reader.
|
|
68
|
-
- Cursor pagination for `done`/`failed`/`pending` using `(finished_at, id)` / `(run_at, id)` tuples. Stable on ties.
|
|
69
|
-
- Args hidden by default (`expose_args: false`); when enabled, content runs through `redact_args`. All user content rendered through `textContent`, never `innerHTML`.
|
|
70
|
-
- Auth hook is **mandatory**. `Configuration#validate!` rejects an unconfigured `auth`. There is no permissive default.
|
|
247
|
+
## 0.7.1
|
|
71
248
|
|
|
72
|
-
|
|
73
|
-
|
|
249
|
+
`Store` exposes three SQLite tuning knobs via `StoreOptions`, validated at
|
|
250
|
+
construction time so misconfigurations fail fast:
|
|
74
251
|
|
|
75
|
-
|
|
252
|
+
- `mmap` (`true` / `false`, default `true`) — memory-mapped I/O.
|
|
253
|
+
- `synchronous` (`:normal` / `:full` / `:extra`, default `:normal`) —
|
|
254
|
+
durability vs throughput.
|
|
255
|
+
- `wal_autocheckpoint` (`Integer` in `100..10_000`, default `1_000`) — WAL
|
|
256
|
+
checkpoint frequency in pages.
|
|
76
257
|
|
|
77
|
-
|
|
78
|
-
|
|
258
|
+
**Breaking change.** `Store.new(path:, mmap:)` → `Store.new(path:, options:
|
|
259
|
+
{ mmap: ... })`. The direct `mmap:` kwarg is removed in favor of the
|
|
260
|
+
unified `options:` hash. Update any call site that constructs `Store`
|
|
261
|
+
manually.
|
|
79
262
|
|
|
80
|
-
|
|
263
|
+
See [Get Started → Store tuning](docs/GET_STARTED.md#appendix-store-tuning)
|
|
264
|
+
for trade-offs.
|
|
81
265
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
## 0.6.2
|
|
271
|
+
|
|
272
|
+
Queue jobs gain a **configurable timeout** at three levels — call-site
|
|
273
|
+
`options:`, class-level `.options`, default 120 s — merged at enqueue time
|
|
274
|
+
so the runner just reads the final value from the payload:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
class HeavyImportJob
|
|
278
|
+
include Async::Background::Job
|
|
279
|
+
options timeout: 600
|
|
93
280
|
end
|
|
94
281
|
|
|
95
|
-
|
|
282
|
+
HeavyImportJob.perform_async(user_id, options: { timeout: 120 }) # wins
|
|
96
283
|
```
|
|
97
284
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
285
|
+
Side effects: an `options TEXT` column in SQLite (added idempotently via
|
|
286
|
+
`ALTER TABLE … rescue nil` on existing databases), an extensible `options:`
|
|
287
|
+
hash across the entire enqueue chain, a `Job::Options` schema via
|
|
288
|
+
`Data.define` (unknown keys raise `ArgumentError`), and queue-timeout
|
|
289
|
+
failure logs now include the actual value (`"timed out after 120s"`).
|
|
101
290
|
|
|
102
|
-
### Breaking changes from 0.7.x
|
|
103
291
|
|
|
104
|
-
None beyond what 0.7.2 already shipped. The 1.0 line locks the existing contract:
|
|
105
292
|
|
|
106
|
-
|
|
107
|
-
- All terminal `Queue::Store` methods (`complete`, `fail`, `retry_or_fail`) require the `claim_token:` kwarg and return CAS success boolean / `:retried` / `:failed` / `nil`.
|
|
108
|
-
- Schema is versioned via `PRAGMA user_version`. Use `Queue::Store.migrate!(path:)` to upgrade. Use `Queue::Store.prepare_dashboard!(path:)` from the dashboard process to lazily create dashboard-only indexes (per-status partial indexes for `done` / `failed`, plus separate `executing` and `claimed` indexes).
|
|
293
|
+
|
|
109
294
|
|
|
110
|
-
## 0.
|
|
295
|
+
## 0.6.1
|
|
111
296
|
|
|
112
|
-
|
|
113
|
-
- Add schema v1, optional dashboard indexes, and a faster enqueue path.
|
|
297
|
+
Two scheduler fixes and one notification fast path:
|
|
114
298
|
|
|
115
|
-
|
|
299
|
+
- **Cron busy-loop on overlap skip.** When a scheduled run was skipped
|
|
300
|
+
because the previous one was still active, the entry was re-pushed to the
|
|
301
|
+
heap without `reschedule`. `next_run_at` never advanced, so the next
|
|
302
|
+
iteration picked it up immediately. Skip branch now calls
|
|
303
|
+
`entry.reschedule(monotonic_now)` like the normal path.
|
|
304
|
+
- **Prepared statement reset on fetch error.** `@fetch_stmt.reset!` ran
|
|
305
|
+
after `execute` returned, so an exception inside `execute` left the
|
|
306
|
+
statement dirty and the next `fetch` could fail. Wrapped in
|
|
307
|
+
`begin / ensure`.
|
|
308
|
+
- **SocketNotifier: 1 connect per enqueue.** `notify_all` no longer
|
|
309
|
+
connects to all N worker sockets on every enqueue. Wakes a single worker
|
|
310
|
+
chosen by random offset, falls back through the ring only if the chosen
|
|
311
|
+
worker is dead. Happy path: 1 connect; worst case (all workers down): N.
|
|
312
|
+
- Pending lookup now uses a partial index
|
|
313
|
+
`idx_jobs_pending(run_at, id) WHERE status = 'pending'`. Smaller on disk,
|
|
314
|
+
cheaper to update, and matches the only query that uses it.
|
|
116
315
|
|
|
117
|
-
### Features
|
|
118
|
-
- **Tunable `Store` options via `StoreOptions`** — three knobs exposed for SQLite tuning, validated at construction time so misconfigurations fail fast at boot:
|
|
119
|
-
- `mmap` (`true`/`false`, default `true`) — toggle memory-mapped I/O
|
|
120
|
-
- `synchronous` (`:normal`/`:full`/`:extra`, default `:normal`) — durability vs throughput
|
|
121
|
-
- `wal_autocheckpoint` (`Integer` in `100..10_000`, default `1_000`) — WAL checkpoint frequency in pages
|
|
122
316
|
|
|
123
|
-
Range and enum validation prevent foot-guns (e.g. `wal_autocheckpoint: 100_000` would bloat WAL beyond `journal_size_limit`). See [Get Started → Store tuning](docs/GET_STARTED.md) for trade-offs of each knob
|
|
124
317
|
|
|
125
|
-
|
|
126
|
-
- `Store.new(path:, mmap:)` → `Store.new(path:, options: { mmap: ... })`. Direct `mmap:` keyword argument removed in favor of the unified `options:` hash. Users who construct `Store` manually (e.g. for web-worker enqueue) need to update the call site
|
|
318
|
+
|
|
127
319
|
|
|
128
|
-
## 0.6.
|
|
320
|
+
## 0.6.0
|
|
129
321
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
options timeout: 600
|
|
137
|
-
|
|
138
|
-
def perform(user_id) = # ...
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Call-site override (wins over class-level)
|
|
142
|
-
HeavyImportJob.perform_async(user_id, options: { timeout: 120 })
|
|
143
|
-
```
|
|
144
|
-
Priority: call-site `options:` → class-level `options` → `DEFAULT_TIMEOUT` (30s). Options are merged at enqueue time so the runner simply reads the final value from the payload
|
|
145
|
-
- **`options:` hash across the entire enqueue chain** — single extensible contract from `perform_async` through `Client` down to `Store`. Currently supports `:timeout`, designed to accommodate future keys (e.g. `:retry`) without API changes
|
|
146
|
-
- **`Job::Options` schema via `Data.define`** — declares known option keys with types and defaults. Unknown keys raise `ArgumentError`, invalid types raise `TypeError`. No manual validation code
|
|
147
|
-
- **`options TEXT` column in SQLite** — stores the merged options hash as JSON. Extensible without schema changes when new options are added
|
|
148
|
-
|
|
149
|
-
### Improvements
|
|
150
|
-
- **Queue timeout logged on failure** — `run_queue_job` error log now includes actual timeout value: `"timed out after 120s"` instead of generic `"timed out"`
|
|
151
|
-
- **Idempotent schema migration** — existing databases get `ALTER TABLE jobs ADD COLUMN options TEXT` on first connection, wrapped in `rescue nil` for safe re-runs. New databases include the column in `CREATE TABLE`
|
|
322
|
+
**Queue notification system rewritten.** The pipe-based `Notifier` is
|
|
323
|
+
replaced with a Unix-domain-socket architecture: each worker listens on its
|
|
324
|
+
own socket (`<dir>/async_bg_worker_N.sock`), producers broadcast wake-ups
|
|
325
|
+
via `SocketNotifier`. Fork-safe by design (no shared FDs), resilient to
|
|
326
|
+
restarts (stale-socket cleanup), and sub-100 µs wake-up latency
|
|
327
|
+
(30–80 µs typical).
|
|
152
328
|
|
|
153
|
-
|
|
329
|
+
**Why.** The pipe-based notifier was fundamentally broken in the
|
|
330
|
+
recommended multi-fork setup: `for_consumer!` closed the writer end in each
|
|
331
|
+
child, making `Client#push → notify` fail silently with `IOError`. All
|
|
332
|
+
writes hit `WRITE_DROPPED`, so the queue silently degraded to 5-second
|
|
333
|
+
polling.
|
|
154
334
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
335
|
+
**Breaking changes.** `Runner` now takes `queue_socket_dir:` instead of
|
|
336
|
+
`queue_notifier:`. `Notifier#for_producer!` / `Notifier#for_consumer!` are
|
|
337
|
+
removed. `Client#push` calls `notifier.notify_all`. Environment variable
|
|
338
|
+
`QUEUE_SOCKET_PATH` is replaced by `QUEUE_SOCKET_DIR` (a directory now).
|
|
158
339
|
|
|
159
|
-
### Improvements
|
|
160
|
-
- **SocketNotifier: non-blocking enqueue with ring fallback** — `notify_all` no longer connects to all N worker sockets on every enqueue. `UNIXSocket.new` is a blocking, non-fiber-aware syscall, and notifying every worker blocked the Falcon reactor for N `connect()` calls on the hot HTTP enqueue path. Now wakes a single worker chosen by random offset, falling back through the ring only if the chosen worker is dead (`ECONNREFUSED` etc.). Happy path: 1 connect. Worst case (all workers down): N connects — same as before, but only when actually needed. Safe because the queue is shared in SQLite, not sharded per worker
|
|
161
|
-
- **SocketNotifier: cleaned up `UNAVAILABLE` error list** — removed `IO::WaitWritable` and `Errno::EAGAIN`. They implied "socket buffer full", but `write_nonblock` of a single byte to a freshly-opened connection cannot fill the kernel buffer. Listing them only misled readers
|
|
162
|
-
- **Store: partial index for pending lookup** — replaced `idx_jobs_status_run_at_id(status, run_at, id)` with partial index `idx_jobs_pending(run_at, id) WHERE status = 'pending'`. Smaller on disk, cheaper to update, and matches the only query that uses it (`fetch`). `done`/`failed`/`running` rows no longer occupy index pages
|
|
163
340
|
|
|
164
|
-
## 0.6.0
|
|
165
341
|
|
|
166
|
-
|
|
167
|
-
- **Queue notification system completely rewritten** — replaced pipe-based `Notifier` with Unix domain socket-based architecture
|
|
168
|
-
- `Runner` now takes `queue_socket_dir:` parameter instead of `queue_notifier:`
|
|
169
|
-
- Removed `Notifier#for_producer!` and `Notifier#for_consumer!` — no longer needed
|
|
170
|
-
- `Client#push` now calls `notifier.notify_all` instead of `notifier.notify`
|
|
171
|
-
|
|
172
|
-
### Features
|
|
173
|
-
- **Unix domain socket-based notifications** — solves all cross-process notification problems
|
|
174
|
-
- New `SocketWaker` class (consumer-side) — each worker listens on its own Unix socket (`/tmp/queue/sockets/async_bg_worker_N.sock`)
|
|
175
|
-
- New `SocketNotifier` class (producer-side) — connects to all worker sockets to broadcast wake-ups
|
|
176
|
-
- **Cross-process wake-up now works correctly** — web workers → background workers, background workers → background workers
|
|
177
|
-
- **Fork-safe by design** — no shared file descriptors, each process creates its own socket after fork
|
|
178
|
-
- **Resilient to restarts** — stale socket cleanup on worker startup, graceful degradation if worker unavailable
|
|
179
|
-
- **Sub-100µs latency** — typical wake-up time 30-80µs vs previous 5-second polling fallback
|
|
180
|
-
|
|
181
|
-
### Bug Fixes
|
|
182
|
-
- **CRITICAL: Notifier bug in recommended setup** — the old pipe-based `Notifier` was fundamentally broken in multi-fork scenarios:
|
|
183
|
-
- `for_consumer!` closed the writer end in each child process, making `Client#push → notify` fail silently with `IOError`
|
|
184
|
-
- All writes were caught by `WRITE_DROPPED` rescue block, causing jobs to use 5-second polling instead of instant wake-up
|
|
185
|
-
- Web workers had no way to notify background workers (no shared pipe after fork)
|
|
186
|
-
- The bug was masked by `WRITE_DROPPED` silently catching `IOError` — appeared to work but degraded to polling
|
|
187
|
-
- **Socket cleanup race conditions** — `SocketWaker#cleanup_stale_socket` now validates if socket is truly stale by attempting connection
|
|
188
|
-
|
|
189
|
-
### Improvements
|
|
190
|
-
- Updated `docs/GET_STARTED.md` with new socket-based setup for Falcon
|
|
191
|
-
- Added section on web worker → background worker job enqueuing with full example
|
|
192
|
-
- Changed environment variable from `QUEUE_SOCKET_PATH` to `QUEUE_SOCKET_DIR` (directory instead of single socket path)
|
|
193
|
-
- Better error handling in `SocketWaker` and `SocketNotifier` with comprehensive `UNAVAILABLE` error list
|
|
194
|
-
- Integrated with `Async::Notification` for local wake-ups (shutdown signals)
|
|
195
|
-
|
|
196
|
-
### Technical Details
|
|
197
|
-
- **Why sockets over pipes?** Pipes require shared FDs across fork boundaries. The recommended Falcon setup calls `for_consumer!` in each child, which closes the writer, breaking the notification chain. Sockets use filesystem paths — any process can connect without inherited FDs.
|
|
198
|
-
- **Performance impact:** Adding ~80µs per enqueue for 8 workers (8 socket connections) vs ~100µs for SQLite transaction = negligible overhead
|
|
199
|
-
- **Graceful degradation:** If worker socket unavailable (`ENOENT`, `ECONNREFUSED`), producer silently skips — job still in database, will be picked up on next poll (5s max delay)
|
|
342
|
+
|
|
200
343
|
|
|
201
344
|
## 0.5.1
|
|
202
345
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
- Added explanatory comments for each error type and handling strategy
|
|
346
|
+
CI infrastructure: full Docker-based integration testing (`Dockerfile.ci`,
|
|
347
|
+
`docker-compose.ci.yml`, `Gemfile.ci`) plus an end-to-end scenario test that
|
|
348
|
+
validates forked-worker behavior — normal execution, crash recovery after
|
|
349
|
+
SIGKILL, no duplicate execution under crashes, proper distribution across
|
|
350
|
+
the pool.
|
|
351
|
+
|
|
352
|
+
Also: `PRAGMA busy_timeout = 5000` on `Queue::Store` to prevent
|
|
353
|
+
`SQLITE_BUSY` under concurrent multi-process access; cleaner IO error
|
|
354
|
+
categorization in `Queue::Notifier` (`WRITE_DROPPED` vs `READ_EXHAUSTED`)
|
|
355
|
+
with explanatory comments.
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
218
360
|
|
|
219
361
|
## 0.5.0
|
|
220
362
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
- `Queue::Notifier` — extracted `IO_ERRORS` constant (`IO::WaitReadable`, `EOFError`, `IOError`) for cleaner `rescue` in `drain`
|
|
243
|
-
- `Queue::Store` — replaced index `idx_jobs_status_id(status, id)` with `idx_jobs_status_run_at_id(status, run_at, id)` for efficient delayed job lookups
|
|
244
|
-
- `Queue::Store` — `fetch` SQL now uses `WHERE status = 'pending' AND run_at <= ?` with `ORDER BY run_at, id` to process jobs in scheduled order
|
|
245
|
-
- Removed duplicated `monotonic_now` / `realtime_now` from `Runner` and `Store` — now provided by `Clock` module
|
|
246
|
-
- Updated documentation: README (Job module examples, Queue architecture diagram, Clock section), GET_STARTED (delayed jobs guide, Job module usage, minimal queue-only example)
|
|
363
|
+
**Delayed jobs.** Full support for scheduling jobs in the future:
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
SomeJob.perform_in(60, *args)
|
|
367
|
+
SomeJob.perform_at(time, *args)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Backed by a new `run_at` column in the SQLite `jobs` table — jobs are only
|
|
371
|
+
fetched when `run_at <= now`.
|
|
372
|
+
|
|
373
|
+
**Job module.** Sidekiq-like `include Async::Background::Job` adds
|
|
374
|
+
`perform_async`, `perform_in`, `perform_at`, instance-level `#perform`, and
|
|
375
|
+
class-level `perform_now` delegation.
|
|
376
|
+
|
|
377
|
+
**Clock module.** Shared `monotonic_now` / `realtime_now` helpers extracted
|
|
378
|
+
to `Async::Background::Clock` and included by `Runner`, `Queue::Store`, and
|
|
379
|
+
`Queue::Client`.
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
|
|
247
384
|
|
|
248
385
|
## 0.4.5
|
|
249
386
|
|
|
250
|
-
|
|
251
|
-
|
|
387
|
+
**Fetch race condition fixed.** Wrapped `UPDATE ... RETURNING` in
|
|
388
|
+
`BEGIN IMMEDIATE` to prevent two workers from picking up the same job
|
|
389
|
+
simultaneously.
|
|
390
|
+
|
|
391
|
+
**mmap on Docker overlay2.** `overlay2` does not guarantee `write()` /
|
|
392
|
+
`mmap()` coherence, which corrupts the WAL under concurrent multi-process
|
|
393
|
+
access. mmap is now configurable via `queue_mmap: false` instead of being
|
|
394
|
+
hardcoded. Proper Docker setup with named volumes is documented in
|
|
395
|
+
[Get Started → Docker](docs/GET_STARTED.md#step-3--docker-setup).
|
|
396
|
+
|
|
397
|
+
Also: `PRAGMA optimize` on shutdown wrapped in `rescue nil`,
|
|
398
|
+
`PRAGMA incremental_vacuum` actually works now (`PRAGMA auto_vacuum =
|
|
399
|
+
INCREMENTAL` added to schema; only takes effect on new databases),
|
|
400
|
+
composite index `idx_jobs_status_id(status, id)` to eliminate a sort in
|
|
401
|
+
`fetch`. New `queue_mmap:` / `mmap:` parameters and a public
|
|
402
|
+
`attr_reader :queue_store` on `Runner`.
|
|
403
|
+
|
|
404
|
+
**Breaking-ish.** `PRAGMAS` is now a frozen lambda `PRAGMAS.call(mmap_size)`
|
|
405
|
+
instead of a static string; update any direct reference.
|
|
252
406
|
|
|
253
|
-
### Features
|
|
254
|
-
- New `queue_mmap:` parameter on `Runner` (default: `true`) — allows disabling SQLite mmap for environments where it's unsafe (Docker overlay2)
|
|
255
|
-
- New `mmap:` parameter on `Queue::Store` (default: `true`) — controls `PRAGMA mmap_size` (256 MB when enabled, 0 when disabled)
|
|
256
|
-
- Public `attr_reader :queue_store` on `Runner` — eliminates need for `instance_variable_get` when sharing Store with Client
|
|
257
407
|
|
|
258
|
-
### Bug Fixes
|
|
259
|
-
- **CRITICAL: fetch race condition** — wrapped `UPDATE ... RETURNING` in `BEGIN IMMEDIATE` transaction to prevent two workers from picking up the same job simultaneously
|
|
260
|
-
- **CRITICAL: mmap + Docker overlay2** — `overlay2` filesystem does not guarantee `write()`/`mmap()` coherence, causing SQLite WAL corruption under concurrent multi-process access. mmap is now configurable via `queue_mmap: false` instead of being hardcoded. Documented proper Docker setup with named volumes in `docs/GET_STARTED.md`
|
|
261
|
-
- **`PRAGMA optimize` on shutdown** — wrapped in `rescue nil` to prevent `SQLite3::BusyException` when another process holds the write lock during graceful shutdown
|
|
262
|
-
- **`PRAGMA incremental_vacuum` was a no-op** — added `PRAGMA auto_vacuum = INCREMENTAL` to schema. Without it, `incremental_vacuum` does nothing. Note: only takes effect on newly created databases; existing databases require a one-time `VACUUM`
|
|
263
408
|
|
|
264
|
-
|
|
265
|
-
- Replaced index `idx_jobs_status(status)` with composite `idx_jobs_status_id(status, id)` — eliminates sort step in `fetch` query (`ORDER BY id LIMIT 1` is now a direct B-tree lookup)
|
|
266
|
-
- Fixed `finalize_statements` — changed `%i[@enqueue_stmt ...]` to `%i[enqueue_stmt ...]` with `:"@#{name}"` interpolation for idiomatic `instance_variable_get`/`set` usage
|
|
267
|
-
- Added documentation: `README.md` (concise, with warning markers) and `docs/GET_STARTED.md` (step-by-step guide covering schedule config, Falcon integration, Docker setup, dynamic queue)
|
|
409
|
+
|
|
268
410
|
|
|
269
411
|
## 0.4.0
|
|
270
412
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
- `Queue::Store` — SQLite-backed persistent storage with WAL mode, prepared statements, and optimized pragmas
|
|
274
|
-
- `Queue::Notifier` — `IO.pipe`-based zero-cost wakeup between producer and consumer processes (no polling)
|
|
275
|
-
- `Queue::Client` — public API: `Async::Background::Queue.enqueue(JobClass, *args)`
|
|
276
|
-
- Automatic recovery of stale `running` jobs on worker restart
|
|
277
|
-
- Periodic cleanup of completed jobs (piggyback on fetch, every 5 minutes)
|
|
278
|
-
- `PRAGMA incremental_vacuum` when cleanup removes 100+ rows
|
|
279
|
-
- Worker isolation via `ISOLATION_FORKS` env variable — exclude specific workers from queue processing
|
|
280
|
-
- Custom database path via `queue_db_path` parameter
|
|
281
|
-
- Requires optional `sqlite3` gem (`~> 2.0`) — not included by default, must be added to Gemfile explicitly
|
|
282
|
-
- New Runner parameters: `queue_notifier:` and `queue_db_path:`
|
|
283
|
-
|
|
284
|
-
### Improvements
|
|
285
|
-
- Unified `monotonic_now` usage across `run_job` and `run_queue_job` (was using direct `Process.clock_gettime` call in `run_job`)
|
|
286
|
-
- `Queue::Notifier#drain` — moved `rescue` inside the loop to avoid stack unwinding on each drain cycle
|
|
413
|
+
**Dynamic job queue.** Enqueue jobs at runtime from any process (web,
|
|
414
|
+
console, rake) with automatic execution by background workers.
|
|
287
415
|
|
|
288
|
-
|
|
416
|
+
- `Queue::Store` — SQLite-backed persistent storage with WAL mode,
|
|
417
|
+
prepared statements, and optimized pragmas.
|
|
418
|
+
- `Queue::Notifier` — `IO.pipe`-based zero-cost wake-up between producer
|
|
419
|
+
and consumer processes.
|
|
420
|
+
- `Queue::Client` — public API: `Async::Background::Queue.enqueue
|
|
421
|
+
(JobClass, *args)`.
|
|
422
|
+
- Automatic recovery of stale `running` jobs on worker restart.
|
|
423
|
+
- Periodic cleanup of completed jobs (piggybacked on fetch, every 5 min);
|
|
424
|
+
`PRAGMA incremental_vacuum` when cleanup removes 100+ rows.
|
|
425
|
+
- `ISOLATION_FORKS` env var excludes specific workers from queue processing.
|
|
426
|
+
- Custom database path via `queue_db_path:` on `Runner`.
|
|
289
427
|
|
|
290
|
-
|
|
291
|
-
- Added optional metrics collection system using shared memory
|
|
292
|
-
- New `Metrics` class with worker-specific performance tracking
|
|
293
|
-
- Public API: `runner.metrics.enabled?`, `runner.metrics.values`, `Metrics.read_all()`
|
|
294
|
-
- Tracks total runs, successes, failures, timeouts, skips, active jobs, and execution times
|
|
295
|
-
- Requires optional `async-utilization` gem dependency
|
|
296
|
-
- Metrics stored in `/tmp/async-background.shm` with lock-free updates per worker
|
|
428
|
+
Requires the optional `sqlite3` gem (`~> 2.0`).
|
|
297
429
|
|
|
298
|
-
|
|
430
|
+
(The 0.6.0 socket-based architecture supersedes the pipe-based notifier
|
|
431
|
+
introduced here.)
|
|
299
432
|
|
|
300
|
-
### Improvements
|
|
301
|
-
- Micro-optimization in `wait_with_shutdown` method: use passed `task` parameter instead of `Async::Task.current` for better consistency and slight performance improvement
|
|
302
433
|
|
|
303
|
-
## 0.2.5
|
|
304
434
|
|
|
305
|
-
|
|
306
|
-
- Added graceful shutdown via signal handlers for SIGINT and SIGTERM
|
|
307
|
-
- Enhanced process lifecycle management with proper signal handling using `Signal.trap` and IO.pipe for async communication
|
|
308
|
-
- Improved robustness for production deployments and container orchestration
|
|
309
|
-
- Updated dependencies to work with latest Async 2.x API (removed deprecated `:parent` parameter usage)
|
|
435
|
+
|
|
310
436
|
|
|
311
|
-
## 0.
|
|
437
|
+
## 0.3.0
|
|
312
438
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
-
|
|
439
|
+
Optional metrics collection via shared memory. `Metrics` tracks per-worker
|
|
440
|
+
counters: `total_runs`, `total_successes`, `total_failures`,
|
|
441
|
+
`total_timeouts`, `total_skips`, `active_jobs`, plus last-run timestamp and
|
|
442
|
+
duration. Public API: `runner.metrics.enabled?`, `runner.metrics.values`,
|
|
443
|
+
`Metrics.read_all(total_workers:)`. Requires the optional
|
|
444
|
+
`async-utilization` gem; absent that, `enabled?` is `false` and `read_all`
|
|
445
|
+
returns `[]`. Default file: `/tmp/async-background.shm`.
|
|
319
446
|
|
|
320
|
-
## 0.2.2
|
|
321
447
|
|
|
322
|
-
### Bug Fixes
|
|
323
|
-
- **CRITICAL**: Removed logger parameter from Runner initialize (was unused). Fixed initialization to use Console.logger directly which now properly initializes in forked processes with correct context
|
|
324
448
|
|
|
325
|
-
|
|
449
|
+
|
|
326
450
|
|
|
327
|
-
|
|
328
|
-
- **CRITICAL**: Added missing `require 'console'` in main module. Logger was nil because Console gem was not imported, causing `undefined method 'info' for nil` errors on worker initialization
|
|
451
|
+
## 0.2.x
|
|
329
452
|
|
|
330
|
-
|
|
453
|
+
- **0.2.6** — `wait_with_shutdown` uses the passed `task` parameter
|
|
454
|
+
instead of `Async::Task.current`.
|
|
455
|
+
- **0.2.5** — Graceful shutdown via `SIGINT` / `SIGTERM` signal handlers
|
|
456
|
+
using `Signal.trap` and `IO.pipe`. Compatible with Async 2.x API
|
|
457
|
+
(removed deprecated `:parent`).
|
|
458
|
+
- **0.2.4** — Removed hardcoded version warning. Use semver pre-release
|
|
459
|
+
suffixes for unstable versions (e.g. `0.3.0.alpha1`).
|
|
460
|
+
- **0.2.2** — Removed unused `logger` parameter from `Runner#initialize`;
|
|
461
|
+
use `Console.logger` directly, which now initializes correctly in
|
|
462
|
+
forked processes.
|
|
463
|
+
- **0.2.1** — Added missing `require 'console'` in main module. Logger
|
|
464
|
+
was `nil`, causing `undefined method 'info' for nil` on worker
|
|
465
|
+
initialization.
|
|
466
|
+
- **0.2.0** — Removed hidden ActiveSupport dependency
|
|
467
|
+
(`safe_constantize` → `Object.const_get` + `NameError`). Job validation
|
|
468
|
+
now checks for `.perform_now` (class method) instead of `.perform`
|
|
469
|
+
(instance method). Fixed a race where an entry could disappear from the
|
|
470
|
+
heap during execution. Added `stop()` and `running?()` to `Runner`.
|
|
331
471
|
|
|
332
|
-
### Bug Fixes
|
|
333
|
-
- **CRITICAL**: Removed hidden ActiveSupport dependency. Replaced `safe_constantize` with `Object.const_get` + `NameError` handling
|
|
334
|
-
- **CRITICAL**: Fixed validator mismatch: now validates `.perform_now` (class method) instead of `.perform` (instance method)
|
|
335
|
-
- **CRITICAL**: Fixed race condition where entry could disappear from heap during execution. `reschedule` and `heap.push` now always execute after job processing
|
|
336
|
-
- Added full exception backtrace to error logs for production debugging
|
|
337
|
-
- Improved YAML security by removing `Symbol` from `permitted_classes`
|
|
338
|
-
- Removed Mutex from graceful shutdown (anti-pattern in Async). Boolean assignment is atomic in MRI
|
|
339
472
|
|
|
340
|
-
### Features
|
|
341
|
-
- Added optional `logger` parameter to Runner constructor for custom loggers (Rails.logger, etc.)
|
|
342
|
-
- Added `stop()` method for graceful shutdown
|
|
343
|
-
- Added `running?()` method to check scheduler status
|
|
344
473
|
|
|
345
|
-
|
|
346
|
-
- Job class validation now checks for `.perform_now` class method (was checking for `.perform` instance method)
|
|
474
|
+
|
|
347
475
|
|
|
348
476
|
## 0.1.0
|
|
349
477
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
-
|
|
353
|
-
-
|
|
354
|
-
-
|
|
355
|
-
-
|
|
356
|
-
-
|
|
357
|
-
-
|
|
358
|
-
-
|
|
478
|
+
Initial release.
|
|
479
|
+
|
|
480
|
+
- Single event loop with min-heap timer (`O(log N)` scheduling).
|
|
481
|
+
- Skip overlapping execution.
|
|
482
|
+
- Startup jitter to prevent thundering herd.
|
|
483
|
+
- Monotonic clock for interval jobs, wall clock for cron jobs.
|
|
484
|
+
- Deterministic worker sharding via `Zlib.crc32`.
|
|
485
|
+
- Semaphore-based concurrency control.
|
|
486
|
+
- Per-job timeout protection.
|
|
487
|
+
- Structured logging via Console.
|