pgbus 0.13.1 → 0.13.3
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 +12 -0
- data/README.md +78 -3
- data/app/models/pgbus/processed_event.rb +45 -0
- data/app/views/pgbus/dashboard/_queues_table.html.erb +5 -3
- data/app/views/pgbus/queues/_queues_list.html.erb +5 -3
- data/app/views/pgbus/queues/show.html.erb +3 -0
- data/config/locales/da.yml +7 -2
- data/config/locales/de.yml +7 -2
- data/config/locales/en.yml +7 -2
- data/config/locales/es.yml +7 -2
- data/config/locales/fi.yml +7 -2
- data/config/locales/fr.yml +7 -2
- data/config/locales/it.yml +7 -2
- data/config/locales/ja.yml +7 -2
- data/config/locales/nb.yml +7 -2
- data/config/locales/nl.yml +7 -2
- data/config/locales/pt.yml +7 -2
- data/config/locales/sv.yml +7 -2
- data/exe/pgbus-health +9 -0
- data/lib/generators/pgbus/add_processed_event_completion_generator.rb +45 -0
- data/lib/generators/pgbus/templates/add_processed_event_completion.rb.erb +19 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +3 -0
- data/lib/pgbus/cli.rb +6 -4
- data/lib/pgbus/client.rb +44 -0
- data/lib/pgbus/configuration.rb +43 -0
- data/lib/pgbus/event_bus/handler.rb +49 -7
- data/lib/pgbus/generators/migration_detector.rb +13 -0
- data/lib/pgbus/health_probe.rb +132 -0
- data/lib/pgbus/integrations/appsignal/probe.rb +8 -4
- data/lib/pgbus/mcp/tools/queues_tool.rb +6 -0
- data/lib/pgbus/process/consumer.rb +4 -1
- data/lib/pgbus/process/readiness_snapshot.rb +30 -0
- data/lib/pgbus/process/supervisor.rb +64 -3
- data/lib/pgbus/process/worker.rb +14 -4
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source.rb +8 -1
- data/lib/pgbus/web/health_app.rb +23 -1
- data/lib/pgbus/web/metrics_serializer.rb +9 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e9af048ef1aa2d0fecab62f5471ca9ab6c3e2595353c7eb1a13cde57c37564a
|
|
4
|
+
data.tar.gz: 29a35e789733ac394136eaea8fc2b7f2e709617ceee818dd2b2752d9b017d45d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 392a67ed233feb5ffe0a98cf597efe3962b9e7c81d07a7e351f6c4e8aab8c03b3edc8e55204c3f0d0a4b82287b7f523cada5ca79e0684752f50c8f3272c9bdc0
|
|
7
|
+
data.tar.gz: 95b48a0ae66aaa239da08fa3c235a412241ddcfacea8384d84a66ca298308678062dc0192592b07484596fe12e27145a71474d5547b5046370bfca2785ae0f03
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Queue-age metrics no longer count vt-parked (scheduled/retrying) messages — one delayed job stops reading as a degraded queue (issue #389).** ⚠️ **Behavior change on the AppSignal `pgbus_queue_latency` gauge.** pgmq's `oldest_msg_age_sec` is computed from `enqueued_at` and ignores `vt`, but a job enqueued with `wait:` or parked on a long retry backoff lives in the queue table with a future `vt` — that *is* the delayed-delivery mechanism. So a single parked message made the age metric grow at wall-clock rate for hours on an otherwise drained queue, and any latency alert thresholding on it fired continuously ("oldest message is 17045s old" on a healthy queue with depth 1, `read_ct` 0). Every metrics surface now also exposes **`oldest_claimable_age_sec`** — `now() - min(vt)` over rows with `vt <= now()`, i.e. the age of the oldest message actually *eligible for pickup*: an immediately-enqueued message contributes from enqueue time (matching the old number on a plain backlog), a scheduled/backoff-parked message contributes nothing until due, an in-flight message (vt pushed forward) is excluded, and nil means "no claimable backlog" even when the table is non-empty. Surfaces: `Web::DataSource` (dashboard, JSON API, MCP `pgbus_queues` tool), a new Prometheus gauge `pgbus_queue_oldest_claimable_age_seconds`, a new AppSignal gauge `pgbus_queue_oldest_claimable_age_seconds`, `Pgbus::Client#oldest_claimable_ages` (raw-SQL reader, since pgmq's `metrics_result` type is frozen upstream), and a CLAIMABLE column in `pgbus queues`. The AppSignal **`pgbus_queue_latency` gauge now derives from the claimable age** and always emits — `(claimable_age || 0) * 1000`, 0 = no claimable backlog — so existing latency alerts stop false-firing with no dashboard changes; the raw `pgbus_queue_oldest_message_age_seconds` gauge keeps its enqueue-time semantics everywhere. The dashboard queue tables additionally split depth into **Parked** (`depth − visible`) and show the claimable age in place of the raw age, so a queue holding only backoff retries reads visibly healthy. Refs #389.
|
|
6
|
+
|
|
3
7
|
### Added
|
|
4
8
|
|
|
9
|
+
- **Health-checked rolling restarts for the job container (issue #386).** ⚠️ **Behavior change on the standalone `/readyz`.** The supervisor's `health_port` server previously answered `/readyz` with the cluster-wide HealthAnalyzer verdict — so during a rolling deploy a freshly-booted container could pass an orchestrator's health gate on the strength of the *old* container's still-heartbeating workers, and the old container (with all its capacity) was stopped before the new one had forked a single child. The standalone `/readyz` is now **container-local**: 200 only when *this* supervisor verified its connection, bootstrapped queues, forked every configured child, and all of them are currently alive — with 503 bodies `BOOTING` (pre-boot), `DEGRADED` (a child died and is waiting out crash-restart backoff — precisely the state a deploy gate must fail on, keeping the old container running), and `DRAINING` (stop signal received). No database access on the probe path; the supervisor publishes an immutable snapshot per monitor pass and the accept thread reads it. The Rails-mounted `Pgbus::Web::HealthApp` keeps the cluster-wide verdict unchanged. Alongside it: **`pgbus-health`**, a shipped executable probe for docker `HEALTHCHECK` blocks (plain Ruby + stdlib sockets, loads neither Bundler nor the gem — cheap at 1–5s intervals, works in curl-less images; exit 0/1/2 = healthy/unhealthy/usage), and a README "Rolling restarts (Kamal, docker)" guide covering the healthcheck block, stop-timeout alignment, overlap-window duplicate-supervisor safety, and the `read_ct`-vs-deploy-kill DLQ caveat. Refs #386.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Shutdown budgets are now alignable end-to-end (issue #386).** New `config.shutdown_timeout` bounds how long the supervisor waits for children after forwarding TERM before escalating to SIGKILL — previously a hardcoded 30s, which silently SIGKILLed workers mid-drain the moment `drain_timeout` was raised past it. Default derives `drain_timeout + 5` so the deadline tracks the drain window automatically; an explicit value below `drain_timeout` logs a boot warning. `Consumer#shutdown`'s pool wait (its only drain bound) now follows `config.drain_timeout` instead of a hardcoded 30s, and `Worker#shutdown`'s post-drain residual wait drops from a second full 30s window to 5s — the drain loop already waited `drain_timeout`, and a job still running has proven it won't finish. Rule of thumb: orchestrator stop grace period > `shutdown_timeout` > `drain_timeout`. Refs #386.
|
|
14
|
+
|
|
5
15
|
- **Streams: one LISTEN connection per web host — `streams_listen_scope` (issue #382).** ⚠️ **Default behavior change.** Previously every Puma worker lazily opened its own dedicated streams LISTEN connection on first SSE use, so a web host pinned one direct connection per worker. Under the new default (`streams_listen_scope = :master`) the `pgbus_streams` Puma plugin runs a **MasterHub** in the preforking master: ONE `Web::Streamer::Listener` on the refcounted union of every worker's stream channels, fanning wakes — **including ephemeral payloads** — out to workers over a Unix domain socket with length-prefixed frames (`Streamer::HubProtocol`). Workers connect lazily (nothing is inherited across fork) and the synchronous `ensure_listening` ack contract is preserved cross-process: a sub is registered before LISTEN executes and acked only after, so the no-lost-broadcast guarantee holds. Backpressure follows the streams rules: durable wakes are droppable at a per-worker cap (they self-heal via `read_after`), **ephemeral wakes are never dropped** — a worker that stops draining is evicted, which triggers its own fallback. **Fallback is per-worker listeners, not loss**: whenever the hub is absent or dies (no `preload_app!`, single-mode Puma, crash, eviction) each worker's `FailoverListener` swaps in a real per-worker `Listener` and re-LISTENs its recorded subscriptions — connection footprint balloons back to pre-#382 levels (census-visible) but no broadcast semantics change; the worker stays local until it recycles. Measured (local PG, n=50): the master→worker hop is noise-level free — single-broadcast SSE roundtrip p50 16.00ms via the hub vs 16.93ms per-worker. **`:master` effectively requires `preload_app!`** (the hub waits for the app's pgbus initializer; without it the deadline expires quietly and workers stay per-worker). **Rollback:** `config.streams_listen_scope = :process`. Refs #382, builds on the #381 patterns.
|
|
6
16
|
|
|
7
17
|
- **Host-level shared LISTEN: `worker_notify_scope` — the supervisor now owns ONE direct LISTEN connection for the whole host (issue #381).** ⚠️ **Default behavior change.** Previously every worker fork and every consumer fork opened its own dedicated LISTEN connection (`NotifyListener`), so a host's direct-connection footprint scaled with fork count — on transaction-pool PgBouncer platforms those connections come out of the scarcest slice of `max_connections`, and a 5-capsule + 2-consumer host pinned 7. Under the new default (`config.worker_notify_scope = :supervisor`) the supervisor runs a single `NotifyHub`: one `NotifyListener` on the union of every capsule's and consumer's queue channels (wildcards via the shared resolver, consumer sets via the registry), fanning wakes out to forks over per-fork pipes (`W` wake / `H` healthy / `P` degraded bytes; a fork whose pipe reports degraded or reaches EOF falls back to fast polling exactly like a failed local listener). Footprint drops to **1 direct LISTEN connection per job host**, verified by integration test: routing is per-fork (an insert wakes only the forks reading that queue, wildcard capsules unconditionally), and `pg_terminate_backend` on the shared connection is survived — reconnect, re-LISTEN, wakes flow again. **Rollback:** `config.worker_notify_scope = :fork` restores the previous per-fork listeners byte-for-byte. Dedicated LISTEN connections are now census-tagged `application_name=pgbus-listen` so `pg_stat_activity` can count them. Refs #381.
|
|
@@ -16,6 +26,8 @@
|
|
|
16
26
|
|
|
17
27
|
### Fixed
|
|
18
28
|
|
|
29
|
+
- **`idempotent!` handlers no longer silently drop an execution when the consumer is killed mid-handler — the idempotency claim is now two-phase (issue #385).** `EventBus::Handler#process!` inserted the `pgbus_processed_events` claim row *before* `handle(event)` ran, so a SIGKILL between claim and completion (supervisor watchdog, container runtime after its stop timeout, OOM — i.e. every container-replacement deploy's worst case) turned the redelivered message into a skip: the mechanism meant to dedup *duplicate* executions converted a crash *during* the one permitted execution into at-most-once. The claim now lands with a NULL `completed_at` (*pending*) and is stamped completed only after `handle` returns; on redelivery a pending claim **re-runs** the handler while a completed claim still skips. The in-memory dedup cache only ever records completed executions, and a `handle` that raises leaves the claim pending so VT redelivery retries it. The only double-execution window is a still-alive handler running past its visibility timeout — the same at-least-once window every non-idempotent handler already has. **Upgrading:** new installs get the column from `pgbus:install`; existing installs run `rails generate pgbus:add_processed_event_completion` (also picked up by `pgbus:update`; `--database` supported) — legacy rows are backfilled as completed so history is not retroactively re-run. Until the migration runs, an upgraded gem detects the missing column once and falls back to the legacy single-phase claim with a warning naming the generator. Refs #385.
|
|
30
|
+
|
|
19
31
|
- **SIGTERM no longer segfaults the process: both LISTEN listeners stopped closing their PG connection from the stopping thread (issue #375).** `Process::NotifyListener#stop` (supervisor thread, on SIGTERM) closed the listener's dedicated `PG::Connection` to interrupt the blocking `wait_for_notify`, but left `@conn` pointing at it — so the listener thread unwound into `run_loop`'s `ensure` and ran `safe_unlisten_all`, exec'ing `UNLISTEN` on a connection the other thread was concurrently freeing. `PG::Connection#close` is `PQfinish`: it frees the PGconn **and its OpenSSL objects**, so `PQsendQuery` walked into freed TLS state and the whole worker died with `[BUG] Segmentation fault` — reproducibly, on essentially every container-replacement deploy against a TLS Postgres, sometimes in several forked workers at once. `rescue PG::Error` cannot catch a C-level SEGV, so the "safe" in `safe_unlisten_all` never held. **The connection is now single-owner**: the listener thread is the only thread that may exec, wait, or close on it, from build through teardown. `#stop` signals by clearing `@running` and joining — nothing more — and the listener thread closes its own connection in `run_loop`'s `ensure`. The stop is observed within one `wait_for_notify` timeout (`health_check_ms`, **250 ms** in the default configuration since workers/consumers derive it from `polling_interval`), so `#stop`'s join budget is now `health_check_ms + 5s` instead of a flat 5 s — a flat timeout could expire before a listener with a large `health_check_ms` had even one chance to observe the stop. The teardown `UNLISTEN` round-trip is **dropped entirely** rather than merely made safe: it ran immediately before the close, and closing a session deregisters every `LISTEN` server-side, so it bought nothing at any time. The identical defect in `Web::Streamer::Listener` — same close-from-`#stop`, same `UNLISTEN`-in-`ensure`, and there without even a mutex around `@conn` — is fixed the same way, so the dashboard's Puma worker stops crashing on the same deploy; its listener thread now also closes its own connection (previously only `#stop` did). Cost: shutdown can take up to one health-check cycle longer per listener — measured against a real PostgreSQL at the default `health_check_ms` of 250: **~207 ms on a fully idle queue, ~5 ms when the queue has any NOTIFY traffic** (a notification returns the wait immediately, and the loop then sees the cleared flag). 40 start/stop cycles under concurrent NOTIFY load leaked zero `LISTEN` backends. Thanks to the reporter for the crash dumps and the root-cause analysis. Refs #375.
|
|
20
32
|
- **The health verdict no longer emits false STALLED reports — `max_read_ct` was never populated, and the wedge signal counted queues no capsule drains (issue #367).** Two correctness defects in `Pgbus::MCP::HealthAnalyzer` (surfaced through `pgbus doctor` / `pgbus_health` / the MCP health tool). **(1)** The `all_unread?` wedge check read `:max_read_ct`, but the metrics query never selected it — so the guard always degenerated to "never claimed" and a busy-but-healthy queue caught mid-burst produced a STALLED verdict with a factually wrong "read_ct=0 (never claimed)" reason. The metrics query now exposes a per-queue **`visible_unread_length`** (`count(*) WHERE vt <= NOW() AND read_ct = 0`) and the analyzer keys the wedge off *visible, never-claimed* messages. Counting per visible message (not `max(read_ct)` over the whole table) means one retried message left in-queue after its backoff, or an in-flight message claimed by a peer, can no longer veto the signal for a pile of genuinely-unclaimed jobs. **(2)** The verdict reasoned about **every** non-DLQ/non-stream queue against the global worker fleet — but a worker can only claim from queues its capsule subscribes to, so "M workers alive but never claimed" was vacuous for a queue nobody drains (ad-hoc queues, unregistered stream queues — see #366). The analyzer now intersects the STALLED backlog with `Web::DataSource#drained_queue_names` (each configured capsule's queues, priority `_pN` sub-tables expanded via the client's queue strategy, unioned with EventBus handler queues; `nil` for a `*` wildcard = drains everything, fail-open on error). Queues nobody drains get their own DEGRADED signal — *"N queue(s) hold messages but no capsule is configured to drain them"* — instead of being folded into the worker-wedge verdict. A heart-beating-but-`:stalled` worker is still reported STALLED regardless of which queue holds the backlog. Reason strings now truncate to the first 10 queue names with `(+N more)` so a flagged fleet of hundreds of queues doesn't produce a multi-KB log line. Refs #367, #366.
|
|
21
33
|
- **`allowed_global_id_models` now actually guards ActiveJob arguments, not only EventBus payloads (issue #368).** The doctor warned in production that `nil` means "allow-all GlobalID arguments", but the allowlist was only enforced in `Serializer.locate_global_id` — reached from EventBus `_global_id` payloads — while the ordinary job path (`Executor` → `ActiveJob::Base.deserialize` → Rails' unrestricted `GlobalID::Locator`) never checked it. Operators who set an allowlist after following the doctor had a false sense of security; the common `SomeJob.perform_later(record)` pattern was unguarded. Job deserialization now goes through `Serializer.deserialize_job_data`, which walks `_aj_globalid` keys (including nested arrays/hashes) and reuses the same gate as EventBus when the allowlist is set; `nil` remains zero-cost allow-all. Rejected models raise `Pgbus::SerializationError` and are treated as a normal job failure. Apps with ActiveStorage attachments should include `ActiveStorage::Blob` (and related models) on the allowlist. Docs + doctor copy updated. Refs #368.
|
data/README.md
CHANGED
|
@@ -221,6 +221,20 @@ Pgbus::EventBus::Registry.instance.subscribe(
|
|
|
221
221
|
)
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
+
`idempotent!` uses a **two-phase claim**: a *pending* row in
|
|
225
|
+
`pgbus_processed_events` is inserted before `handle` runs, and only stamped
|
|
226
|
+
`completed_at` after `handle` returns. Deduplication applies to **completed**
|
|
227
|
+
executions only — if the consumer process is killed mid-handler (deploy,
|
|
228
|
+
OOM, supervisor watchdog), the redelivered message finds the pending claim
|
|
229
|
+
and **re-runs the handler** instead of silently skipping it. The semantics
|
|
230
|
+
are at-least-once with dedup of completed executions: the only
|
|
231
|
+
double-execution window is a handler still running past its visibility
|
|
232
|
+
timeout — the same window every non-idempotent handler already has.
|
|
233
|
+
Installs created before this feature need the upgrade migration:
|
|
234
|
+
`rails generate pgbus:add_processed_event_completion` (supports
|
|
235
|
+
`--database`); until it runs, idempotent handlers fall back to the old
|
|
236
|
+
single-phase claim and log a warning.
|
|
237
|
+
|
|
224
238
|
### 4. Start workers
|
|
225
239
|
|
|
226
240
|
```bash
|
|
@@ -977,6 +991,7 @@ When `config.metrics_enabled = true` (default), the dashboard exposes Prometheus
|
|
|
977
991
|
|
|
978
992
|
| Metric | Description |
|
|
979
993
|
|--------|-------------|
|
|
994
|
+
| `pgbus_queue_oldest_claimable_age_seconds` | Age of the oldest message eligible for pickup (visibility timeout elapsed) — safe to alert on: scheduled/backoff-parked messages don't count until due; the series is omitted entirely when no claimable backlog exists (while the raw `pgbus_queue_oldest_message_age_seconds` gauge may still report a parked message's age) |
|
|
980
995
|
| `pgbus_table_dead_tuples` | Dead tuple count per PGMQ table |
|
|
981
996
|
| `pgbus_table_live_tuples` | Live tuple count per PGMQ table |
|
|
982
997
|
| `pgbus_table_bloat_ratio` | Dead / (dead + live) per table |
|
|
@@ -1103,14 +1118,18 @@ For the **HTTP** transport, point the client at the mounted URL with a streamabl
|
|
|
1103
1118
|
|
|
1104
1119
|
### Health endpoints (liveness / readiness)
|
|
1105
1120
|
|
|
1106
|
-
For orchestrators like Kubernetes, Pgbus exposes two HTTP probes: `/livez` (is the serving process up?) and `/readyz
|
|
1121
|
+
For orchestrators like Kubernetes, Pgbus exposes two HTTP probes: `/livez` (is the serving process up?) and `/readyz`. Readiness means different things in the two places the probes are served:
|
|
1122
|
+
|
|
1123
|
+
- **Mounted in Rails** (`Pgbus::Web::HealthApp`): `/readyz` runs the cluster-wide `OK` / `DEGRADED` / `STALLED` verdict, same as the MCP `pgbus_health` tool — `STALLED` (visible backlog while workers heart-beat but don't claim) fails readiness.
|
|
1124
|
+
- **Standalone from the supervisor** (`health_port`): `/readyz` is **container-local** — did *this* supervisor finish booting, and are all the children *it* forked alive? That is the signal a rolling deploy's health gate needs; the cluster verdict would let a brand-new container pass on the strength of the *old* container's workers.
|
|
1107
1125
|
|
|
1108
1126
|
| Path | Method | 200 | 503 | Touches DB |
|
|
1109
1127
|
|---|---|---|---|---|
|
|
1110
1128
|
| `/livez` | GET | always (`ok`) | never | no |
|
|
1111
|
-
| `/readyz` | GET | verdict `OK` or `DEGRADED` | verdict `STALLED`, or DB unreachable (`{"status":"ERROR"}`) | yes |
|
|
1129
|
+
| `/readyz` (mounted) | GET | verdict `OK` or `DEGRADED` | verdict `STALLED`, or DB unreachable (`{"status":"ERROR"}`) | yes |
|
|
1130
|
+
| `/readyz` (supervisor) | GET | `OK` — booted, all children live | `BOOTING`, `DEGRADED` (child down), `DRAINING` (stopping) | no |
|
|
1112
1131
|
|
|
1113
|
-
Unknown paths return `404`; non-`GET` methods return `405`. The `/readyz` body is
|
|
1132
|
+
Unknown paths return `404`; non-`GET` methods return `405`. The `/readyz` body is JSON, so a probe failure is self-describing in the pod's event log.
|
|
1114
1133
|
|
|
1115
1134
|
#### Mount in your Rails app
|
|
1116
1135
|
|
|
@@ -1151,6 +1170,61 @@ readinessProbe:
|
|
|
1151
1170
|
httpGet: { path: /readyz, port: 9394 }
|
|
1152
1171
|
```
|
|
1153
1172
|
|
|
1173
|
+
The supervisor's `/readyz` answers from its own state, never the database:
|
|
1174
|
+
|
|
1175
|
+
```json
|
|
1176
|
+
{ "status": "OK", "expected": 3, "live": 3 }
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
- `BOOTING` (503) until the connection is verified, queues are bootstrapped, and every configured child has been forked. `expected` is stamped at that instant.
|
|
1180
|
+
- `OK` (200) while all expected children are in the fork table. A clean worker recycle never dips the count — the snapshot refreshes after reap-and-restart each monitor pass.
|
|
1181
|
+
- `DEGRADED` (503) when a child died and is waiting out crash-restart backoff. During a rolling deploy this is the desired failure mode: a crash-looping replacement never goes ready, so the old container keeps running.
|
|
1182
|
+
- `DRAINING` (503) the moment a stop signal arrives.
|
|
1183
|
+
|
|
1184
|
+
#### `pgbus-health`: container HEALTHCHECK probe
|
|
1185
|
+
|
|
1186
|
+
`pgbus-health` ships with the gem: a dependency-free probe (plain Ruby + stdlib sockets — no Bundler, no Rails, nothing else loaded) that GETs `127.0.0.1:<port>/readyz` and exits `0` on 200, `1` on anything else, `2` on usage errors. Cheap enough for a 1–5s `HEALTHCHECK` interval, and it works in images without curl:
|
|
1187
|
+
|
|
1188
|
+
```bash
|
|
1189
|
+
pgbus-health --port 9394 # or PGBUS_HEALTH_PORT=9394 pgbus-health
|
|
1190
|
+
pgbus-health --port 9394 --path /livez --timeout 2
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
### Rolling restarts (Kamal, docker)
|
|
1194
|
+
|
|
1195
|
+
Kamal distributions with per-role health checks (for example the [`dash` branch](https://github.com/mhenrixon/kamal)) can rolling-restart a non-proxied job role: start the new container, poll its docker `HEALTHCHECK` until healthy, and only then `docker stop` the old one. Wire the pgbus container into that gate:
|
|
1196
|
+
|
|
1197
|
+
```yaml
|
|
1198
|
+
# config/deploy.yml
|
|
1199
|
+
servers:
|
|
1200
|
+
job:
|
|
1201
|
+
hosts: [...]
|
|
1202
|
+
cmd: bin/pgbus start
|
|
1203
|
+
healthcheck:
|
|
1204
|
+
cmd: bin/pgbus-health --port 9394
|
|
1205
|
+
interval: 5s
|
|
1206
|
+
start_period: 30s # cover Rails boot + queue bootstrap
|
|
1207
|
+
stop_timeout: 45 # must exceed pgbus shutdown_timeout (see below)
|
|
1208
|
+
env:
|
|
1209
|
+
clear:
|
|
1210
|
+
PGBUS_HEALTH_PORT: 9394
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
(`bundle binstubs pgbus` generates `bin/pgbus-health`; adjust the path if your image invokes gem executables differently.)
|
|
1214
|
+
|
|
1215
|
+
**The shutdown timeline.** On `docker stop`, SIGTERM reaches the supervisor and readiness flips to `DRAINING`; children stop claiming work and drain in-flight jobs for up to `drain_timeout` (default 30s); the supervisor waits `shutdown_timeout` (default `drain_timeout + 5`) before SIGKILLing stragglers. Align the three knobs outside-in:
|
|
1216
|
+
|
|
1217
|
+
```text
|
|
1218
|
+
orchestrator stop_timeout > pgbus shutdown_timeout > pgbus drain_timeout
|
|
1219
|
+
45s 35s (derived) 30s
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
If the orchestrator's stop grace period is *shorter* than `shutdown_timeout`, docker SIGKILLs the whole tree mid-drain and the graceful path never gets to finish. Raising `drain_timeout` raises the derived `shutdown_timeout` automatically; raise `stop_timeout` to match.
|
|
1223
|
+
|
|
1224
|
+
**The overlap window is safe by construction.** Between "new container healthy" and "old container stopped", two supervisors run against the same database. Nothing double-fires: queue claims use `FOR UPDATE SKIP LOCKED`, `single_active_consumer` queues arbitrate via session-level advisory locks (released the instant a killed process's connection dies), two live recurring schedulers dedup on the `(task_key, run_at)` unique record, and dispatcher maintenance is idempotent. "One scheduler per deployment" is a steady-state rule; a deploy window may briefly violate it without consequence.
|
|
1225
|
+
|
|
1226
|
+
**What a hard kill still costs.** Jobs killed past the drain window are redelivered after their visibility timeout (at-least-once holds) — but PGMQ's `read_ct` increments exactly like a logical failure, so a long-running job that straddles *repeated* deploy kills can be pushed to the DLQ without its code ever raising. `zombie_detection` logs exactly this pattern (`read_ct > 1` with no recorded failure). Keep jobs shorter than `drain_timeout`, or raise it (and `stop_timeout`) for queues that can't be. For `idempotent!` event handlers there is a separate crash-window caveat tracked in [#385](https://github.com/mhenrixon/pgbus/issues/385).
|
|
1227
|
+
|
|
1154
1228
|
### Boot diagnostics banner
|
|
1155
1229
|
|
|
1156
1230
|
`Supervisor#run` logs a one-block banner right after the heartbeat starts and before queues bootstrap, so a misconfigured deployment states its actual settings instead of forcing an operator to attach a console. Every line is `"[Pgbus] boot:"`-prefixed and renders cleanly under both the `:text` and `:json` log formatters:
|
|
@@ -2097,6 +2171,7 @@ Curated headline options for the README. The full operator reference (with types
|
|
|
2097
2171
|
| `zombie_detection` | `true` | Detect and reclaim work from crashed workers |
|
|
2098
2172
|
| `read_timeout` | `30` | Seconds before a single PGMQ read is bounded (libpq `statement_timeout` + `tcp_user_timeout` on a dedicated connection; nil disables) |
|
|
2099
2173
|
| `drain_timeout` | `30` | Seconds to wait for in-flight jobs during graceful shutdown before abandoning them |
|
|
2174
|
+
| `shutdown_timeout` | `drain_timeout + 5` | Seconds the supervisor waits for children after TERM before SIGKILL; an orchestrator's stop grace period must exceed it |
|
|
2100
2175
|
| `stall_threshold` | `300` | Seconds without progress before a worker is considered stalled |
|
|
2101
2176
|
| `priority_levels` | `nil` | Number of priority sub-queues (nil = disabled, 2-10) |
|
|
2102
2177
|
| `default_priority` | `1` | Default priority for jobs without explicit priority |
|
|
@@ -5,5 +5,50 @@ module Pgbus
|
|
|
5
5
|
self.table_name = "pgbus_processed_events"
|
|
6
6
|
|
|
7
7
|
scope :expired, ->(before) { where("processed_at < ?", before) }
|
|
8
|
+
|
|
9
|
+
@completion_column_mutex = Mutex.new
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
# Whether pgbus_processed_events has the completed_at column that backs
|
|
13
|
+
# the two-phase idempotency claim (issue #385). Detected once per process
|
|
14
|
+
# (memoized under a mutex) so the schema probe never lands on the
|
|
15
|
+
# per-event hot path. An upgraded gem running against a not-yet-migrated
|
|
16
|
+
# table gets `false` plus a one-time warning pointing at the upgrade
|
|
17
|
+
# generator — Handler then falls back to the legacy single-phase claim.
|
|
18
|
+
#
|
|
19
|
+
# A detection error (e.g. the database is briefly unreachable) is NOT
|
|
20
|
+
# memoized: it propagates to the caller — where the event's normal
|
|
21
|
+
# failure path leaves the message for VT redelivery — and the next
|
|
22
|
+
# delivery probes again.
|
|
23
|
+
def completion_column?
|
|
24
|
+
detected = @completion_column
|
|
25
|
+
return detected unless detected.nil?
|
|
26
|
+
|
|
27
|
+
@completion_column_mutex.synchronize do
|
|
28
|
+
@completion_column = detect_completion_column if @completion_column.nil?
|
|
29
|
+
@completion_column
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Test seam: clear the memoized detection so specs can exercise both
|
|
34
|
+
# schema shapes in one process.
|
|
35
|
+
def reset_completion_column_check!
|
|
36
|
+
@completion_column_mutex.synchronize { @completion_column = nil }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def detect_completion_column
|
|
42
|
+
supported = column_names.include?("completed_at")
|
|
43
|
+
unless supported
|
|
44
|
+
Pgbus.logger.warn do
|
|
45
|
+
"[Pgbus] pgbus_processed_events is missing the completed_at column; idempotent handlers " \
|
|
46
|
+
"fall back to single-phase claims (a crash mid-handler can skip work on redelivery). " \
|
|
47
|
+
"Run `rails generate pgbus:add_processed_event_completion` and migrate."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
supported
|
|
51
|
+
end
|
|
52
|
+
end
|
|
8
53
|
end
|
|
9
54
|
end
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.queue") %></th>
|
|
13
13
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.depth") %></th>
|
|
14
14
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.visible") %></th>
|
|
15
|
-
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.
|
|
15
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.parked") %></th>
|
|
16
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.oldest_claimable") %></th>
|
|
16
17
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.total") %></th>
|
|
17
18
|
</tr>
|
|
18
19
|
</thead>
|
|
@@ -25,12 +26,13 @@
|
|
|
25
26
|
</td>
|
|
26
27
|
<td data-label="Depth" class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_length]) %></td>
|
|
27
28
|
<td data-label="Visible" class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_visible_length]) %></td>
|
|
28
|
-
<td data-label="
|
|
29
|
+
<td data-label="Parked" class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:parked_length]) %></td>
|
|
30
|
+
<td data-label="Oldest claimable" class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:oldest_claimable_age_sec] || "—" %></td>
|
|
29
31
|
<td data-label="Total" class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:total_messages]) %></td>
|
|
30
32
|
</tr>
|
|
31
33
|
<% end %>
|
|
32
34
|
<% if @queues.empty? %>
|
|
33
|
-
<tr><td colspan="
|
|
35
|
+
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dashboard.queues_table.empty") %></td></tr>
|
|
34
36
|
<% end %>
|
|
35
37
|
</tbody>
|
|
36
38
|
</table>
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.queue") %></th>
|
|
7
7
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.depth") %></th>
|
|
8
8
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.visible") %></th>
|
|
9
|
-
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.
|
|
9
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.parked") %></th>
|
|
10
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.oldest_claimable") %></th>
|
|
10
11
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.newest") %></th>
|
|
11
12
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.total_ever") %></th>
|
|
12
13
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.queues.queues_list.headers.actions") %></th>
|
|
@@ -24,7 +25,8 @@
|
|
|
24
25
|
</td>
|
|
25
26
|
<td data-label="Depth" class="px-4 py-3 text-sm text-right font-mono text-gray-700"><%= pgbus_number(q[:queue_length]) %></td>
|
|
26
27
|
<td data-label="Visible" class="px-4 py-3 text-sm text-right font-mono text-gray-700"><%= pgbus_number(q[:queue_visible_length]) %></td>
|
|
27
|
-
<td data-label="
|
|
28
|
+
<td data-label="Parked" class="px-4 py-3 text-sm text-right font-mono text-gray-500"><%= pgbus_number(q[:parked_length]) %></td>
|
|
29
|
+
<td data-label="Oldest claimable" class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:oldest_claimable_age_sec] || "—" %></td>
|
|
28
30
|
<td data-label="Newest" class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:newest_msg_age_sec] || "—" %></td>
|
|
29
31
|
<td data-label="Total" class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:total_messages]) %></td>
|
|
30
32
|
<td data-label="Actions" class="px-4 py-3 text-sm text-right space-x-2">
|
|
@@ -51,7 +53,7 @@
|
|
|
51
53
|
</tr>
|
|
52
54
|
<% end %>
|
|
53
55
|
<% if @queues.empty? %>
|
|
54
|
-
<tr><td colspan="
|
|
56
|
+
<tr><td colspan="8" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.queues.queues_list.empty") %></td></tr>
|
|
55
57
|
<% end %>
|
|
56
58
|
</tbody>
|
|
57
59
|
</table>
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
<p class="text-sm text-gray-500 mt-1">
|
|
14
14
|
<%= t("pgbus.queues.show.depth") %> <span class="font-mono"><%= @queue[:queue_length] %></span> |
|
|
15
15
|
<%= t("pgbus.queues.show.visible") %> <span class="font-mono"><%= @queue[:queue_visible_length] %></span> |
|
|
16
|
+
<%= t("pgbus.queues.show.parked") %> <span class="font-mono"><%= @queue[:parked_length] %></span> |
|
|
17
|
+
<%= t("pgbus.queues.show.oldest") %> <span class="font-mono"><%= @queue[:oldest_msg_age_sec] || "—" %></span> |
|
|
18
|
+
<%= t("pgbus.queues.show.oldest_claimable") %> <span class="font-mono"><%= @queue[:oldest_claimable_age_sec] || "—" %></span> |
|
|
16
19
|
<%= t("pgbus.queues.show.total") %> <span class="font-mono"><%= pgbus_number(@queue[:total_messages]) %></span>
|
|
17
20
|
</p>
|
|
18
21
|
<% end %>
|
data/config/locales/da.yml
CHANGED
|
@@ -65,7 +65,8 @@ da:
|
|
|
65
65
|
empty: Ingen køer fundet
|
|
66
66
|
headers:
|
|
67
67
|
depth: Dybde
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Ældste tilgængelig (s)
|
|
69
|
+
parked: Parkeret
|
|
69
70
|
queue: Kø
|
|
70
71
|
total: Total
|
|
71
72
|
visible: Synlig
|
|
@@ -475,7 +476,8 @@ da:
|
|
|
475
476
|
actions: Handlinger
|
|
476
477
|
depth: Dybde
|
|
477
478
|
newest: Nyeste (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Ældste tilgængelig (s)
|
|
480
|
+
parked: Parkeret
|
|
479
481
|
queue: Kø
|
|
480
482
|
total_ever: Total nogensinde
|
|
481
483
|
visible: Synlig
|
|
@@ -516,6 +518,9 @@ da:
|
|
|
516
518
|
scheduled: 'Planlagt:'
|
|
517
519
|
timezone: 'Tidszone:'
|
|
518
520
|
visible_at: 'Synlig fra:'
|
|
521
|
+
oldest: 'Ældste:'
|
|
522
|
+
oldest_claimable: 'Ældste tilgængelig:'
|
|
523
|
+
parked: 'Parkeret:'
|
|
519
524
|
pause: Pause
|
|
520
525
|
pause_confirm: Pause behandling?
|
|
521
526
|
purge_confirm: Rens alle beskeder?
|
data/config/locales/de.yml
CHANGED
|
@@ -65,7 +65,8 @@ de:
|
|
|
65
65
|
empty: Keine Warteschlangen gefunden
|
|
66
66
|
headers:
|
|
67
67
|
depth: Tiefe
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Älteste verfügbar (s)
|
|
69
|
+
parked: Geparkt
|
|
69
70
|
queue: Warteschlange
|
|
70
71
|
total: Gesamt
|
|
71
72
|
visible: Sichtbar
|
|
@@ -475,7 +476,8 @@ de:
|
|
|
475
476
|
actions: Aktionen
|
|
476
477
|
depth: Tiefe
|
|
477
478
|
newest: Neueste (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Älteste verfügbar (s)
|
|
480
|
+
parked: Geparkt
|
|
479
481
|
queue: Warteschlange
|
|
480
482
|
total_ever: Insgesamt jemals
|
|
481
483
|
visible: Sichtbar
|
|
@@ -516,6 +518,9 @@ de:
|
|
|
516
518
|
scheduled: 'Geplant:'
|
|
517
519
|
timezone: 'Zeitzone:'
|
|
518
520
|
visible_at: 'Sichtbar ab:'
|
|
521
|
+
oldest: 'Älteste:'
|
|
522
|
+
oldest_claimable: 'Älteste verfügbar:'
|
|
523
|
+
parked: 'Geparkt:'
|
|
519
524
|
pause: Pause
|
|
520
525
|
pause_confirm: Verarbeitung pausieren?
|
|
521
526
|
purge_confirm: Alle Nachrichten löschen?
|
data/config/locales/en.yml
CHANGED
|
@@ -65,7 +65,8 @@ en:
|
|
|
65
65
|
empty: No queues found
|
|
66
66
|
headers:
|
|
67
67
|
depth: Depth
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Oldest claimable (s)
|
|
69
|
+
parked: Parked
|
|
69
70
|
queue: Queue
|
|
70
71
|
total: Total
|
|
71
72
|
visible: Visible
|
|
@@ -475,7 +476,8 @@ en:
|
|
|
475
476
|
actions: Actions
|
|
476
477
|
depth: Depth
|
|
477
478
|
newest: Newest (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Oldest claimable (s)
|
|
480
|
+
parked: Parked
|
|
479
481
|
queue: Queue
|
|
480
482
|
total_ever: Total Ever
|
|
481
483
|
visible: Visible
|
|
@@ -516,6 +518,9 @@ en:
|
|
|
516
518
|
scheduled: 'Scheduled at:'
|
|
517
519
|
timezone: 'Timezone:'
|
|
518
520
|
visible_at: 'Visible at:'
|
|
521
|
+
oldest: 'Oldest:'
|
|
522
|
+
oldest_claimable: 'Oldest claimable:'
|
|
523
|
+
parked: 'Parked:'
|
|
519
524
|
pause: Pause
|
|
520
525
|
pause_confirm: Pause processing?
|
|
521
526
|
purge_confirm: Purge all messages?
|
data/config/locales/es.yml
CHANGED
|
@@ -65,7 +65,8 @@ es:
|
|
|
65
65
|
empty: No se encontraron colas
|
|
66
66
|
headers:
|
|
67
67
|
depth: Profundidad
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Más antiguo disponible (s)
|
|
69
|
+
parked: Aparcados
|
|
69
70
|
queue: Cola
|
|
70
71
|
total: Total
|
|
71
72
|
visible: Visible
|
|
@@ -475,7 +476,8 @@ es:
|
|
|
475
476
|
actions: Acciones
|
|
476
477
|
depth: Profundidad
|
|
477
478
|
newest: Más nuevo (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Más antiguo disponible (s)
|
|
480
|
+
parked: Aparcados
|
|
479
481
|
queue: Cola
|
|
480
482
|
total_ever: Total acumulado
|
|
481
483
|
visible: Visible
|
|
@@ -516,6 +518,9 @@ es:
|
|
|
516
518
|
scheduled: 'Programado:'
|
|
517
519
|
timezone: 'Zona horaria:'
|
|
518
520
|
visible_at: 'Visible en:'
|
|
521
|
+
oldest: 'Más antiguo:'
|
|
522
|
+
oldest_claimable: 'Más antiguo disponible:'
|
|
523
|
+
parked: 'Aparcados:'
|
|
519
524
|
pause: Pausar
|
|
520
525
|
pause_confirm: "¿Pausar el procesamiento?"
|
|
521
526
|
purge_confirm: "¿Purgar todos los mensajes?"
|
data/config/locales/fi.yml
CHANGED
|
@@ -65,7 +65,8 @@ fi:
|
|
|
65
65
|
empty: Jonot eivät löytyneet
|
|
66
66
|
headers:
|
|
67
67
|
depth: Syvyys
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Vanhin saatavilla (s)
|
|
69
|
+
parked: Pysäköidyt
|
|
69
70
|
queue: Jono
|
|
70
71
|
total: Yhteensä
|
|
71
72
|
visible: Näkyvissä
|
|
@@ -475,7 +476,8 @@ fi:
|
|
|
475
476
|
actions: Toiminnot
|
|
476
477
|
depth: Syvyys
|
|
477
478
|
newest: Uusimmat (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Vanhin saatavilla (s)
|
|
480
|
+
parked: Pysäköidyt
|
|
479
481
|
queue: Jono
|
|
480
482
|
total_ever: Yhteensä koskaan
|
|
481
483
|
visible: Näkyvissä
|
|
@@ -516,6 +518,9 @@ fi:
|
|
|
516
518
|
scheduled: 'Aikataulutettu:'
|
|
517
519
|
timezone: 'Aikavyöhyke:'
|
|
518
520
|
visible_at: 'Näkyvissä:'
|
|
521
|
+
oldest: 'Vanhin:'
|
|
522
|
+
oldest_claimable: 'Vanhin saatavilla:'
|
|
523
|
+
parked: 'Pysäköidyt:'
|
|
519
524
|
pause: Tauko
|
|
520
525
|
pause_confirm: Keskeytetäänkö käsittely?
|
|
521
526
|
purge_confirm: Tyhjennetäänkö kaikki viestit?
|
data/config/locales/fr.yml
CHANGED
|
@@ -65,7 +65,8 @@ fr:
|
|
|
65
65
|
empty: Aucune file d'attente trouvée
|
|
66
66
|
headers:
|
|
67
67
|
depth: Profondeur
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Plus ancien disponible (s)
|
|
69
|
+
parked: Différés
|
|
69
70
|
queue: File d'attente
|
|
70
71
|
total: Total
|
|
71
72
|
visible: Visible
|
|
@@ -475,7 +476,8 @@ fr:
|
|
|
475
476
|
actions: Actions
|
|
476
477
|
depth: Profondeur
|
|
477
478
|
newest: Le plus récent (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Plus ancien disponible (s)
|
|
480
|
+
parked: Différés
|
|
479
481
|
queue: File d'attente
|
|
480
482
|
total_ever: Total jamais
|
|
481
483
|
visible: Visible
|
|
@@ -516,6 +518,9 @@ fr:
|
|
|
516
518
|
scheduled: 'Planifié :'
|
|
517
519
|
timezone: 'Fuseau horaire :'
|
|
518
520
|
visible_at: 'Visible à :'
|
|
521
|
+
oldest: 'Plus ancien :'
|
|
522
|
+
oldest_claimable: 'Plus ancien disponible :'
|
|
523
|
+
parked: 'Différés :'
|
|
519
524
|
pause: Pause
|
|
520
525
|
pause_confirm: Mettre en pause le traitement ?
|
|
521
526
|
purge_confirm: Purger tous les messages ?
|
data/config/locales/it.yml
CHANGED
|
@@ -65,7 +65,8 @@ it:
|
|
|
65
65
|
empty: Nessuna coda trovata
|
|
66
66
|
headers:
|
|
67
67
|
depth: Profondità
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Più vecchio disponibile (s)
|
|
69
|
+
parked: Posticipati
|
|
69
70
|
queue: Coda
|
|
70
71
|
total: Totale
|
|
71
72
|
visible: Visibile
|
|
@@ -475,7 +476,8 @@ it:
|
|
|
475
476
|
actions: Azioni
|
|
476
477
|
depth: Profondità
|
|
477
478
|
newest: Più recente (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Più vecchio disponibile (s)
|
|
480
|
+
parked: Posticipati
|
|
479
481
|
queue: Coda
|
|
480
482
|
total_ever: Totale mai
|
|
481
483
|
visible: Visibile
|
|
@@ -516,6 +518,9 @@ it:
|
|
|
516
518
|
scheduled: 'Programmato:'
|
|
517
519
|
timezone: 'Fuso orario:'
|
|
518
520
|
visible_at: 'Visibile alle:'
|
|
521
|
+
oldest: 'Più vecchio:'
|
|
522
|
+
oldest_claimable: 'Più vecchio disponibile:'
|
|
523
|
+
parked: 'Posticipati:'
|
|
519
524
|
pause: Pausa
|
|
520
525
|
pause_confirm: Mettere in pausa l'elaborazione?
|
|
521
526
|
purge_confirm: Eliminare tutti i messaggi?
|
data/config/locales/ja.yml
CHANGED
|
@@ -65,7 +65,8 @@ ja:
|
|
|
65
65
|
empty: キューが見つかりません
|
|
66
66
|
headers:
|
|
67
67
|
depth: 深さ
|
|
68
|
-
|
|
68
|
+
oldest_claimable: 取得可能な最古 (秒)
|
|
69
|
+
parked: 待機中
|
|
69
70
|
queue: キュー
|
|
70
71
|
total: 合計
|
|
71
72
|
visible: 表示中
|
|
@@ -475,7 +476,8 @@ ja:
|
|
|
475
476
|
actions: アクション
|
|
476
477
|
depth: 深さ
|
|
477
478
|
newest: 最新 (秒)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: 取得可能な最古 (秒)
|
|
480
|
+
parked: 待機中
|
|
479
481
|
queue: キュー
|
|
480
482
|
total_ever: 合計数
|
|
481
483
|
visible: 表示中
|
|
@@ -516,6 +518,9 @@ ja:
|
|
|
516
518
|
scheduled: スケジュール済み:
|
|
517
519
|
timezone: タイムゾーン:
|
|
518
520
|
visible_at: 表示可能日時:
|
|
521
|
+
oldest: '最古:'
|
|
522
|
+
oldest_claimable: '取得可能な最古:'
|
|
523
|
+
parked: '待機中:'
|
|
519
524
|
pause: 一時停止
|
|
520
525
|
pause_confirm: 処理を一時停止しますか?
|
|
521
526
|
purge_confirm: すべてのメッセージを削除しますか?
|
data/config/locales/nb.yml
CHANGED
|
@@ -65,7 +65,8 @@ nb:
|
|
|
65
65
|
empty: Ingen køer funnet
|
|
66
66
|
headers:
|
|
67
67
|
depth: Dybde
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Eldste tilgjengelig (s)
|
|
69
|
+
parked: Parkert
|
|
69
70
|
queue: Kø
|
|
70
71
|
total: Totalt
|
|
71
72
|
visible: Synlig
|
|
@@ -475,7 +476,8 @@ nb:
|
|
|
475
476
|
actions: Handlinger
|
|
476
477
|
depth: Dybde
|
|
477
478
|
newest: Nyeste (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Eldste tilgjengelig (s)
|
|
480
|
+
parked: Parkert
|
|
479
481
|
queue: Kø
|
|
480
482
|
total_ever: Totalt noensinne
|
|
481
483
|
visible: Synlig
|
|
@@ -516,6 +518,9 @@ nb:
|
|
|
516
518
|
scheduled: 'Planlagt:'
|
|
517
519
|
timezone: 'Tidssone:'
|
|
518
520
|
visible_at: 'Synlig fra:'
|
|
521
|
+
oldest: 'Eldste:'
|
|
522
|
+
oldest_claimable: 'Eldste tilgjengelig:'
|
|
523
|
+
parked: 'Parkert:'
|
|
519
524
|
pause: Pause
|
|
520
525
|
pause_confirm: Pause behandling?
|
|
521
526
|
purge_confirm: Rens alle meldinger?
|
data/config/locales/nl.yml
CHANGED
|
@@ -65,7 +65,8 @@ nl:
|
|
|
65
65
|
empty: Geen wachtrijen gevonden
|
|
66
66
|
headers:
|
|
67
67
|
depth: Diepte
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Oudste beschikbaar (s)
|
|
69
|
+
parked: Geparkeerd
|
|
69
70
|
queue: Wachtrij
|
|
70
71
|
total: Totaal
|
|
71
72
|
visible: Zichtbaar
|
|
@@ -475,7 +476,8 @@ nl:
|
|
|
475
476
|
actions: Acties
|
|
476
477
|
depth: Diepte
|
|
477
478
|
newest: Nieuwste (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Oudste beschikbaar (s)
|
|
480
|
+
parked: Geparkeerd
|
|
479
481
|
queue: Wachtrij
|
|
480
482
|
total_ever: Totaal ooit
|
|
481
483
|
visible: Zichtbaar
|
|
@@ -516,6 +518,9 @@ nl:
|
|
|
516
518
|
scheduled: 'Gepland:'
|
|
517
519
|
timezone: 'Tijdzone:'
|
|
518
520
|
visible_at: 'Zichtbaar op:'
|
|
521
|
+
oldest: 'Oudste:'
|
|
522
|
+
oldest_claimable: 'Oudste beschikbaar:'
|
|
523
|
+
parked: 'Geparkeerd:'
|
|
519
524
|
pause: Pauzeren
|
|
520
525
|
pause_confirm: Verwerking pauzeren?
|
|
521
526
|
purge_confirm: Alle berichten verwijderen?
|
data/config/locales/pt.yml
CHANGED
|
@@ -65,7 +65,8 @@ pt:
|
|
|
65
65
|
empty: Nenhuma fila encontrada
|
|
66
66
|
headers:
|
|
67
67
|
depth: Profundidade
|
|
68
|
-
|
|
68
|
+
oldest_claimable: Mais antigo disponível (s)
|
|
69
|
+
parked: Adiados
|
|
69
70
|
queue: Fila
|
|
70
71
|
total: Total
|
|
71
72
|
visible: Visível
|
|
@@ -475,7 +476,8 @@ pt:
|
|
|
475
476
|
actions: Ações
|
|
476
477
|
depth: Profundidade
|
|
477
478
|
newest: Mais novo (s)
|
|
478
|
-
|
|
479
|
+
oldest_claimable: Mais antigo disponível (s)
|
|
480
|
+
parked: Adiados
|
|
479
481
|
queue: Fila
|
|
480
482
|
total_ever: Total de todos os tempos
|
|
481
483
|
visible: Visível
|
|
@@ -516,6 +518,9 @@ pt:
|
|
|
516
518
|
scheduled: 'Agendado:'
|
|
517
519
|
timezone: 'Fuso horário:'
|
|
518
520
|
visible_at: 'Visível em:'
|
|
521
|
+
oldest: 'Mais antigo:'
|
|
522
|
+
oldest_claimable: 'Mais antigo disponível:'
|
|
523
|
+
parked: 'Adiados:'
|
|
519
524
|
pause: Pausar
|
|
520
525
|
pause_confirm: Pausar processamento?
|
|
521
526
|
purge_confirm: Limpar todas as mensagens?
|