pgbus 0.18.2 → 0.19.1
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 +345 -142
- data/README.md +49 -0
- data/app/controllers/pgbus/deploy_tasks_controller.rb +38 -0
- data/app/controllers/pgbus/frontends_controller.rb +10 -8
- data/app/helpers/pgbus/application_helper.rb +4 -3
- data/app/helpers/pgbus/deploy_tasks_helper.rb +77 -0
- data/app/helpers/pgbus/processes_helper.rb +1 -1
- data/app/models/pgbus/deploy_task.rb +104 -0
- data/app/models/pgbus/deploy_task_run.rb +45 -0
- data/app/models/pgbus/job_stat.rb +4 -0
- data/app/models/pgbus/processed_event.rb +3 -1
- data/app/models/pgbus/stream_queue.rb +34 -3
- data/app/models/pgbus/stream_stat.rb +3 -0
- data/app/views/layouts/pgbus/application.html.erb +2 -0
- data/app/views/pgbus/deploy_tasks/_rollover_banner.html.erb +25 -0
- data/app/views/pgbus/deploy_tasks/_tasks_table.html.erb +71 -0
- data/app/views/pgbus/deploy_tasks/index.html.erb +6 -0
- data/app/views/pgbus/deploy_tasks/show.html.erb +134 -0
- data/config/i18n-tasks.yml +2 -0
- data/config/locales/da.yml +97 -0
- data/config/locales/de.yml +97 -0
- data/config/locales/en.yml +97 -0
- data/config/locales/es.yml +97 -0
- data/config/locales/fi.yml +97 -0
- data/config/locales/fr.yml +97 -0
- data/config/locales/it.yml +97 -0
- data/config/locales/ja.yml +97 -0
- data/config/locales/nb.yml +97 -0
- data/config/locales/nl.yml +97 -0
- data/config/locales/pt.yml +97 -0
- data/config/locales/sv.yml +97 -0
- data/config/routes.rb +7 -0
- data/lib/generators/pgbus/add_deploy_tasks_generator.rb +51 -0
- data/lib/generators/pgbus/templates/add_deploy_tasks_tables.rb.erb +43 -0
- data/lib/generators/pgbus/templates/deploy_tasks.yml.erb +42 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +43 -0
- data/lib/pgbus/active_job/current_attributes.rb +3 -1
- data/lib/pgbus/batch.rb +17 -9
- data/lib/pgbus/cli.rb +3 -0
- data/lib/pgbus/client/read_after.rb +10 -1
- data/lib/pgbus/client.rb +54 -9
- data/lib/pgbus/concurrency.rb +3 -0
- data/lib/pgbus/configuration/deploy_settings.rb +46 -0
- data/lib/pgbus/configuration.rb +87 -9
- data/lib/pgbus/current_attributes.rb +3 -1
- data/lib/pgbus/dedup_cache.rb +15 -7
- data/lib/pgbus/deploy/revision.rb +59 -0
- data/lib/pgbus/deploy/rollover.rb +65 -0
- data/lib/pgbus/deploy/runner.rb +259 -0
- data/lib/pgbus/deploy/task.rb +105 -0
- data/lib/pgbus/engine.rb +28 -0
- data/lib/pgbus/event_bus/handler.rb +16 -4
- data/lib/pgbus/execution_pools/async_pool.rb +23 -2
- data/lib/pgbus/execution_pools/thread_pool.rb +22 -0
- data/lib/pgbus/execution_pools.rb +26 -0
- data/lib/pgbus/generators/migration_detector.rb +11 -0
- data/lib/pgbus/instrumentation.rb +3 -0
- data/lib/pgbus/integrations/appsignal/probe.rb +17 -9
- data/lib/pgbus/integrations/appsignal/subscriber.rb +30 -18
- data/lib/pgbus/integrations/appsignal.rb +26 -16
- data/lib/pgbus/mcp/rack_app.rb +1 -1
- data/lib/pgbus/mcp/server.rb +2 -1
- data/lib/pgbus/mcp/tools/deploy_tasks_tool.rb +86 -0
- data/lib/pgbus/mcp.rb +1 -0
- data/lib/pgbus/metrics/subscriber.rb +35 -21
- data/lib/pgbus/outbox/poller.rb +1 -0
- data/lib/pgbus/process/consumer.rb +2 -1
- data/lib/pgbus/process/consumer_priority.rb +1 -1
- data/lib/pgbus/process/dispatcher.rb +18 -1
- data/lib/pgbus/process/heartbeat.rb +7 -2
- data/lib/pgbus/process/lifecycle.rb +5 -5
- data/lib/pgbus/process/notify_listener.rb +32 -18
- data/lib/pgbus/process/supervisor.rb +35 -3
- data/lib/pgbus/process/wake_pipe.rb +4 -1
- data/lib/pgbus/process/worker.rb +5 -2
- data/lib/pgbus/recurring/scheduler.rb +54 -20
- data/lib/pgbus/retry_backoff.rb +3 -0
- data/lib/pgbus/shareable.rb +70 -0
- data/lib/pgbus/streams/broadcastable_override.rb +3 -0
- data/lib/pgbus/streams/presence.rb +6 -7
- data/lib/pgbus/streams.rb +38 -27
- data/lib/pgbus/testing.rb +19 -3
- data/lib/pgbus/uniqueness.rb +3 -0
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/visibility_heartbeat.rb +51 -34
- data/lib/pgbus/web/authentication.rb +19 -2
- data/lib/pgbus/web/data_source/deploy_task_list.rb +114 -0
- data/lib/pgbus/web/data_source/list_counts.rb +10 -7
- data/lib/pgbus/web/data_source.rb +1 -0
- data/lib/pgbus/web/deploy_task_state.rb +51 -0
- data/lib/pgbus/web/health_server.rb +4 -1
- data/lib/pgbus/web/process_activity.rb +1 -1
- data/lib/pgbus/web/streamer/heartbeat.rb +4 -1
- data/lib/pgbus/web/streamer/hub_client.rb +4 -1
- data/lib/pgbus/web/streamer/listener.rb +10 -1
- data/lib/pgbus/web/streamer/master_hub.rb +11 -5
- data/lib/pgbus/web/streamer/master_hub_boot.rb +3 -1
- data/lib/pgbus/web/streamer/outbound_pump.rb +4 -1
- data/lib/pgbus/web/streamer/peer_close_watcher.rb +4 -1
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +13 -1
- data/lib/pgbus.rb +92 -30
- data/sig/pgbus/active_job/adapter.rbs +57 -0
- data/sig/pgbus/client.rbs +78 -0
- data/sig/pgbus/configuration.rbs +326 -0
- data/sig/pgbus/streams/stream.rbs +25 -0
- data/sig/pgbus/streams.rbs +22 -0
- data/sig/pgbus.rbs +73 -0
- metadata +27 -1
data/README.md
CHANGED
|
@@ -139,6 +139,8 @@ The capsule string DSL is the shortest form for the common case. Use `c.capsule`
|
|
|
139
139
|
|
|
140
140
|
Configuration is validated eagerly: `Pgbus.configure` runs `Configuration#validate!` right after your block yields, so an invalid value (`visibility_timeout = 0`, for example) raises `Pgbus::ConfigurationError` at boot instead of failing later inside a worker. Set `c.eager_validation = false` for the rare setup that intentionally holds a transiently-invalid config across sequential `configure` calls.
|
|
141
141
|
|
|
142
|
+
Configuration is finalized at boot: once the app's initializers have run, the engine calls `Pgbus.finalize!`, which freezes every plain-data setting (strings, symbols, numbers, regexps, and arrays and hashes of those) into a Ractor-shareable value. Callables and the logger are left as they are. Setters keep working, and a later `Pgbus.configure` finalizes again, but changing a collection in place after boot (`Pgbus.configuration.streams_durable_patterns << "x"`) raises `FrozenError`: assign a new value instead. `c.error_reporters << reporter` keeps working. Without Rails, each `Pgbus.configure` finalizes at its end; `pgbus start` finalizes again once its flags are applied. Call `Pgbus.finalize!` yourself only when you configure pgbus through setters alone. A value that cannot be made shareable (a lambda that closes over app state, a logger, a filter list holding a proc) never makes finalizing raise: it is kept as it is, for the main Ractor. More in [docs/concurrency.md](docs/concurrency.md).
|
|
143
|
+
|
|
142
144
|
> **Upgrading from an older pgbus?** Run `rails generate pgbus:update`. It inspects your live database and adds any missing pgbus migrations to `db/migrate` (or `db/pgbus_migrate` if you use `connects_to`). The generator detects your separate-database config automatically from `Pgbus.configuration.connects_to` or by scanning the initializer / `config/application.rb`, so you don't have to re-specify `--database=pgbus` every time.
|
|
143
145
|
>
|
|
144
146
|
> Useful flags: `--dry-run` (print the plan without creating files), `--skip-migrations`, `--quiet`. Running it on a database with no pgbus tables at all will redirect you to `pgbus:install` instead of stacking individual add_* migrations.
|
|
@@ -691,6 +693,8 @@ end
|
|
|
691
693
|
|
|
692
694
|
**Not recommended for:** CPU-bound jobs (image processing, heavy computation). These block the single reactor thread and should use thread mode.
|
|
693
695
|
|
|
696
|
+
Which state is fiber-local (broadcast options, the batch a job joins, the uniqueness key an enqueue holds) and what that means when work hops between fibers is in [docs/concurrency.md](docs/concurrency.md), with the threads each process runs and pgbus's Ractor readiness.
|
|
697
|
+
|
|
694
698
|
## Routing and ordering
|
|
695
699
|
|
|
696
700
|
How messages flow between producers and the workers that handle them: priority sub-queues, consumer priority for active/standby workers, and single-active-consumer for strict ordering.
|
|
@@ -1213,6 +1217,7 @@ All tools are read-only — no tool mutates state, and there is no raw-SQL passt
|
|
|
1213
1217
|
| `pgbus_dlq` / `pgbus_dlq_detail` | Dead-letter inspection. Paginated. |
|
|
1214
1218
|
| `pgbus_locks` | Active uniqueness locks (the leaked-lock diagnostic). Paginated (`page`, `per_page` max 100; returns `total`, capped at 10,000, and `has_more`). |
|
|
1215
1219
|
| `pgbus_throughput` / `pgbus_stats` | Recent throughput time series and status counts. |
|
|
1220
|
+
| `pgbus_deploy_tasks` | Deploy tasks, their state and the rollover status (read-only). |
|
|
1216
1221
|
| `pgbus_recurring` | Recurring task schedule + last/next run times. All tasks unless `page` / `per_page` is given (then `total` is capped at 10,000); returns `total`, `has_more`. |
|
|
1217
1222
|
|
|
1218
1223
|
#### Security
|
|
@@ -2212,6 +2217,42 @@ pgbus start --workers-only --capsule default
|
|
|
2212
2217
|
|
|
2213
2218
|
The capsule name is the `:name` you passed to `c.capsule` in your initializer (or the first queue token when using the string DSL).
|
|
2214
2219
|
|
|
2220
|
+
### Deploy tasks
|
|
2221
|
+
|
|
2222
|
+
One-time work owed by a release (a backfill, a data fix, a cache warm). A deploy task runs once per `(key, version)`, only after every live pgbus process reports the new app revision and at least one worker on that revision is alive. A scheduled run never happens on old code during the deploy overlap window (Run now, below, is deliberately ungated), two schedulers (multi-host, overlap) never both fire it, and an owed task is not forgotten. It is not exactly-once: a failure between the enqueue and recording the run can repeat it after a restart, so keep deploy tasks idempotent.
|
|
2223
|
+
|
|
2224
|
+
```yaml
|
|
2225
|
+
# config/deploy_tasks.yml
|
|
2226
|
+
production:
|
|
2227
|
+
backfill_order_totals:
|
|
2228
|
+
class: BackfillOrderTotalsJob # or command: "Orders.backfill_totals!"
|
|
2229
|
+
version: 2 # bump to run again after the next rollover (default 1)
|
|
2230
|
+
queue: maintenance
|
|
2231
|
+
args: [{ batch_size: 500 }]
|
|
2232
|
+
description: Recompute totals after the currency migration
|
|
2233
|
+
warm_fragment_caches:
|
|
2234
|
+
command: "FragmentCacheWarmer.run"
|
|
2235
|
+
every_deploy: true # runs after every rollover, dedup'd on the app revision
|
|
2236
|
+
```
|
|
2237
|
+
|
|
2238
|
+
The file has the shape of `config/recurring.yml` minus `schedule:`, plus `version:` and `every_deploy:`. It is environment-scoped or flat, ERB is allowed, and `config.deploy_tasks_files` lists several files (explicit files replace the default). A task is a `class:` (an ActiveJob class; anything else is skipped with a warning at scheduler boot) or a `command:` (`Const.method`, run by `Pgbus::Recurring::CommandJob`), with optional `queue`, `args`, `priority` and `description`.
|
|
2239
|
+
|
|
2240
|
+
- **`version:`** (default `"1"`, compared as a string): bump it in the same commit as a new data fix to run the task again after that deploy.
|
|
2241
|
+
- **`every_deploy: true`**: runs after every rollover, dedup'd on the revision, so a rollback deploy re-fires it and a scheduler crash-restart does not. `version` is ignored (a warning is logged).
|
|
2242
|
+
- **Revision**, first non-blank wins: `config.app_revision` (a String or a callable) → `ENV["PGBUS_REVISION"]` → `ENV["DASH_VERSION"]` → `ENV["KAMAL_VERSION"]` → the `REVISION` file in `Rails.root` (Capistrano). dash and Kamal need no configuration. Every pgbus process stamps it into its heartbeat row (`metadata["revision"]`, shown in the Processes page's expanded row). Without a revision the gate stays closed: tasks wait, the scheduler warns once at boot, and only Run now enqueues.
|
|
2243
|
+
- **The gate** (checked by the scheduler at most every 5 s): live processes are those with a heartbeat in the last 5 minutes. It is closed while any live process reports another revision (a process with no revision, such as a pre-upgrade pgbus, counts as lagging) or while no live worker runs the new revision. A gracefully stopped process deletes its row at once, so a clean dash/Kamal deploy opens the gate within a scheduler tick after the last old container drains; a hard-killed old container keeps it closed until its row goes stale (5 minutes).
|
|
2244
|
+
- **Who runs it**: the scheduler that runs recurring tasks. The supervisor forks it when recurring or deploy tasks are configured (`config.deploy_tasks_enabled`, default `true`); `--scheduler-only` works as before. At boot it syncs the declared tasks into `pgbus_deploy_tasks` and arms the owed ones. A compare-and-set claim on the row means two schedulers never both fire.
|
|
2245
|
+
- **"Ran" means enqueued**, like recurring: the task goes through ActiveJob `perform_later`, so uniqueness, batches, concurrency, priority, retries and the DLQ apply, and the outcome is on the Jobs page. A failed enqueue re-arms the task, logs it, reports it to the error reporters and retries after 5 s, doubling up to 5 minutes (Disable the task to stop it). The run row marks a version as done: if it cannot be written right after a successful enqueue, the next scheduler boot runs the task again, so keep deploy tasks idempotent. Due tasks are enqueued in key order with no completion ordering between them.
|
|
2246
|
+
- **Dashboard**: the "Deploy tasks" page (`/pgbus/deploy_tasks`, plus a show page per task with its configuration and last 25 runs) shows whether the fleet has rolled over to the scheduler's revision, with live process counts per revision, and each task as ran, waiting (and why), due, disabled or never ran. **Run now** enqueues immediately, with no gate and no claim, and records a manual run; it does not cancel a pending rollover run (Disable the task for that). **Enable/Disable** is honoured on every tick.
|
|
2247
|
+
- **History**: `pgbus_deploy_task_runs`, kept for `config.deploy_task_run_retention` (default 90 days, `nil` keeps forever); the dispatcher prunes hourly. Each enqueue emits `pgbus.deploy_task.enqueue` (`task`, `version`, `revision`, `triggered_by`).
|
|
2248
|
+
|
|
2249
|
+
```bash
|
|
2250
|
+
rails generate pgbus:add_deploy_tasks # migration + config/deploy_tasks.yml
|
|
2251
|
+
rails generate pgbus:add_deploy_tasks --database=pgbus # for a separate database
|
|
2252
|
+
```
|
|
2253
|
+
|
|
2254
|
+
`rails generate pgbus:update` detects the missing tables; fresh installs (`pgbus:install`) already have them.
|
|
2255
|
+
|
|
2215
2256
|
### Dashboard
|
|
2216
2257
|
|
|
2217
2258
|
The dashboard is a mountable Rails engine at `/pgbus` with:
|
|
@@ -2286,6 +2327,8 @@ Pgbus uses these tables (created via PGMQ and migrations):
|
|
|
2286
2327
|
| `pgbus_outbox_entries` | Transactional outbox entries pending publication |
|
|
2287
2328
|
| `pgbus_recurring_tasks` | Recurring job definitions |
|
|
2288
2329
|
| `pgbus_recurring_executions` | Recurring job execution history |
|
|
2330
|
+
| `pgbus_deploy_tasks` | Declared deploy tasks, their arm (`armed_at`/`armed_for`) and `enabled` flag |
|
|
2331
|
+
| `pgbus_deploy_task_runs` | Deploy task run history (task_key, version, revision, triggered_by, queue_name, msg_id, run_at) |
|
|
2289
2332
|
| `pgbus_presence_members` | Stream presence tracking (who is subscribed) |
|
|
2290
2333
|
| `pgbus_stream_stats` | Stream broadcast/connect/disconnect metrics (opt-in) |
|
|
2291
2334
|
|
|
@@ -2369,6 +2412,7 @@ Curated headline options for the README. The full operator reference (with types
|
|
|
2369
2412
|
| `zombie_detection` | `true` | Detect and reclaim work from crashed workers |
|
|
2370
2413
|
| `read_timeout` | `30` | Seconds before a single PGMQ read is bounded (libpq `statement_timeout` + `tcp_user_timeout` on a dedicated connection; nil disables) |
|
|
2371
2414
|
| `drain_timeout` | `30` | Seconds to wait for in-flight jobs during graceful shutdown before abandoning them |
|
|
2415
|
+
| `immediate_shutdown_grace` | `2` | On QUIT, seconds a worker or consumer gives running jobs to finish before killing them; killed jobs' ActiveRecord connections are reclaimed |
|
|
2372
2416
|
| `shutdown_timeout` | `drain_timeout + 5` | Seconds the supervisor waits for children after TERM before SIGKILL; an orchestrator's stop grace period must exceed it |
|
|
2373
2417
|
| `stall_threshold` | `300` | Seconds without progress before a worker is considered stalled |
|
|
2374
2418
|
| `priority_levels` | `nil` | Number of priority sub-queues (nil = disabled, 2-10) |
|
|
@@ -2383,6 +2427,11 @@ Curated headline options for the README. The full operator reference (with types
|
|
|
2383
2427
|
| `outbox_batch_size` | `100` | Max entries per outbox poll cycle |
|
|
2384
2428
|
| `outbox_retention` | `1.day` | How long to keep published outbox entries. Accepts seconds, Duration, or `nil` to disable cleanup |
|
|
2385
2429
|
| `idempotency_ttl` | `7.days` | How long to keep processed event records. Accepts seconds, Duration, or `nil` to disable cleanup |
|
|
2430
|
+
| `app_revision` | `nil` | App revision for deploy tasks: a String or a callable returning one. Falls back to `PGBUS_REVISION`, `DASH_VERSION`, `KAMAL_VERSION`, then the `REVISION` file |
|
|
2431
|
+
| `deploy_tasks` | `nil` | Hash of deploy tasks, same shape as `config/deploy_tasks.yml` |
|
|
2432
|
+
| `deploy_tasks_files` | `nil` | Array of deploy-task YAML files (replaces the default `config/deploy_tasks.yml`) |
|
|
2433
|
+
| `deploy_tasks_enabled` | `true` | Fork the scheduler for deploy tasks; set `false` to turn them off |
|
|
2434
|
+
| `deploy_task_run_retention` | `90.days` | How long to keep deploy task run history. Accepts seconds, Duration, or `nil` to keep forever |
|
|
2386
2435
|
| `allowed_global_id_models` | `nil` | Allowlist of Class/Module models permitted as GlobalID **job arguments** and EventBus payloads. `nil` = allow-all (default). `[]` = deny-all. Apps with ActiveStorage should include `ActiveStorage::Blob` (and related models). |
|
|
2387
2436
|
| `base_controller_class` | `"::ActionController::Base"` | Base class for dashboard controllers (string, constantized at load time) |
|
|
2388
2437
|
| `return_to_app_url` | `nil` | URL for "back to app" button in dashboard nav (nil hides the button) |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class DeployTasksController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@page = page_param
|
|
7
|
+
@per_page = per_page
|
|
8
|
+
@deploy_tasks = data_source.deploy_tasks(page: @page, per_page: @per_page)
|
|
9
|
+
@deploy_tasks_count = data_source.list_count(:deploy_tasks)
|
|
10
|
+
@rollover = data_source.deploy_rollover
|
|
11
|
+
render_frame("pgbus/deploy_tasks/tasks_table") if params[:frame] == "deploy_tasks"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
@task = data_source.deploy_task(params[:id])
|
|
16
|
+
return redirect_to pgbus.deploy_tasks_path, alert: t("pgbus.deploy_tasks.not_found") unless @task
|
|
17
|
+
|
|
18
|
+
@rollover = data_source.deploy_rollover
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def toggle
|
|
22
|
+
result = data_source.toggle_deploy_task(params[:id])
|
|
23
|
+
if result
|
|
24
|
+
redirect_to pgbus.deploy_tasks_path, notice: t("pgbus.deploy_tasks.toggle.#{result}")
|
|
25
|
+
else
|
|
26
|
+
redirect_to pgbus.deploy_tasks_path, alert: t("pgbus.deploy_tasks.toggle.failed")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def enqueue
|
|
31
|
+
if data_source.enqueue_deploy_task_now(params[:id]) == :enqueued
|
|
32
|
+
redirect_to pgbus.deploy_tasks_path, notice: t("pgbus.deploy_tasks.enqueue.enqueued")
|
|
33
|
+
else
|
|
34
|
+
redirect_to pgbus.deploy_tasks_path, alert: t("pgbus.deploy_tasks.enqueue.failed")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -5,27 +5,29 @@ module Pgbus
|
|
|
5
5
|
protect_from_forgery with: :exception
|
|
6
6
|
skip_after_action :verify_same_origin_request, raise: false
|
|
7
7
|
|
|
8
|
-
FRONTEND_ROOT = Engine.root.join("app", "frontend", "pgbus")
|
|
8
|
+
FRONTEND_ROOT = Engine.root.join("app", "frontend", "pgbus").freeze
|
|
9
9
|
|
|
10
10
|
STATIC_ASSETS = {
|
|
11
11
|
css: {
|
|
12
|
-
style: FRONTEND_ROOT.join("style.css")
|
|
13
|
-
}
|
|
12
|
+
style: FRONTEND_ROOT.join("style.css").freeze
|
|
13
|
+
}.freeze
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
16
16
|
# Modules are served minified (rake frontend:js), each with its .min.js.map
|
|
17
17
|
# beside it. ApexCharts ships minified already; charts.js imports it on
|
|
18
18
|
# demand, so only Insights downloads it.
|
|
19
19
|
MODULE_OVERRIDES = {
|
|
20
|
-
application: FRONTEND_ROOT.join("application.min.js"),
|
|
21
|
-
turbo: FRONTEND_ROOT.join("vendor", "turbo.min.js"),
|
|
22
|
-
apexcharts: FRONTEND_ROOT.join("vendor", "apexcharts.js")
|
|
20
|
+
application: FRONTEND_ROOT.join("application.min.js").freeze,
|
|
21
|
+
turbo: FRONTEND_ROOT.join("vendor", "turbo.min.js").freeze,
|
|
22
|
+
apexcharts: FRONTEND_ROOT.join("vendor", "apexcharts.js").freeze
|
|
23
23
|
}.freeze
|
|
24
24
|
|
|
25
|
+
# An idempotent memo: concurrent first requests glob the same files and
|
|
26
|
+
# build equal Hashes; whichever is stored, every reader gets a frozen one.
|
|
25
27
|
def self.js_modules
|
|
26
|
-
@js_modules ||= FRONTEND_ROOT.join("modules").glob("*.min.js").sort
|
|
28
|
+
@js_modules ||= FRONTEND_ROOT.join("modules").glob("*.min.js").sort # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
27
29
|
.to_h { |f| [f.basename(".min.js").to_s.to_sym, f] }
|
|
28
|
-
.merge(MODULE_OVERRIDES)
|
|
30
|
+
.merge(MODULE_OVERRIDES).freeze
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
CURRENT_VERSION = Pgbus::VERSION.tr(".", "-").freeze
|
|
@@ -94,10 +94,11 @@ module Pgbus
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
BATCH_BADGE_BASE = "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
|
|
97
|
+
# Interpolated strings are not frozen by the magic comment: freeze each.
|
|
97
98
|
BATCH_BADGE_CSS = {
|
|
98
|
-
"finished" => "#{BATCH_BADGE_BASE} bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
|
99
|
-
"processing" => "#{BATCH_BADGE_BASE} bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
|
100
|
-
"pending" => "#{BATCH_BADGE_BASE} bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
|
99
|
+
"finished" => "#{BATCH_BADGE_BASE} bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400".freeze,
|
|
100
|
+
"processing" => "#{BATCH_BADGE_BASE} bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400".freeze,
|
|
101
|
+
"pending" => "#{BATCH_BADGE_BASE} bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400".freeze
|
|
101
102
|
}.freeze
|
|
102
103
|
|
|
103
104
|
def pgbus_batch_status_badge(status)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# The Deploy tasks page's words (issue #536): DeployTaskState results and
|
|
5
|
+
# the rollover banner as badges and sentences. Moments go through
|
|
6
|
+
# pgbus_time; the _html keys let the <time> and <code> through, translate
|
|
7
|
+
# escapes every other argument.
|
|
8
|
+
module DeployTasksHelper
|
|
9
|
+
BANNER_TONE_CSS = {
|
|
10
|
+
green: "bg-green-50 text-green-800 ring-green-200 dark:bg-green-900/30 dark:text-green-200 dark:ring-green-800",
|
|
11
|
+
yellow: "bg-yellow-50 text-yellow-800 ring-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-200 dark:ring-yellow-800",
|
|
12
|
+
red: "bg-red-50 text-red-800 ring-red-200 dark:bg-red-900/30 dark:text-red-200 dark:ring-red-800",
|
|
13
|
+
gray: "bg-gray-50 text-gray-700 ring-gray-200 dark:bg-gray-900/40 dark:text-gray-200 dark:ring-gray-700"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def pgbus_deploy_state_badge(result)
|
|
17
|
+
tag.span(t("pgbus.deploy_tasks.states.#{result.state}"),
|
|
18
|
+
class: "#{ApplicationHelper::BATCH_BADGE_BASE} #{ApplicationHelper::JOB_STATE_BADGE_CSS[result.badge_tone]}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# "Waiting for rollover: 2 processes are not on abc123 yet. · ran 1m ago (manual)"
|
|
22
|
+
def pgbus_deploy_task_reason(result)
|
|
23
|
+
reason = translate("pgbus.deploy_tasks.reasons.#{result.reason_key}_html", **pgbus_reason_args(result.reason_args))
|
|
24
|
+
return reason unless result.manual_args
|
|
25
|
+
|
|
26
|
+
safe_join([reason, " ", translate("pgbus.deploy_tasks.reasons.manual_html", **pgbus_reason_args(result.manual_args))])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pgbus_deploy_rollover_key(rollover)
|
|
30
|
+
return "unavailable" unless rollover
|
|
31
|
+
return "no_scheduler" unless rollover[:scheduler]
|
|
32
|
+
|
|
33
|
+
rollover[:reason].to_s
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pgbus_deploy_rollover_tone(rollover)
|
|
37
|
+
case pgbus_deploy_rollover_key(rollover)
|
|
38
|
+
when "ready" then :green
|
|
39
|
+
when "no_scheduler", "unavailable" then :red
|
|
40
|
+
when "no_revision" then :gray
|
|
41
|
+
else :yellow
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def pgbus_deploy_rollover_sentence(rollover)
|
|
46
|
+
key = pgbus_deploy_rollover_key(rollover)
|
|
47
|
+
args = { revision: rollover&.dig(:revision) }
|
|
48
|
+
args[:count] = rollover[:lagging].size if key == "lagging"
|
|
49
|
+
translate("pgbus.deploy_tasks.banner.#{key}_html", **args)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def pgbus_deploy_lagging_process(process)
|
|
53
|
+
t("pgbus.deploy_tasks.banner.lagging_process",
|
|
54
|
+
kind: process[:kind], hostname: process[:hostname], pid: process[:pid],
|
|
55
|
+
revision: process[:revision] || t("pgbus.deploy_tasks.banner.no_revision_label"))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# "6 on 8d41b07 · 2 on 3f2a9c1", the most common revision first.
|
|
59
|
+
def pgbus_deploy_live_counts(rollover)
|
|
60
|
+
counts = rollover&.dig(:live_by_revision) || {}
|
|
61
|
+
parts = counts.sort_by { |revision, count| [-count, revision.to_s] }.map do |revision, count|
|
|
62
|
+
t("pgbus.deploy_tasks.banner.live_count", count: count,
|
|
63
|
+
revision: revision || t("pgbus.deploy_tasks.banner.no_revision_label"))
|
|
64
|
+
end
|
|
65
|
+
safe_join(parts, ProcessesHelper::PART_SEPARATOR)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def pgbus_deploy_version(task)
|
|
69
|
+
task[:every_deploy] ? t("pgbus.deploy_tasks.every_deploy") : t("pgbus.deploy_tasks.version", version: task[:version])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The newer of the last rollover and the last manual run.
|
|
73
|
+
def pgbus_deploy_last_run(task)
|
|
74
|
+
[task[:last_rollover_run], task[:last_manual_run]].compact.max_by { |run| Web::JobState.time(run[:run_at]).to_f }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -51,7 +51,7 @@ module Pgbus
|
|
|
51
51
|
def pgbus_process_raw_metadata(metadata)
|
|
52
52
|
return {} unless metadata.is_a?(Hash)
|
|
53
53
|
|
|
54
|
-
metadata.reject { |k,
|
|
54
|
+
metadata.reject { |k, v| v.nil? || RENDERED_METADATA_KEYS.include?(k.to_s) }
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
private
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# A declared deploy task and its pending arm (issue #536). armed_at/armed_for
|
|
5
|
+
# is a row-level compare-and-set: sync arms an owed task, the scheduler's
|
|
6
|
+
# claim! clears the arm before it enqueues, and a failed enqueue re-arms it.
|
|
7
|
+
class DeployTask < BusRecord
|
|
8
|
+
self.table_name = "pgbus_deploy_tasks"
|
|
9
|
+
|
|
10
|
+
validates :key, presence: true, uniqueness: true
|
|
11
|
+
validates :version, presence: true
|
|
12
|
+
|
|
13
|
+
scope :static_tasks, -> { where(static: true) }
|
|
14
|
+
scope :enabled, -> { where(enabled: true) }
|
|
15
|
+
scope :armed, -> { where.not(armed_at: nil) }
|
|
16
|
+
|
|
17
|
+
# Any enabled task waiting to fire: the runner's cheap per-tick check.
|
|
18
|
+
def self.pending?
|
|
19
|
+
enabled.armed.exists?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Upserts every declared task (valid Deploy::Task objects) and arms the
|
|
23
|
+
# owed ones, then removes static rows no longer declared. Never disarms
|
|
24
|
+
# and never touches enabled; an arm already held keeps its armed_at and
|
|
25
|
+
# takes the new armed_for (a later bump supersedes an unfired one).
|
|
26
|
+
def self.sync_from_config!(tasks, revision:, now: Time.current)
|
|
27
|
+
transaction do
|
|
28
|
+
tasks.each { |task| sync_task!(task, revision: revision, now: now) }
|
|
29
|
+
static_tasks.where.not(key: tasks.map(&:key)).delete_all
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Owed-ness comes from the rollover runs, never from the stored row: any
|
|
34
|
+
# scheduler that boots syncs, including an old-revision one.
|
|
35
|
+
def self.owed?(task, revision)
|
|
36
|
+
return !DeployTaskRun.rollover_run_for?(task.key, task.version) unless task.every_deploy
|
|
37
|
+
return false if revision.nil?
|
|
38
|
+
|
|
39
|
+
DeployTaskRun.last_rollover_for(task.key)&.revision != revision
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.sync_task!(task, revision:, now:)
|
|
43
|
+
record = find_or_initialize_by(key: task.key)
|
|
44
|
+
record.assign_attributes(
|
|
45
|
+
class_name: task.class_name, command: task.command, arguments: JSON.generate(task.arguments),
|
|
46
|
+
queue_name: task.queue_name, priority: task.priority, version: task.version,
|
|
47
|
+
every_deploy: task.every_deploy, description: task.description, static: true
|
|
48
|
+
)
|
|
49
|
+
if owed?(task, revision)
|
|
50
|
+
record.armed_at ||= now
|
|
51
|
+
record.armed_for = task.armed_for(revision)
|
|
52
|
+
end
|
|
53
|
+
record.save!
|
|
54
|
+
end
|
|
55
|
+
private_class_method :sync_task!
|
|
56
|
+
|
|
57
|
+
# true when this caller took the arm; false when another scheduler took
|
|
58
|
+
# it first or the arm is for another version or revision.
|
|
59
|
+
def claim!(armed_for:, now: Time.current) # rubocop:disable Naming/PredicateMethod -- compare-and-set that reports whether it won
|
|
60
|
+
self.class.armed.where(id: id, armed_for: armed_for).update_all(armed_at: nil, updated_at: now) == 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Re-stamps a held arm for +armed_for+, if it is still armed for what
|
|
64
|
+
# this row was read with. The runner does this when its gate is open on
|
|
65
|
+
# its own revision and its own version is owed: the other arm belongs to
|
|
66
|
+
# code no live process runs.
|
|
67
|
+
def take_over!(armed_for:, now: Time.current) # rubocop:disable Naming/PredicateMethod -- compare-and-set, like #claim!
|
|
68
|
+
self.class.armed.where(id: id, armed_for: self.armed_for).update_all(armed_for: armed_for, updated_at: now) == 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Restores this caller's claimed arm after a failed enqueue. Compare-and-set:
|
|
72
|
+
# only while the arm is still cleared and still for +armed_for+, so a
|
|
73
|
+
# failed enqueue never overwrites an arm another scheduler set meanwhile.
|
|
74
|
+
def rearm!(armed_for:, now: Time.current)
|
|
75
|
+
self.class.where(id: id, armed_at: nil, armed_for: armed_for).update_all(armed_at: now, updated_at: now)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def armed?
|
|
79
|
+
!armed_at.nil?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# For display: a corrupt value shows as no arguments.
|
|
83
|
+
def parsed_arguments
|
|
84
|
+
decoded_arguments
|
|
85
|
+
rescue JSON::ParserError
|
|
86
|
+
[]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# For Run now: a corrupt value raises (the enqueue reports :failed and
|
|
90
|
+
# logs it) rather than running the job without its arguments.
|
|
91
|
+
def to_task
|
|
92
|
+
Deploy::Task.new(
|
|
93
|
+
key: key, class_name: class_name, command: command, version: version, every_deploy: every_deploy,
|
|
94
|
+
queue_name: queue_name, arguments: decoded_arguments, priority: priority, description: description
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def decoded_arguments
|
|
101
|
+
arguments.blank? ? [] : Array(JSON.parse(arguments))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
# One enqueue of a deploy task (issue #536): by the scheduler after a
|
|
5
|
+
# rollover, or by "Run now" on the dashboard. Rollover runs are what
|
|
6
|
+
# DeployTask.owed? reads; manual runs never satisfy an obligation.
|
|
7
|
+
class DeployTaskRun < BusRecord
|
|
8
|
+
self.table_name = "pgbus_deploy_task_runs"
|
|
9
|
+
|
|
10
|
+
ROLLOVER = "rollover"
|
|
11
|
+
MANUAL = "manual"
|
|
12
|
+
|
|
13
|
+
validates :task_key, :version, :triggered_by, :run_at, presence: true
|
|
14
|
+
|
|
15
|
+
scope :for_task, ->(key) { where(task_key: key) }
|
|
16
|
+
scope :rollover, -> { where(triggered_by: ROLLOVER) }
|
|
17
|
+
scope :manual, -> { where(triggered_by: MANUAL) }
|
|
18
|
+
scope :recent, ->(limit = 25) { order(id: :desc).limit(limit) }
|
|
19
|
+
scope :older_than, ->(time) { where(run_at: ...time) }
|
|
20
|
+
|
|
21
|
+
# Runs the dispatcher may prune: older than +time+, except the newest
|
|
22
|
+
# rollover run per (task_key, version). That run is what marks a version
|
|
23
|
+
# done (DeployTask.owed?); pruning it would re-arm the task on the next
|
|
24
|
+
# scheduler boot and run it again.
|
|
25
|
+
def self.prunable_before(time)
|
|
26
|
+
older_than(time).where.not(id: rollover.group(:task_key, :version).select("MAX(id)"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.last_rollover_for(key)
|
|
30
|
+
rollover.for_task(key).order(id: :desc).first
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The newest run per (task_key, triggered_by) for +keys+: the dashboard's
|
|
34
|
+
# "last rollover run" and "last manual run" columns, in one query.
|
|
35
|
+
def self.latest_for(keys)
|
|
36
|
+
where(task_key: keys)
|
|
37
|
+
.select("DISTINCT ON (task_key, triggered_by) #{quoted_table_name}.*")
|
|
38
|
+
.order(:task_key, :triggered_by, id: :desc)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.rollover_run_for?(key, version)
|
|
42
|
+
rollover.for_task(key).exists?(version: version.to_s)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -37,6 +37,9 @@ module Pgbus
|
|
|
37
37
|
# false on the first hiccup would permanently disable job stats
|
|
38
38
|
# for the process lifetime, which is a worse failure mode than
|
|
39
39
|
# a few retries. See issue #98 / PR #91 (StreamStat fix).
|
|
40
|
+
# An idempotent schema memo: concurrent first callers probe the same
|
|
41
|
+
# schema and store the same Boolean, so the unlocked read/write is safe.
|
|
42
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
40
43
|
def self.table_exists?
|
|
41
44
|
return @table_exists if defined?(@table_exists)
|
|
42
45
|
|
|
@@ -59,6 +62,7 @@ module Pgbus
|
|
|
59
62
|
Pgbus.logger.debug { "[Pgbus] Failed to check job stat latency columns: #{e.message}" }
|
|
60
63
|
false
|
|
61
64
|
end
|
|
65
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
62
66
|
|
|
63
67
|
# Throughput: jobs per minute bucketed by minute for the last N minutes
|
|
64
68
|
def self.throughput(minutes: 60)
|
|
@@ -21,7 +21,9 @@ module Pgbus
|
|
|
21
21
|
# failure path leaves the message for VT redelivery — and the next
|
|
22
22
|
# delivery probes again.
|
|
23
23
|
def completion_column?
|
|
24
|
-
|
|
24
|
+
# Double-checked: the unlocked read only skips the lock once the
|
|
25
|
+
# answer is set; it is written under @completion_column_mutex.
|
|
26
|
+
detected = @completion_column # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
25
27
|
return detected unless detected.nil?
|
|
26
28
|
|
|
27
29
|
@completion_column_mutex.synchronize do
|
|
@@ -30,6 +30,15 @@ module Pgbus
|
|
|
30
30
|
# unsynchronized check-and-set could WARN more than once per process.
|
|
31
31
|
@record_failure_mutex = Mutex.new
|
|
32
32
|
|
|
33
|
+
# Guards the @all_names memo and its generation (issue #527). The memo is a frozen Set,
|
|
34
|
+
# replaced whole (record! publishes a copy with the new name), so a
|
|
35
|
+
# caller iterating the set it got can never see it change underneath:
|
|
36
|
+
# record! runs on broadcast threads while the dispatcher iterates it.
|
|
37
|
+
@names_lock = Mutex.new
|
|
38
|
+
# Bumped by every record! and reset_cache! (under @names_lock): a cold
|
|
39
|
+
# load publishes its snapshot only if no such change happened while it ran.
|
|
40
|
+
@names_generation = 0
|
|
41
|
+
|
|
33
42
|
class << self
|
|
34
43
|
# Inserts the physical queue name. Idempotent and cheap to call on
|
|
35
44
|
# every broadcast; the caller (`ensure_stream_queue`) also memoizes
|
|
@@ -61,7 +70,10 @@ module Pgbus
|
|
|
61
70
|
# already-registered stream until the next reset_cache!. Leaving it
|
|
62
71
|
# nil lets the next all_names call do a real load, which already
|
|
63
72
|
# includes this row since the insert above has committed.
|
|
64
|
-
@
|
|
73
|
+
@names_lock.synchronize do
|
|
74
|
+
@names_generation += 1
|
|
75
|
+
@all_names = (@all_names | [queue_name]).freeze if @all_names
|
|
76
|
+
end
|
|
65
77
|
true
|
|
66
78
|
rescue ActiveRecord::ActiveRecordError => e
|
|
67
79
|
Pgbus.logger.debug { "[Pgbus] Failed to record stream queue #{queue_name}: #{e.message}" }
|
|
@@ -76,8 +88,21 @@ module Pgbus
|
|
|
76
88
|
# not one per queue. Callers that need freshness (a long-lived process
|
|
77
89
|
# picking up newly-created streams) call `reset_cache!` at the top of
|
|
78
90
|
# their loop.
|
|
91
|
+
# A cold load runs outside the lock (record! takes a connection before
|
|
92
|
+
# the lock, so a load holding the lock while it waits for a connection
|
|
93
|
+
# could deadlock an exhausted pool) and is published only when no
|
|
94
|
+
# record! or reset_cache! ran meanwhile: its snapshot could predate
|
|
95
|
+
# them, and publishing it would lose their change. Otherwise the caller
|
|
96
|
+
# gets the snapshot and the memo stays empty, so the next call reloads.
|
|
79
97
|
def all_names
|
|
80
|
-
@all_names
|
|
98
|
+
cached, generation = @names_lock.synchronize { [@all_names, @names_generation] }
|
|
99
|
+
return cached if cached
|
|
100
|
+
|
|
101
|
+
loaded = load_names.freeze
|
|
102
|
+
@names_lock.synchronize do
|
|
103
|
+
@all_names ||= loaded if @names_generation == generation
|
|
104
|
+
@all_names || loaded
|
|
105
|
+
end
|
|
81
106
|
end
|
|
82
107
|
|
|
83
108
|
# Registry ∪ fingerprint-matched names. Use this for classification
|
|
@@ -128,11 +153,17 @@ module Pgbus
|
|
|
128
153
|
# wildcard resolution so freshly-created streams are picked up without
|
|
129
154
|
# a process restart.
|
|
130
155
|
def reset_cache!
|
|
131
|
-
@
|
|
156
|
+
@names_lock.synchronize do
|
|
157
|
+
@names_generation += 1
|
|
158
|
+
@all_names = nil
|
|
159
|
+
end
|
|
132
160
|
end
|
|
133
161
|
|
|
134
162
|
# Memoized like StreamStat: a successful probe sticks; a transient
|
|
135
163
|
# error returns false without caching, so the next call retries.
|
|
164
|
+
# An idempotent memo: concurrent first callers probe the same schema
|
|
165
|
+
# and store the same Boolean, so the unlocked read/write is safe.
|
|
166
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
136
167
|
def table_exists?
|
|
137
168
|
return @table_exists if defined?(@table_exists) && @table_exists
|
|
138
169
|
|
|
@@ -49,6 +49,9 @@ module Pgbus
|
|
|
49
49
|
# false on the first hiccup would permanently disable stream
|
|
50
50
|
# stats for the process lifetime, which is a worse failure mode
|
|
51
51
|
# than a few retries.
|
|
52
|
+
# An idempotent schema memo: concurrent first callers probe the same
|
|
53
|
+
# schema and store the same Boolean, so the unlocked read/write is safe.
|
|
54
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
52
55
|
def self.table_exists?
|
|
53
56
|
return @table_exists if defined?(@table_exists)
|
|
54
57
|
|
|
@@ -126,6 +126,7 @@
|
|
|
126
126
|
<%= pgbus_nav_link t("pgbus.layout.nav.queues"), pgbus.queues_path %>
|
|
127
127
|
<%= pgbus_nav_link t("pgbus.layout.nav.jobs"), pgbus.jobs_path %>
|
|
128
128
|
<%= pgbus_nav_link t("pgbus.layout.nav.recurring"), pgbus.recurring_tasks_path %>
|
|
129
|
+
<%= pgbus_nav_link t("pgbus.layout.nav.deploy_tasks"), pgbus.deploy_tasks_path %>
|
|
129
130
|
<%= pgbus_nav_link t("pgbus.layout.nav.processes"), pgbus.processes_path %>
|
|
130
131
|
<%= pgbus_nav_link t("pgbus.layout.nav.events"), pgbus.events_path %>
|
|
131
132
|
<%= pgbus_nav_link t("pgbus.layout.nav.batches"), pgbus.batches_path %>
|
|
@@ -186,6 +187,7 @@
|
|
|
186
187
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.queues"), pgbus.queues_path %>
|
|
187
188
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.jobs"), pgbus.jobs_path %>
|
|
188
189
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.recurring"), pgbus.recurring_tasks_path %>
|
|
190
|
+
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.deploy_tasks"), pgbus.deploy_tasks_path %>
|
|
189
191
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.processes"), pgbus.processes_path %>
|
|
190
192
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.events"), pgbus.events_path %>
|
|
191
193
|
<%= pgbus_mobile_nav_link t("pgbus.layout.nav.batches"), pgbus.batches_path %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%# The rollover the scheduler acts on (DataSource#deploy_rollover): the same
|
|
2
|
+
reason every waiting task's sentence names. %>
|
|
3
|
+
<% key = pgbus_deploy_rollover_key(@rollover) %>
|
|
4
|
+
<% lagging = Array(@rollover&.dig(:lagging)) %>
|
|
5
|
+
<section aria-labelledby="deploy-rollover-heading" data-rollover="<%= key %>"
|
|
6
|
+
class="mb-6 rounded-lg p-4 ring-1 <%= Pgbus::DeployTasksHelper::BANNER_TONE_CSS.fetch(pgbus_deploy_rollover_tone(@rollover)) %>">
|
|
7
|
+
<h2 id="deploy-rollover-heading" class="text-sm font-semibold"><%= t("pgbus.deploy_tasks.banner.heading") %></h2>
|
|
8
|
+
<p class="mt-1 text-sm"><%= pgbus_deploy_rollover_sentence(@rollover) %></p>
|
|
9
|
+
<% if key == "no_revision" %>
|
|
10
|
+
<p class="mt-1 text-sm"><%= t("pgbus.deploy_tasks.banner.no_revision_hint_html") %></p>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% if lagging.any? %>
|
|
13
|
+
<ul class="mt-2 list-disc pl-5 text-sm">
|
|
14
|
+
<% lagging.first(10).each do |process| %>
|
|
15
|
+
<li><%= pgbus_deploy_lagging_process(process) %></li>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if lagging.size > 10 %>
|
|
18
|
+
<li><%= t("pgbus.deploy_tasks.banner.lagging_more", count: lagging.size - 10) %></li>
|
|
19
|
+
<% end %>
|
|
20
|
+
</ul>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% if @rollover&.dig(:live_by_revision).present? %>
|
|
23
|
+
<p class="mt-2 text-xs"><%= t("pgbus.deploy_tasks.banner.live_heading") %> <%= pgbus_deploy_live_counts(@rollover) %></p>
|
|
24
|
+
<% end %>
|
|
25
|
+
</section>
|