async-background 0.7.2 → 1.0.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 +108 -1
- data/README.md +13 -7
- data/async-background.gemspec +3 -1
- data/lib/async/background/metrics.rb +3 -1
- data/lib/async/background/queue/schema.rb +6 -1
- data/lib/async/background/queue/sql.rb +15 -4
- data/lib/async/background/runner/schedule.rb +2 -0
- data/lib/async/background/runner.rb +17 -2
- data/lib/async/background/version.rb +1 -1
- data/lib/async/background/web/app.rb +138 -0
- data/lib/async/background/web/assets.rb +726 -0
- data/lib/async/background/web/auth.rb +19 -0
- data/lib/async/background/web/configuration.rb +158 -0
- data/lib/async/background/web/cursor.rb +58 -0
- data/lib/async/background/web/errors.rb +14 -0
- data/lib/async/background/web/event_hub.rb +194 -0
- data/lib/async/background/web/metrics_reader.rb +96 -0
- data/lib/async/background/web/request.rb +36 -0
- data/lib/async/background/web/response.rb +85 -0
- data/lib/async/background/web/router.rb +30 -0
- data/lib/async/background/web/serializer.rb +154 -0
- data/lib/async/background/web/snapshot.rb +247 -0
- data/lib/async/background/web/sql.rb +88 -0
- data/lib/async/background/web/stream.rb +43 -0
- data/lib/async/background/web.rb +52 -0
- metadata +46 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d7244ccfb7156c423229ed59f36df9354d293711bab53e6a87d943743bc7f44
|
|
4
|
+
data.tar.gz: 84752c52033b89c5b957d39976f5d025a56215b378138eda8813c05635c5d58e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e90dd0f6be9ff7361aba7d84e7a7cdfdd712b351938c50dc8b84358d10a20f65514880a3eac3ab2ad8c0adf5164c06f9668ead03df5161fc4482b4e27aa03ec
|
|
7
|
+
data.tar.gz: 1867b17975c63966aeaf437414795b3b5d9083c9871a9f6c44dc6b7280651bbf46a8c929b4751349f969ffbdf72c32aeb8c6965634d149e74a0a352e4ef958df
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,112 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Dashboard SSE hardening
|
|
6
|
+
|
|
7
|
+
- Make SSE the default dashboard transport. `:polling` remains an explicit compatibility fallback.
|
|
8
|
+
- Replace one `PRAGMA data_version` loop **per browser connection** with one shared `EventHub` watcher per Rack app process while at least one dashboard tab is connected. It fans complete overview snapshots to every SSE body through latest-value mailboxes, so slow tabs do not accumulate unbounded event queues.
|
|
9
|
+
- Use complete snapshots on connect and after a change rather than a replay log: reconnecting EventSource clients cannot miss the current queue state.
|
|
10
|
+
- Coalesce client-side list refreshes after a burst of changes, cancel stale list fetches on tab switches, and retain previous pages when `Load more` is used.
|
|
11
|
+
- Add SSE retry control, 25-second heartbeat frames, `no-cache, no-transform`, and `X-Accel-Buffering: no`. Remove the hop-by-hop `Connection` response header.
|
|
12
|
+
- Fingerprint asset URLs and cache immutable assets by digest, so a dashboard deploy cannot leave a browser on incompatible HTML/JS/CSS.
|
|
13
|
+
- Add coverage for event fan-out, latest-value coalescing, clean stream shutdown, forced overview reads, and the SSE configuration constraints.
|
|
14
|
+
|
|
15
|
+
## 1.1.0
|
|
16
|
+
|
|
17
|
+
Server-Sent Events transport for the dashboard. Replaces HTTP polling as the recommended transport.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **SSE transport for the dashboard.** Set `c.transport = :sse` in `Async::Background::Web.configure` and the dashboard now uses a single long-lived `text/event-stream` connection per browser tab instead of polling `/api/overview` every 2 seconds. The browser opens `EventSource(mount_path + '/api/stream')` once; the server pushes an `overview` event whenever `PRAGMA data_version` changes, and a `:keepalive` comment frame every 30 seconds. Result: 1 HTTP connection per dashboard tab regardless of how long it stays open, instead of 30 req/min per tab.
|
|
22
|
+
|
|
23
|
+
- New module `Async::Background::Web::Stream` implements the event loop as a Rack streaming body (responds to `#each`, yields SSE frames). Holds no state across requests.
|
|
24
|
+
- New route `GET /api/stream` returns `200 text/event-stream` when `transport == :sse`, `404` otherwise. Subject to the same auth gate as every other endpoint.
|
|
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.
|
|
27
|
+
|
|
28
|
+
- **`Configuration#transport`** with default `:polling` (backward compatible) and accepted values `:polling | :sse`. Validation rejects anything else with `ConfigurationError`. The chosen transport is exposed at `/api/config` so the client knows which path to take.
|
|
29
|
+
|
|
30
|
+
### Migration
|
|
31
|
+
|
|
32
|
+
Existing deployments keep working unchanged. To opt into SSE:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Async::Background::Web.configure do |c|
|
|
36
|
+
c.queue_path = ...
|
|
37
|
+
c.auth = ->(env) { ... }
|
|
38
|
+
c.transport = :sse
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Server compatibility note
|
|
43
|
+
|
|
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
|
+
|
|
46
|
+
### Backend-side polling
|
|
47
|
+
|
|
48
|
+
The server still polls `PRAGMA data_version` every 500ms inside the snapshot connection to detect changes. This is a connection-local PRAGMA call, microseconds, never hits a rate limiter. Client-facing transport is push.
|
|
49
|
+
|
|
50
|
+
### Tests
|
|
51
|
+
|
|
52
|
+
- New `spec/async/background/web/stream_spec.rb` — covers overview event on data_version change, heartbeat after idle, graceful exit on `EPIPE`/`IOError`, error frame on `ClosedError`/`UnavailableError`.
|
|
53
|
+
- Extended `spec/async/background/web/app_spec.rb` — `/api/stream` returns 404 on polling default, 200 text/event-stream on `:sse`, 401 without auth.
|
|
54
|
+
- Extended `spec/async/background/web/configuration_spec.rb` — accepts `:sse`, rejects unknown transports.
|
|
55
|
+
|
|
56
|
+
## 1.0.0
|
|
57
|
+
|
|
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
|
+
|
|
60
|
+
### Features
|
|
61
|
+
|
|
62
|
+
- **Web dashboard.** Rack-mountable read-only UI under `require 'async/background/web'`. Vanilla HTML/CSS/JS, no JS framework, no npm.
|
|
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.
|
|
71
|
+
|
|
72
|
+
- Add the optional Rack dashboard for the SQLite queue.
|
|
73
|
+
- Make sqlite3 an explicit runtime dependency for queue/dashboard installs.
|
|
74
|
+
|
|
75
|
+
### Configuration
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
require 'async/background/web'
|
|
79
|
+
|
|
80
|
+
Async::Background::Queue::Store.prepare_dashboard!(path: '/var/lib/app/queue.db')
|
|
81
|
+
|
|
82
|
+
Async::Background::Web.configure do |c|
|
|
83
|
+
c.queue_path = '/var/lib/app/queue.db'
|
|
84
|
+
c.auth = ->(env) { env['warden'].user&.admin? }
|
|
85
|
+
c.expose_args = false
|
|
86
|
+
c.metrics_path = '/run/app/async-background.shm'
|
|
87
|
+
c.total_workers = 4
|
|
88
|
+
c.counts_cache_ttl = 3.0
|
|
89
|
+
c.poll_interval_ms = 2000
|
|
90
|
+
c.list_limit = 50
|
|
91
|
+
c.mount_path = '/admin/background'
|
|
92
|
+
c.title = 'My App background jobs'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
run Async::Background::Web.app
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Dependencies
|
|
99
|
+
|
|
100
|
+
- `rack` is an optional dependency. Required only when `require 'async/background/web'` is loaded. Core gem and worker processes do not require it.
|
|
101
|
+
|
|
102
|
+
### Breaking changes from 0.7.x
|
|
103
|
+
|
|
104
|
+
None beyond what 0.7.2 already shipped. The 1.0 line locks the existing contract:
|
|
105
|
+
|
|
106
|
+
- `Queue::Store#fetch` returns `claim_token` in the result hash.
|
|
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).
|
|
109
|
+
|
|
3
110
|
## 0.7.2
|
|
4
111
|
|
|
5
112
|
- Harden queue execution, retries, shutdown, and metrics.
|
|
@@ -102,7 +209,7 @@
|
|
|
102
209
|
- Proper job distribution validation across worker pool
|
|
103
210
|
- **Test fixtures** — dedicated `ci/fixtures/jobs.rb` and `ci/fixtures/schedule.yml` for scenario testing
|
|
104
211
|
|
|
105
|
-
### Bug Fixes
|
|
212
|
+
### Bug Fixes
|
|
106
213
|
- **SQLite busy timeout** — added `PRAGMA busy_timeout = 5000` to `Queue::Store` to prevent `SQLITE_BUSY` errors under concurrent multi-process database access
|
|
107
214
|
- **Enhanced Queue::Notifier error handling** — restructured IO error handling with clearer categorization:
|
|
108
215
|
- `WRITE_DROPPED` for write failures (`IO::WaitWritable`, `Errno::EAGAIN`, `IOError`, `Errno::EPIPE`) — all non-fatal as job is already in store
|
data/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A lightweight cron, interval, and job-queue scheduler for Ruby's [Async](https:/
|
|
|
12
12
|
|
|
13
13
|
- Ruby >= 3.3
|
|
14
14
|
- `async ~> 2.0`, `fugit ~> 1.0`
|
|
15
|
-
- `sqlite3 ~> 2.0` (optional,
|
|
15
|
+
- `sqlite3 ~> 2.0` (optional, storage)
|
|
16
16
|
- `async-utilization >= 0.3, < 0.5` (optional, for metrics)
|
|
17
17
|
|
|
18
18
|
## Install
|
|
@@ -20,7 +20,7 @@ A lightweight cron, interval, and job-queue scheduler for Ruby's [Async](https:/
|
|
|
20
20
|
```ruby
|
|
21
21
|
# Gemfile
|
|
22
22
|
gem "async-background"
|
|
23
|
-
gem "sqlite3", "~> 2.0"
|
|
23
|
+
gem "sqlite3", "~> 2.0" # optional
|
|
24
24
|
gem "async-utilization", ">= 0.3", "< 0.5" # optional
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -126,6 +126,12 @@ The dynamic queue runs alongside it:
|
|
|
126
126
|
|
|
127
127
|
Jobs are persisted in SQLite, so a missed wake-up is never a lost job — workers also poll every 5 seconds as a safety net.
|
|
128
128
|
|
|
129
|
+
### Queue-only workers
|
|
130
|
+
|
|
131
|
+
Recurring schedules are optional. A worker that serves only dynamic jobs starts with
|
|
132
|
+
`config_path: nil` and a `queue_socket_dir`; it does not need a placeholder schedule file.
|
|
133
|
+
A supplied schedule path stays strict and raises when the file is missing or empty.
|
|
134
|
+
|
|
129
135
|
### Schema migration during deploy
|
|
130
136
|
|
|
131
137
|
Run queue migrations once in the release/pre-deploy step, before starting new web or worker
|
|
@@ -140,18 +146,18 @@ A fresh database still self-initializes on first use for local development, but
|
|
|
140
146
|
migration is the production path. For an existing queue, finish or stop 0.7.1 producers/workers,
|
|
141
147
|
run the migration once, then start 0.7.2 processes.
|
|
142
148
|
|
|
143
|
-
###
|
|
149
|
+
### Dashboard indexes
|
|
144
150
|
|
|
145
151
|
The queue does **not** install dashboard indexes by default. They slow every enqueue even though
|
|
146
|
-
pending rows never enter terminal or in-flight read-model indexes.
|
|
147
|
-
|
|
152
|
+
pending rows never enter terminal or in-flight read-model indexes. Enable them once before
|
|
153
|
+
mounting the dashboard:
|
|
148
154
|
|
|
149
155
|
```ruby
|
|
150
156
|
Async::Background::Queue.prepare_dashboard!(path: ENV.fetch("QUEUE_DB_PATH"))
|
|
151
157
|
```
|
|
152
158
|
|
|
153
|
-
It adds
|
|
154
|
-
|
|
159
|
+
It adds four compact indexes: cursor-sorted `done` / `failed` history plus separate
|
|
160
|
+
`executing` / `claimed` in-flight lists. It does not change queue behavior or rerun the core migration.
|
|
155
161
|
|
|
156
162
|
## Metrics
|
|
157
163
|
|
data/async-background.gemspec
CHANGED
|
@@ -33,12 +33,14 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.add_dependency 'async', '~> 2.0'
|
|
34
34
|
spec.add_dependency 'console', '~> 1.0'
|
|
35
35
|
spec.add_dependency 'fugit', '~> 1.0'
|
|
36
|
+
spec.add_dependency 'base64', '~> 0.2'
|
|
36
37
|
|
|
37
38
|
# Optional: add to your own Gemfile if you need these features
|
|
38
|
-
# gem 'sqlite3',
|
|
39
|
+
# gem 'sqlite3', '~> 2.0'
|
|
39
40
|
# gem 'async-utilization', '>= 0.3', '< 0.5' # shared-memory worker metrics
|
|
40
41
|
|
|
41
42
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
42
43
|
spec.add_development_dependency 'rspec', '~> 3.12'
|
|
44
|
+
spec.add_development_dependency 'rack', '~> 3.0'
|
|
43
45
|
spec.add_development_dependency 'async-utilization', '>= 0.3', '< 0.5'
|
|
44
46
|
end
|
|
@@ -191,10 +191,12 @@ module Async
|
|
|
191
191
|
|
|
192
192
|
def ensure_shm!(total_workers, path)
|
|
193
193
|
required_size = self.class.segment_size * total_workers
|
|
194
|
+
page_size = IO::Buffer::PAGE_SIZE
|
|
195
|
+
mapped_size = ((required_size + page_size - 1) / page_size) * page_size
|
|
194
196
|
|
|
195
197
|
File.open(path, File::CREAT | File::RDWR, 0o644) do |file|
|
|
196
198
|
file.flock(File::LOCK_EX)
|
|
197
|
-
file.truncate(
|
|
199
|
+
file.truncate(mapped_size) if file.size < mapped_size
|
|
198
200
|
ensure
|
|
199
201
|
file.flock(File::LOCK_UN) rescue nil
|
|
200
202
|
end
|
|
@@ -13,7 +13,12 @@ module Async
|
|
|
13
13
|
VERSION = 1
|
|
14
14
|
MIGRATION_BUSY_TIMEOUT_MS = 30_000
|
|
15
15
|
CORE_INDEXES = %w[idx_jobs_pending].freeze
|
|
16
|
-
DASHBOARD_INDEXES = %w[
|
|
16
|
+
DASHBOARD_INDEXES = %w[
|
|
17
|
+
idx_jobs_done_finished_at
|
|
18
|
+
idx_jobs_failed_finished_at
|
|
19
|
+
idx_jobs_executing_started_at
|
|
20
|
+
idx_jobs_claimed_locked_at
|
|
21
|
+
].freeze
|
|
17
22
|
REQUIRED_INDEXES = CORE_INDEXES
|
|
18
23
|
|
|
19
24
|
module_function
|
|
@@ -192,13 +192,24 @@ module Async
|
|
|
192
192
|
WHERE status = 'failed'
|
|
193
193
|
SQL
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
CREATE INDEX IF NOT EXISTS
|
|
195
|
+
CREATE_EXECUTING_INDEX = <<~SQL.freeze
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_executing_started_at
|
|
197
|
+
ON jobs(started_at)
|
|
198
|
+
WHERE status = 'running' AND started_at IS NOT NULL
|
|
199
|
+
SQL
|
|
200
|
+
|
|
201
|
+
CREATE_CLAIMED_INDEX = <<~SQL.freeze
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_claimed_locked_at
|
|
197
203
|
ON jobs(locked_at)
|
|
198
|
-
WHERE status = 'running'
|
|
204
|
+
WHERE status = 'running' AND started_at IS NULL
|
|
199
205
|
SQL
|
|
200
206
|
|
|
201
|
-
CREATE_DASHBOARD_INDEXES = [
|
|
207
|
+
CREATE_DASHBOARD_INDEXES = [
|
|
208
|
+
CREATE_DONE_INDEX,
|
|
209
|
+
CREATE_FAILED_INDEX,
|
|
210
|
+
CREATE_EXECUTING_INDEX,
|
|
211
|
+
CREATE_CLAIMED_INDEX
|
|
212
|
+
].freeze
|
|
202
213
|
end
|
|
203
214
|
end
|
|
204
215
|
end
|
|
@@ -29,8 +29,11 @@ module Async
|
|
|
29
29
|
:metrics,
|
|
30
30
|
:queue_store
|
|
31
31
|
|
|
32
|
+
# `config_path: nil` explicitly disables recurring jobs. This keeps the
|
|
33
|
+
# dynamic SQLite queue usable on its own; a supplied path remains strict
|
|
34
|
+
# so a typo cannot silently disable scheduled work.
|
|
32
35
|
def initialize(
|
|
33
|
-
config_path
|
|
36
|
+
config_path: nil,
|
|
34
37
|
job_count: 2,
|
|
35
38
|
worker_index:,
|
|
36
39
|
total_workers:,
|
|
@@ -53,8 +56,9 @@ module Async
|
|
|
53
56
|
|
|
54
57
|
@drain_barrier = ::Async::Barrier.new
|
|
55
58
|
@semaphore = ::Async::Semaphore.new(job_count, parent: @drain_barrier)
|
|
56
|
-
@heap = build_heap(config_path)
|
|
59
|
+
@heap = config_path.nil? ? MinHeap.new : build_heap(config_path)
|
|
57
60
|
setup_queue(queue_socket_dir, queue_db_path, queue_mmap)
|
|
61
|
+
validate_work_source!(config_path)
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def run
|
|
@@ -82,6 +86,11 @@ module Async
|
|
|
82
86
|
private
|
|
83
87
|
|
|
84
88
|
def scheduler_loop(task)
|
|
89
|
+
# Queue-only workers have no heap entry to sleep on. Keep the runner
|
|
90
|
+
# alive until #stop / SIGTERM wakes this condition; the queue listener
|
|
91
|
+
# continues independently in its own Async task.
|
|
92
|
+
return shutdown.wait if heap.empty? && @listen_queue
|
|
93
|
+
|
|
85
94
|
loop do
|
|
86
95
|
entry = heap.peek
|
|
87
96
|
break unless entry
|
|
@@ -93,6 +102,12 @@ module Async
|
|
|
93
102
|
end
|
|
94
103
|
end
|
|
95
104
|
|
|
105
|
+
def validate_work_source!(config_path)
|
|
106
|
+
return unless config_path.nil? && !@listen_queue
|
|
107
|
+
|
|
108
|
+
raise ConfigError, 'Runner requires config_path or queue_socket_dir'
|
|
109
|
+
end
|
|
110
|
+
|
|
96
111
|
def wait_for_next_entry(task, entry)
|
|
97
112
|
wait = [entry.next_run_at - monotonic_now, MIN_SLEEP_TIME].max
|
|
98
113
|
wait_with_shutdown(task, wait)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Background
|
|
5
|
+
module Web
|
|
6
|
+
class App
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config.validate!
|
|
9
|
+
@auth = Auth.new(@config.auth)
|
|
10
|
+
@snapshot = Snapshot.new(path: @config.queue_path, counts_cache_ttl: @config.counts_cache_ttl).open!
|
|
11
|
+
@metrics_reader = build_metrics_reader
|
|
12
|
+
@serializer = Serializer.new(@config)
|
|
13
|
+
@event_hub = build_event_hub
|
|
14
|
+
@router = Router.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(env)
|
|
18
|
+
return Response.unauthorized unless @auth.authorized?(env)
|
|
19
|
+
|
|
20
|
+
route = @router.match(env)
|
|
21
|
+
return Response.not_found unless route
|
|
22
|
+
|
|
23
|
+
dispatch(route, env)
|
|
24
|
+
rescue RequestError => error
|
|
25
|
+
Response.bad_request(error.message)
|
|
26
|
+
rescue UnavailableError, ClosedError
|
|
27
|
+
Response.unavailable
|
|
28
|
+
rescue StandardError
|
|
29
|
+
# Do not turn internal class names, paths or database errors into an
|
|
30
|
+
# unauthenticated information disclosure channel.
|
|
31
|
+
Response.internal_error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def close
|
|
35
|
+
@event_hub&.close
|
|
36
|
+
@snapshot.close
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def build_metrics_reader
|
|
43
|
+
return unless @config.metrics_enabled?
|
|
44
|
+
|
|
45
|
+
MetricsReader.new(path: @config.metrics_path, total_workers: @config.total_workers)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_event_hub
|
|
49
|
+
return unless @config.transport == :sse
|
|
50
|
+
|
|
51
|
+
EventHub.new(
|
|
52
|
+
@snapshot,
|
|
53
|
+
@serializer,
|
|
54
|
+
metrics_reader: @metrics_reader,
|
|
55
|
+
poll_seconds: @config.stream_poll_seconds
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dispatch(route, env)
|
|
60
|
+
case route
|
|
61
|
+
when :index then Response.html(Assets.render_index(@config))
|
|
62
|
+
when :javascript then Response.javascript(Assets::JS)
|
|
63
|
+
when :stylesheet then Response.stylesheet(Assets::CSS)
|
|
64
|
+
when :overview then overview_response
|
|
65
|
+
when :executing then in_flight_response(:executing, env)
|
|
66
|
+
when :claimed then in_flight_response(:claimed, env)
|
|
67
|
+
when :done then terminal_response(:done, env)
|
|
68
|
+
when :failed then terminal_response(:failed, env)
|
|
69
|
+
when :pending then pending_response(env)
|
|
70
|
+
when :metrics then metrics_response
|
|
71
|
+
when :config then config_response
|
|
72
|
+
when :stream then stream_response
|
|
73
|
+
else Response.not_found
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def overview_response
|
|
78
|
+
Response.json(@serializer.overview(@snapshot.overview, metrics_payload))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def in_flight_response(kind, env)
|
|
82
|
+
request = Request.new(env, @config)
|
|
83
|
+
rows = kind == :executing ? @snapshot.executing(limit: request.limit) : @snapshot.claimed(limit: request.limit)
|
|
84
|
+
payload = kind == :executing ? @serializer.executing(rows) : @serializer.claimed(rows)
|
|
85
|
+
Response.json({items: payload})
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def terminal_response(kind, env)
|
|
89
|
+
request = Request.new(env, @config)
|
|
90
|
+
cursor = request.finished_cursor
|
|
91
|
+
rows = kind == :done ? @snapshot.recent_done(limit: request.limit, cursor: cursor) :
|
|
92
|
+
@snapshot.recent_failed(limit: request.limit, cursor: cursor)
|
|
93
|
+
payload = kind == :done ? @serializer.done(rows) : @serializer.failed(rows)
|
|
94
|
+
Response.json(payload)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def pending_response(env)
|
|
98
|
+
request = Request.new(env, @config)
|
|
99
|
+
rows = @snapshot.pending(limit: request.limit, cursor: request.pending_cursor)
|
|
100
|
+
Response.json(@serializer.pending(rows))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def metrics_response
|
|
104
|
+
Response.json(metrics_payload || {available: false, workers: [], totals: MetricsReader::EMPTY_TOTALS})
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def metrics_payload
|
|
108
|
+
@metrics_reader&.aggregated
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def config_response
|
|
112
|
+
Response.json(
|
|
113
|
+
{
|
|
114
|
+
title: @config.title,
|
|
115
|
+
poll_interval_ms: @config.poll_interval_ms,
|
|
116
|
+
transport: @config.transport.to_s,
|
|
117
|
+
expose_args: @config.expose_args,
|
|
118
|
+
list_limit: @config.list_limit,
|
|
119
|
+
mount_path: @config.mount_path
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def stream_response
|
|
125
|
+
return Response.not_found unless @config.transport == :sse
|
|
126
|
+
|
|
127
|
+
Response.sse(
|
|
128
|
+
Stream.new(
|
|
129
|
+
@event_hub,
|
|
130
|
+
heartbeat_seconds: @config.stream_heartbeat_seconds,
|
|
131
|
+
retry_ms: @config.stream_retry_ms
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|