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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1da83cfa99bb2a77a32b8bd72dd5bee692f94f87b453706f4443cc015e065ac4
|
|
4
|
+
data.tar.gz: b0c60214b0e13a7a0c346f5e681d78b1701a7de95e5e816f84193d6593f2c282
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cdc11bc1e0bbfb5d76e8d26bb80a71d498c3a04a0fbd1a7e3212640c3f0ae1513fb8bb18784e1dfdd7b96aafc1771ed8cf0db00ac1fedde8e63579943df1e2b
|
|
7
|
+
data.tar.gz: ed97b4b602abb304db44199247b533d2fce410ab8a3a80182e9a08ac65eff680d0df198f529ef53b87e947ccf8114b4e89e333f5695928fc1b0137b6dff90c53
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,158 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
The gem no longer depends on `async`. The same worker runs under any
|
|
6
|
+
`Fiber::Scheduler` the host installs — Falcon/Async, Itsi, or another
|
|
7
|
+
implementation.
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- `Runner#run` no longer starts a reactor. It needs an active
|
|
12
|
+
`Fiber.scheduler` and raises `Runtime::SchedulerRequired` without one:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
Async { runner.run } # Falcon / Async
|
|
16
|
+
Async::Background::Scheduler.run { runner.run } # async or itsi via ENV
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Job timeouts raise `Async::Background::Runtime::TimeoutError` instead of
|
|
20
|
+
`Async::TimeoutError`. Update rescue sites and any stored `error_class`.
|
|
21
|
+
- `Runner#drain_jobs` is bounded (`drain_timeout:` default 30s). In-flight jobs
|
|
22
|
+
that outlive it are cancelled. Pass `drain_timeout: nil` for an unbounded wait.
|
|
23
|
+
- `TaskGroup#stop_all(grace = nil)` returns whether the group drained, not a
|
|
24
|
+
count of cancelled tasks.
|
|
25
|
+
- `Async::Background::Error` is the base class for `Runtime::Error` and
|
|
26
|
+
`ConfigError`. Both are still `StandardError`.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `Async::Background::Runtime` — `Task`, `TaskGroup`, `Semaphore`,
|
|
31
|
+
`Notification`, `with_timeout`, `native_timeouts?`, `with_error_handler`.
|
|
32
|
+
- `Async::Background::Scheduler` — optional bootstrap
|
|
33
|
+
(`require "async/background/scheduler"`) that installs `async` or
|
|
34
|
+
`itsi-scheduler` from `ASYNC_BACKGROUND_SCHEDULER`. `Scheduler.preload!`
|
|
35
|
+
loads the gem before fork. `Scheduler.run` on Itsi uses the current thread;
|
|
36
|
+
`ASYNC_BACKGROUND_SCHEDULER_THREAD=1` restores a dedicated thread.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- The saturated-queue wake-up fires from `TaskGroup#on_release`, after the job
|
|
41
|
+
has left `@jobs`. Signalling from the task's `ensure` left the listener
|
|
42
|
+
seeing a full group on any scheduler that resumes `unblock` immediately.
|
|
43
|
+
- `Runtime.with_timeout` calls `scheduler.timeout_after` when the hook exists.
|
|
44
|
+
The `::Timeout.timeout` fallback uses `Thread#raise` and can hit the wrong
|
|
45
|
+
fiber; a missing hook now warns once per scheduler class.
|
|
46
|
+
- `SocketWaker#close` stops the accept loop before the self-connect that
|
|
47
|
+
unblocks it, and hangs up tracked client sockets. A fiber parked in
|
|
48
|
+
`IO#wait_readable` is not cancellable via `Task#stop` on a scheduler without
|
|
49
|
+
`#fiber_interrupt`.
|
|
50
|
+
- `Runtime.error_handler` is scoped per runner (`on_error:` /
|
|
51
|
+
`with_error_handler`). A process-global handler was overwritten by the next
|
|
52
|
+
runner and never restored.
|
|
53
|
+
- A failure awaited via `Task#wait` is no longer also sent to the error handler.
|
|
54
|
+
- `Semaphore.new(0)` raises `ArgumentError` instead of deadlocking on acquire.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- `async` is a development dependency. Runtime gems are `console`, `fugit`
|
|
59
|
+
and `base64`.
|
|
60
|
+
- `SocketWaker#start_accept_loop` no longer needs a parent task (the
|
|
61
|
+
argument is still accepted and ignored).
|
|
62
|
+
- Shutdown closes the waker before the store, so the listener cannot be inside
|
|
63
|
+
`Store#fetch` when the connection disappears.
|
|
64
|
+
|
|
65
|
+
### Unchanged
|
|
66
|
+
|
|
67
|
+
- Dashboard, `perform_async` / `perform_in` / `perform_at`, SQLite schema,
|
|
68
|
+
and the cross-process wake protocol.
|
|
69
|
+
|
|
70
|
+
## 1.0.2
|
|
71
|
+
|
|
72
|
+
Queue maintenance bug fix plus profiler-driven work on the hot paths that
|
|
73
|
+
actually showed up in StackProf: the sqlite3 statement wrapper, transaction
|
|
74
|
+
control, and the socket notifier.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- `Store#cleanup_finished_jobs` tested `@db.changes` after running *both*
|
|
79
|
+
DELETEs. `sqlite3_changes()` reports only the most recent statement, so the
|
|
80
|
+
incremental-vacuum decision saw the failed-job count alone and ignored every
|
|
81
|
+
deleted done-job. On a busy queue, where done-jobs vastly outnumber failed
|
|
82
|
+
ones, the vacuum effectively never ran and the database file grew without
|
|
83
|
+
bound. Both counts are now summed.
|
|
84
|
+
- `PRAGMA incremental_vacuum` ran without a page limit, releasing every free
|
|
85
|
+
page in a single blocking call — an unbounded reactor stall proportional to
|
|
86
|
+
the accumulated free list. Now capped at 64 pages per call
|
|
87
|
+
(`SQL::INCREMENTAL_VACUUM_PAGES`).
|
|
88
|
+
- `SocketWaker` signalled its notification only from the `ensure` block, so a
|
|
89
|
+
wake-up was really driven by the client disconnecting rather than by the
|
|
90
|
+
wake byte arriving. It happened to work because `SocketNotifier` closes
|
|
91
|
+
immediately after writing, but it made the protocol depend on a disconnect.
|
|
92
|
+
The signal now fires for every byte received; the `ensure` still signals so a
|
|
93
|
+
disconnect racing with a read cannot drop a wake-up.
|
|
94
|
+
|
|
95
|
+
### Performance
|
|
96
|
+
|
|
97
|
+
Measured on the `enqueue_stress` CI scenario (2.1M inserts / 30s, two producer
|
|
98
|
+
processes). `SQLite3::Statement#step` accounts for ~82% of producer wall time
|
|
99
|
+
and is irreducible; the changes below target what surrounded it.
|
|
100
|
+
|
|
101
|
+
- `Store#enqueue` and `Store#fetch` bind and step their prepared statements
|
|
102
|
+
directly instead of calling `Statement#execute`. The wrapper built a splat
|
|
103
|
+
array, ran `Array#flatten` over it and allocated a `ResultSet` — roughly 8%
|
|
104
|
+
of producer wall time, and `Array#flatten` alone was 5.8% of all object
|
|
105
|
+
allocations. Behaviour is unchanged: `execute` only steps when
|
|
106
|
+
`column_count == 0`, which is exactly what the INSERT path needed, and the
|
|
107
|
+
`UPDATE ... RETURNING` fetch still consumes a single row.
|
|
108
|
+
- `SocketNotifier` caches its socket paths at construction. It previously ran
|
|
109
|
+
`File.join` plus a string interpolation on every enqueue attempt —
|
|
110
|
+
13.3% of allocations in the normal scenario.
|
|
111
|
+
- `SocketNotifier` remembers unreachable workers for `DEAD_WORKER_TTL` (5s)
|
|
112
|
+
instead of reconnecting to them on every enqueue. Because the scan started at
|
|
113
|
+
a random index, a dead worker was re-probed indefinitely and each attempt
|
|
114
|
+
raised and swallowed an `Errno`; `SystemCallError#initialize` alone was 6.8%
|
|
115
|
+
of allocations. A worker that starts during the TTL window is still picked up
|
|
116
|
+
by the listener's own polling fallback.
|
|
117
|
+
- `SocketNotifier` rotates its scan start with a cursor rather than calling
|
|
118
|
+
`rand` per enqueue, which also spreads wake-ups deterministically.
|
|
119
|
+
- `Errno::EAGAIN` on the wake byte (`IO::WaitWritable`) is now treated as
|
|
120
|
+
success rather than falling through to the generic rescue and logging a
|
|
121
|
+
warning: a full send buffer means the worker already has unread wake-ups.
|
|
122
|
+
- `Store#transaction` runs `BEGIN IMMEDIATE`, `COMMIT` and `ROLLBACK` as
|
|
123
|
+
prepared statements. They previously went through `Database#execute`, which
|
|
124
|
+
compiles a fresh `Statement` and builds a `ResultSet` on every call: 26
|
|
125
|
+
allocations per BEGIN+COMMIT pair against 2 prepared, and 6x the wall time
|
|
126
|
+
over 30k pairs. Every `fetch` paid this twice, and it was 11.8% of worker
|
|
127
|
+
allocations in the object profile.
|
|
128
|
+
- `mark_started!`, `complete`, `fail`, `retry_job!`, `recover`,
|
|
129
|
+
`stored_options_for`, `lease_alive?`, `next_pending_run_at` and the two
|
|
130
|
+
cleanup DELETEs now bind and step directly, like `enqueue` and `fetch`
|
|
131
|
+
already did. `Statement#execute` is gone from the Store entirely;
|
|
132
|
+
`Database#execute` remains only on the cold pragma path. This is what was
|
|
133
|
+
still leaving `Array#flatten` at 4.9% of worker allocations and
|
|
134
|
+
`Statement#execute` at 10.8% of worker wall time.
|
|
135
|
+
- Combined effect on a full job cycle (enqueue omitted; fetch + mark_started +
|
|
136
|
+
complete, 8000 jobs, measured twice): **49 -> 18 allocations per job**, wall
|
|
137
|
+
time down roughly 10-20% depending on run.
|
|
138
|
+
- `@db.changes` and `@db.last_insert_row_id` are read after the statement is
|
|
139
|
+
reset; both were verified to survive `sqlite3_reset()`, so the claim-token
|
|
140
|
+
lease checks behave exactly as before (valid token true, stale token false,
|
|
141
|
+
repeat completion false).
|
|
142
|
+
|
|
143
|
+
### Notes
|
|
144
|
+
|
|
145
|
+
- `SocketNotifier` still opens a fresh connection per notification. This is
|
|
146
|
+
the largest remaining win and it is paid on both sides: 8.5% of producer wall
|
|
147
|
+
time in the scenario where workers are actually up, plus 16.4% of worker
|
|
148
|
+
allocations, because every connection makes `SocketWaker#handle_client` spawn
|
|
149
|
+
a fresh `Async::Task` and fiber. Persistent connections require the
|
|
150
|
+
`SocketWaker` fix above to be deployed on every worker first — a 1.0.2 producer holding a connection open
|
|
151
|
+
to a 1.0.1 worker would never wake it, silently degrading queue latency to
|
|
152
|
+
the 5s poll interval. Deferred until 1.0.2 is the deployed floor.
|
|
153
|
+
- No schema change; `Schema::VERSION` is untouched and 1.0.2 is a drop-in
|
|
154
|
+
replacement for 1.0.1 on an existing database.
|
|
155
|
+
|
|
3
156
|
## 1.0.1
|
|
4
157
|
|
|
5
158
|
Dashboard security headers and a fiber-native rewrite of the SSE stream.
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Async::Background
|
|
2
2
|
|
|
3
|
-
A lightweight cron, interval, and job-queue scheduler for Ruby
|
|
4
|
-
[
|
|
5
|
-
[
|
|
3
|
+
A lightweight cron, interval, and job-queue scheduler for Ruby fibers.
|
|
4
|
+
Works with any `Fiber::Scheduler` — [Falcon](https://github.com/socketry/falcon) /
|
|
5
|
+
[Async](https://github.com/socketry/async), [Itsi](https://itsi.fyi), or your own.
|
|
6
6
|
|
|
7
7
|
- **Cron & interval scheduling** on a single event loop with a min-heap.
|
|
8
8
|
- **Dynamic job queue** backed by SQLite, with delayed jobs
|
|
@@ -15,11 +15,11 @@ A lightweight cron, interval, and job-queue scheduler for Ruby's
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
## Why
|
|
18
|
+
## Why fibers?
|
|
19
19
|
|
|
20
|
-
The whole gem is built around the assumption that
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
The whole gem is built around the assumption that the host schedules many
|
|
21
|
+
fibers on top of one OS thread per process — so the dashboard's SSE stream,
|
|
22
|
+
the cron scheduler, and the queue worker all share that one thread
|
|
23
23
|
cooperatively. A blocked fiber yields; a blocked thread doesn't.
|
|
24
24
|
|
|
25
25
|

|
|
@@ -30,13 +30,31 @@ open tab.
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## Fiber scheduler
|
|
34
|
+
|
|
35
|
+
`Runner#run` does not start a reactor. The host must install a
|
|
36
|
+
`Fiber.scheduler` first:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
Async { runner.run } # Falcon / Async
|
|
40
|
+
|
|
41
|
+
require "async/background/scheduler"
|
|
42
|
+
Async::Background::Scheduler.run { runner.run } # ASYNC_BACKGROUND_SCHEDULER=async|itsi
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Without a scheduler it raises `Runtime::SchedulerRequired`.
|
|
46
|
+
Worker setup for Falcon and Itsi is in [Get started](docs/GET_STARTED.md).
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
33
50
|
## Requirements
|
|
34
51
|
|
|
35
52
|
| Dependency | Version | Required? |
|
|
36
53
|
| -------------------- | ---------------- | -------------------- |
|
|
37
54
|
| Ruby | `>= 3.3` | yes |
|
|
38
|
-
| `
|
|
55
|
+
| a `Fiber::Scheduler` | `async ~> 2.0`, `itsi-scheduler`, … | yes — installed by the host, not by this gem |
|
|
39
56
|
| `fugit` | `~> 1.0` | yes |
|
|
57
|
+
| `console` | `~> 1.0` | yes |
|
|
40
58
|
| `sqlite3` | `~> 2.0` | for the queue & dashboard |
|
|
41
59
|
| `async-utilization` | `>= 0.3, < 0.5` | for metrics |
|
|
42
60
|
|
|
@@ -48,16 +66,16 @@ open tab.
|
|
|
48
66
|
# Gemfile
|
|
49
67
|
gem "async-background"
|
|
50
68
|
|
|
51
|
-
gem "
|
|
52
|
-
gem "
|
|
69
|
+
gem "async", "~> 2.0" # or `itsi-scheduler`
|
|
70
|
+
gem "sqlite3", "~> 2.0" # queue / dashboard
|
|
71
|
+
gem "async-utilization", ">= 0.3", "< 0.5" # optional metrics
|
|
53
72
|
```
|
|
54
73
|
|
|
55
74
|
---
|
|
56
75
|
|
|
57
76
|
## ➡️ [Get started](docs/GET_STARTED.md)
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
delayed jobs.
|
|
78
|
+
Schedule config, Falcon or Itsi workers, Docker, queue, delayed jobs.
|
|
61
79
|
|
|
62
80
|
---
|
|
63
81
|
|
|
@@ -120,9 +138,12 @@ jobs use wall-clock time, because "every day at 3am" needs to mean 3am.
|
|
|
120
138
|
|
|
121
139
|
## How it works
|
|
122
140
|
|
|
123
|
-
A single
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
A single fiber sleeps until the next entry is due, then dispatches it under a
|
|
142
|
+
semaphore that caps concurrency. Overlapping ticks are skipped and rescheduled.
|
|
143
|
+
Every dispatched job is tracked in a `Runtime::TaskGroup`, so shutdown drains
|
|
144
|
+
in-flight work before the SQLite store and the wake-up socket are closed. The
|
|
145
|
+
drain is bounded by `drain_timeout:` (30s by default; pass `nil` for the old
|
|
146
|
+
unbounded wait) so one wedged job cannot turn SIGTERM into SIGKILL.
|
|
126
147
|
|
|
127
148
|
```
|
|
128
149
|
schedule.yml → build_heap → MinHeap<Entry> → scheduler loop → Semaphore → run_job
|
data/async-background.gemspec
CHANGED
|
@@ -8,12 +8,13 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ['Roman Hajdarov']
|
|
9
9
|
spec.email = ['romnhajdarov@gmail.com']
|
|
10
10
|
|
|
11
|
-
spec.summary = 'Lightweight heap-based cron/interval scheduler for
|
|
12
|
-
spec.description = 'A production-grade lightweight scheduler built on
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
11
|
+
spec.summary = 'Lightweight heap-based cron/interval scheduler for any Fiber scheduler.'
|
|
12
|
+
spec.description = 'A production-grade lightweight background job scheduler built on the ' \
|
|
13
|
+
'Fiber::Scheduler interface itself, not on any one implementation: it ' \
|
|
14
|
+
'runs under Async/Falcon, Itsi, or anything else that installs a ' \
|
|
15
|
+
'scheduler. Single event loop with min-heap timer, skip-overlapping ' \
|
|
16
|
+
'execution, jitter, monotonic clock intervals, semaphore concurrency ' \
|
|
17
|
+
'control, and deterministic worker sharding.'
|
|
17
18
|
|
|
18
19
|
spec.homepage = 'https://github.com/roman-haidarov/async-background'
|
|
19
20
|
spec.license = 'MIT'
|
|
@@ -30,15 +31,20 @@ Gem::Specification.new do |spec|
|
|
|
30
31
|
|
|
31
32
|
spec.required_ruby_version = '>= 3.3'
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
# No scheduler dependency on purpose. The gem uses the Fiber::Scheduler
|
|
35
|
+
# interface (Fiber.schedule, #block/#unblock, Timeout) and requires the host
|
|
36
|
+
# to install an implementation.
|
|
34
37
|
spec.add_dependency 'console', '~> 1.0'
|
|
35
38
|
spec.add_dependency 'fugit', '~> 1.0'
|
|
36
39
|
spec.add_dependency 'base64', '~> 0.2'
|
|
37
40
|
|
|
38
41
|
# Optional: add to your own Gemfile if you need these features
|
|
42
|
+
# gem 'async', '~> 2.0' # or any other Fiber scheduler
|
|
43
|
+
# gem 'itsi-scheduler' # ... such as this one
|
|
39
44
|
# gem 'sqlite3', '~> 2.0'
|
|
40
45
|
# gem 'async-utilization', '>= 0.3', '< 0.5' # shared-memory worker metrics
|
|
41
46
|
|
|
47
|
+
spec.add_development_dependency 'async', '~> 2.0'
|
|
42
48
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
43
49
|
spec.add_development_dependency 'rspec', '~> 3.12'
|
|
44
50
|
spec.add_development_dependency 'rack', '~> 3.0'
|
|
@@ -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)
|
|
@@ -29,11 +29,9 @@ module Async
|
|
|
29
29
|
|
|
30
30
|
enable_incremental_vacuum!(db) unless jobs_table?(db)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
upgrade!(db) unless current?(db)
|
|
36
|
-
end
|
|
32
|
+
synchronized_change(db) do
|
|
33
|
+
reject_future_version!(db)
|
|
34
|
+
upgrade!(db) unless current?(db)
|
|
37
35
|
end
|
|
38
36
|
end
|
|
39
37
|
|
|
@@ -41,20 +39,20 @@ module Async
|
|
|
41
39
|
migrate!(db)
|
|
42
40
|
return if dashboard_indexes_current?(db)
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
create_dashboard_indexes!(db) unless dashboard_indexes_current?(db)
|
|
47
|
-
end
|
|
42
|
+
synchronized_change(db) do
|
|
43
|
+
create_dashboard_indexes!(db) unless dashboard_indexes_current?(db)
|
|
48
44
|
end
|
|
49
45
|
end
|
|
50
46
|
|
|
47
|
+
def synchronized_change(db, &change)
|
|
48
|
+
with_migration_timeout(db) { immediate_transaction(db, &change) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
51
|
def current?(db)
|
|
52
52
|
jobs_table?(db) && version(db) == VERSION && core_indexes_current?(db)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def dashboard_indexes_current?(db)
|
|
56
|
-
(DASHBOARD_INDEXES - index_names(db)).empty?
|
|
57
|
-
end
|
|
55
|
+
def dashboard_indexes_current?(db) = indexes_present?(db, DASHBOARD_INDEXES)
|
|
58
56
|
|
|
59
57
|
def version(db)
|
|
60
58
|
db.get_first_value(SQL::USER_VERSION).to_i
|
|
@@ -101,8 +99,10 @@ module Async
|
|
|
101
99
|
SQL::CREATE_DASHBOARD_INDEXES.each { |statement| db.execute(statement) }
|
|
102
100
|
end
|
|
103
101
|
|
|
104
|
-
def core_indexes_current?(db)
|
|
105
|
-
|
|
102
|
+
def core_indexes_current?(db) = indexes_present?(db, CORE_INDEXES)
|
|
103
|
+
|
|
104
|
+
def indexes_present?(db, names)
|
|
105
|
+
(names - index_names(db)).empty?
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def jobs_table?(db)
|
|
@@ -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
|