pgbus 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +329 -147
- data/README.md +65 -27
- data/app/controllers/pgbus/deploy_tasks_controller.rb +38 -0
- 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/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 +98 -1
- 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_batch_callback_jobs_generator.rb +4 -1
- 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/batch_id.rb +10 -4
- data/lib/pgbus/batch/builder.rb +92 -0
- data/lib/pgbus/batch.rb +97 -22
- data/lib/pgbus/configuration/deploy_settings.rb +46 -0
- data/lib/pgbus/configuration.rb +3 -0
- 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 +17 -0
- data/lib/pgbus/generators/migration_detector.rb +12 -1
- data/lib/pgbus/instrumentation.rb +14 -0
- data/lib/pgbus/integrations/appsignal/subscriber.rb +15 -0
- 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 +12 -0
- data/lib/pgbus/outbox/poller.rb +1 -0
- data/lib/pgbus/process/consumer.rb +57 -3
- data/lib/pgbus/process/dispatcher.rb +18 -1
- data/lib/pgbus/process/heartbeat.rb +7 -2
- data/lib/pgbus/process/supervisor.rb +35 -3
- data/lib/pgbus/process/worker.rb +1 -0
- data/lib/pgbus/recurring/scheduler.rb +54 -20
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/deploy_task_list.rb +114 -0
- data/lib/pgbus/web/data_source/list_counts.rb +2 -1
- data/lib/pgbus/web/data_source.rb +1 -0
- data/lib/pgbus/web/deploy_task_state.rb +51 -0
- data/lib/pgbus.rb +11 -0
- data/sig/pgbus/configuration.rbs +10 -0
- data/sig/pgbus.rbs +9 -0
- metadata +21 -1
data/README.md
CHANGED
|
@@ -801,28 +801,27 @@ How Pgbus integrates with your application's transactions and tracks groups of r
|
|
|
801
801
|
Coordinate groups of jobs with callbacks when all complete:
|
|
802
802
|
|
|
803
803
|
```ruby
|
|
804
|
-
batch = Pgbus
|
|
805
|
-
|
|
806
|
-
on_success
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
properties: { initiated_by: current_user.id }
|
|
810
|
-
)
|
|
811
|
-
|
|
812
|
-
batch.enqueue do
|
|
813
|
-
users.each { |user| ImportUserJob.perform_later(user.id) }
|
|
804
|
+
batch = Pgbus.batch(description: "Import users", properties: { initiated_by: current_user.id }) do |b|
|
|
805
|
+
b.on_complete BatchFinishedJob
|
|
806
|
+
b.on_success BatchSucceededJob
|
|
807
|
+
b.on_failure BatchFailedJob
|
|
808
|
+
b.add(*users.map { |user| ImportUserJob.new(user.id) })
|
|
814
809
|
end
|
|
815
810
|
```
|
|
816
811
|
|
|
812
|
+
`b.add` takes job instances (each keeps its own `.set(wait:, queue:)`) and sends them in one bulk enqueue. The batch row exists before the block runs and stays pending until it returns, so a job that finishes while the block is still adding cannot finish the batch early. Set callbacks first: if the block raises after an `add`, the added jobs still run and the batch finishes later with only the callbacks set by then. A block that goes `config.batch_stall_threshold` (default 5 minutes) without adding a job is started by the stalled-batch sweep, so keep slow work outside it. Callbacks are job classes or configured instances, never blocks: they are stored on the batch row.
|
|
813
|
+
|
|
814
|
+
Inside a job, `batch { |b| ... }` opens a new batch the same way, and a job that is already a member adds siblings with `batch.add(NextJob.new(id))`. `Pgbus::Batch.new(on_complete: ...)` plus `batch.enqueue { ... }` remains the lower-level form.
|
|
815
|
+
|
|
817
816
|
#### Callbacks
|
|
818
817
|
|
|
819
818
|
| Callback | Fired when |
|
|
820
819
|
|----------|------------|
|
|
821
|
-
| `
|
|
822
|
-
| `on_success` | All jobs completed successfully (zero
|
|
823
|
-
| `
|
|
820
|
+
| `on_complete` | All jobs completed (success or failure). `on_finish` is a deprecated alias until 1.0 |
|
|
821
|
+
| `on_success` | All jobs completed successfully (zero failed) |
|
|
822
|
+
| `on_failure` | At least one job was dead-lettered. `on_discard` is a deprecated alias until 1.0 |
|
|
824
823
|
|
|
825
|
-
Callback jobs receive the batch `properties` hash as their argument:
|
|
824
|
+
Callback jobs given as a bare class receive the batch `properties` hash as their argument:
|
|
826
825
|
|
|
827
826
|
```ruby
|
|
828
827
|
class BatchFinishedJob < ApplicationJob
|
|
@@ -838,23 +837,18 @@ end
|
|
|
838
837
|
`ensures_uniqueness` protects one job. A multi-job run — a nightly import that fans out into hundreds of chunk jobs, a pipeline whose jobs enqueue the next stage — has no single job that lives for the whole run, so a lock on the entry job releases the moment that job finishes and the next scheduler tick starts a second run on top of the first. Give the batch the lock instead:
|
|
839
838
|
|
|
840
839
|
```ruby
|
|
841
|
-
batch
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
on_success: ExportPricesJob
|
|
845
|
-
)
|
|
846
|
-
|
|
847
|
-
batch.enqueue do
|
|
848
|
-
ImportChunkJob.perform_later("products", 0)
|
|
840
|
+
Pgbus.batch(uniqueness_key: "perfecta-daily", on_conflict: :discard) do |b| # :reject (raise AlreadyRunning), :discard, :log
|
|
841
|
+
b.on_success ExportPricesJob
|
|
842
|
+
b.add(ImportChunkJob.new("products", 0))
|
|
849
843
|
end
|
|
850
844
|
```
|
|
851
845
|
|
|
852
|
-
The lock is taken at `enqueue` and held until the batch finishes — through every open-batch `batch.
|
|
846
|
+
The lock is taken at the top of the block (or at `enqueue`) and held until the batch finishes — through every open-batch `batch.add` from inside a running job — then released in the same single-winner finish that fires the callbacks, so completion, the stalled-batch sweep and cleanup all release it. While it is held, another `Pgbus.batch(uniqueness_key: "perfecta-daily") { ... }` raises `Pgbus::Batch::AlreadyRunning` (`:reject`), or skips its block and returns a batch that answers `discarded?` (`:discard`, `:log`). The row lives in `pgbus_uniqueness_keys` as `batch:<key>`, so it never collides with a job's key and shows up in the dashboard like any other lock; the reaper judges it by the batch's status, never by queue contents.
|
|
853
847
|
|
|
854
848
|
#### How batches work
|
|
855
849
|
|
|
856
|
-
1. `
|
|
857
|
-
2.
|
|
850
|
+
1. The top of the `Pgbus.batch` block (or the first `batch.enqueue`) creates a tracking row in `pgbus_batches` with `status: "pending"`
|
|
851
|
+
2. Each job enqueued in the block is tagged with the `pgbus_batch_id` in its payload
|
|
858
852
|
3. After each job completes or is dead-lettered, the executor atomically updates the batch counters
|
|
859
853
|
4. When `completed_jobs + discarded_jobs == total_jobs`, the batch status flips to `"finished"` and callback jobs are enqueued
|
|
860
854
|
5. The dispatcher cleans up finished batches older than 7 days
|
|
@@ -1017,7 +1011,7 @@ c.metrics_backend = MyOpenTelemetryBackend.new
|
|
|
1017
1011
|
| `pgbus_queue_job_count` | counter | `queue`, `job_class`, `status` (`processed`/`failed`/`dead_lettered`) |
|
|
1018
1012
|
| `pgbus_visibility_extended` | counter | `queue`, `job_class` — one per re-armed visibility timeout; a class listed here outlives `visibility_timeout` |
|
|
1019
1013
|
| `pgbus_job_duration_ms` | histogram | `queue`, `job_class` |
|
|
1020
|
-
| `pgbus_event_count` | counter | `handler`, `routing_key`, `status` |
|
|
1014
|
+
| `pgbus_event_count` | counter | `handler`, `routing_key`, `status` (`processed`/`failed`/`skipped`/`dead_lettered`), plus `reason` on `skipped` only (`completed`/`cached`/`owned`) |
|
|
1021
1015
|
| `pgbus_event_duration_ms` | histogram | `handler`, `routing_key` |
|
|
1022
1016
|
| `pgbus_messages_sent` / `pgbus_messages_read` | counter | `queue` |
|
|
1023
1017
|
| `pgbus_stream_broadcast_count` | counter | `stream`, `deferred` |
|
|
@@ -1052,7 +1046,7 @@ ActiveSupport::Notifications.subscribe(/^pgbus\./) do |name, start, finish, _id,
|
|
|
1052
1046
|
end
|
|
1053
1047
|
```
|
|
1054
1048
|
|
|
1055
|
-
Metrics-relevant events emitted (the subset the metrics subscriber maps; see `lib/pgbus/instrumentation.rb` for the full `pgbus.*` catalog): `pgbus.executor.execute`, `pgbus.job_completed`, `pgbus.job_failed`, `pgbus.job_dead_lettered`, `pgbus.event_processed`, `pgbus.event_failed`, `pgbus.client.send_message`, `pgbus.client.send_batch`, `pgbus.client.read_batch`, `pgbus.client.pool`, `pgbus.stream.broadcast`, `pgbus.outbox.publish`, `pgbus.recurring.enqueue`, `pgbus.worker.recycle`, `pgbus.consumer.recycle`. Payload keys are documented in `lib/pgbus/instrumentation.rb`.
|
|
1049
|
+
Metrics-relevant events emitted (the subset the metrics subscriber maps; see `lib/pgbus/instrumentation.rb` for the full `pgbus.*` catalog): `pgbus.executor.execute`, `pgbus.job_completed`, `pgbus.job_failed`, `pgbus.job_dead_lettered`, `pgbus.event_processed`, `pgbus.event_failed`, `pgbus.event_dead_lettered`, `pgbus.event_skipped`, `pgbus.job_visibility_extended`, `pgbus.client.send_message`, `pgbus.client.send_batch`, `pgbus.client.read_batch`, `pgbus.client.pool`, `pgbus.stream.broadcast`, `pgbus.outbox.publish`, `pgbus.recurring.enqueue`, `pgbus.worker.recycle`, `pgbus.consumer.recycle`. Payload keys are documented in `lib/pgbus/instrumentation.rb`.
|
|
1056
1050
|
|
|
1057
1051
|
### Connection pool metrics
|
|
1058
1052
|
|
|
@@ -1217,6 +1211,7 @@ All tools are read-only — no tool mutates state, and there is no raw-SQL passt
|
|
|
1217
1211
|
| `pgbus_dlq` / `pgbus_dlq_detail` | Dead-letter inspection. Paginated. |
|
|
1218
1212
|
| `pgbus_locks` | Active uniqueness locks (the leaked-lock diagnostic). Paginated (`page`, `per_page` max 100; returns `total`, capped at 10,000, and `has_more`). |
|
|
1219
1213
|
| `pgbus_throughput` / `pgbus_stats` | Recent throughput time series and status counts. |
|
|
1214
|
+
| `pgbus_deploy_tasks` | Deploy tasks, their state and the rollover status (read-only). |
|
|
1220
1215
|
| `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`. |
|
|
1221
1216
|
|
|
1222
1217
|
#### Security
|
|
@@ -2216,6 +2211,42 @@ pgbus start --workers-only --capsule default
|
|
|
2216
2211
|
|
|
2217
2212
|
The capsule name is the `:name` you passed to `c.capsule` in your initializer (or the first queue token when using the string DSL).
|
|
2218
2213
|
|
|
2214
|
+
### Deploy tasks
|
|
2215
|
+
|
|
2216
|
+
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.
|
|
2217
|
+
|
|
2218
|
+
```yaml
|
|
2219
|
+
# config/deploy_tasks.yml
|
|
2220
|
+
production:
|
|
2221
|
+
backfill_order_totals:
|
|
2222
|
+
class: BackfillOrderTotalsJob # or command: "Orders.backfill_totals!"
|
|
2223
|
+
version: 2 # bump to run again after the next rollover (default 1)
|
|
2224
|
+
queue: maintenance
|
|
2225
|
+
args: [{ batch_size: 500 }]
|
|
2226
|
+
description: Recompute totals after the currency migration
|
|
2227
|
+
warm_fragment_caches:
|
|
2228
|
+
command: "FragmentCacheWarmer.run"
|
|
2229
|
+
every_deploy: true # runs after every rollover, dedup'd on the app revision
|
|
2230
|
+
```
|
|
2231
|
+
|
|
2232
|
+
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`.
|
|
2233
|
+
|
|
2234
|
+
- **`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.
|
|
2235
|
+
- **`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).
|
|
2236
|
+
- **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.
|
|
2237
|
+
- **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).
|
|
2238
|
+
- **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.
|
|
2239
|
+
- **"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.
|
|
2240
|
+
- **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.
|
|
2241
|
+
- **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`).
|
|
2242
|
+
|
|
2243
|
+
```bash
|
|
2244
|
+
rails generate pgbus:add_deploy_tasks # migration + config/deploy_tasks.yml
|
|
2245
|
+
rails generate pgbus:add_deploy_tasks --database=pgbus # for a separate database
|
|
2246
|
+
```
|
|
2247
|
+
|
|
2248
|
+
`rails generate pgbus:update` detects the missing tables; fresh installs (`pgbus:install`) already have them.
|
|
2249
|
+
|
|
2219
2250
|
### Dashboard
|
|
2220
2251
|
|
|
2221
2252
|
The dashboard is a mountable Rails engine at `/pgbus` with:
|
|
@@ -2290,6 +2321,8 @@ Pgbus uses these tables (created via PGMQ and migrations):
|
|
|
2290
2321
|
| `pgbus_outbox_entries` | Transactional outbox entries pending publication |
|
|
2291
2322
|
| `pgbus_recurring_tasks` | Recurring job definitions |
|
|
2292
2323
|
| `pgbus_recurring_executions` | Recurring job execution history |
|
|
2324
|
+
| `pgbus_deploy_tasks` | Declared deploy tasks, their arm (`armed_at`/`armed_for`) and `enabled` flag |
|
|
2325
|
+
| `pgbus_deploy_task_runs` | Deploy task run history (task_key, version, revision, triggered_by, queue_name, msg_id, run_at) |
|
|
2293
2326
|
| `pgbus_presence_members` | Stream presence tracking (who is subscribed) |
|
|
2294
2327
|
| `pgbus_stream_stats` | Stream broadcast/connect/disconnect metrics (opt-in) |
|
|
2295
2328
|
|
|
@@ -2388,6 +2421,11 @@ Curated headline options for the README. The full operator reference (with types
|
|
|
2388
2421
|
| `outbox_batch_size` | `100` | Max entries per outbox poll cycle |
|
|
2389
2422
|
| `outbox_retention` | `1.day` | How long to keep published outbox entries. Accepts seconds, Duration, or `nil` to disable cleanup |
|
|
2390
2423
|
| `idempotency_ttl` | `7.days` | How long to keep processed event records. Accepts seconds, Duration, or `nil` to disable cleanup |
|
|
2424
|
+
| `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 |
|
|
2425
|
+
| `deploy_tasks` | `nil` | Hash of deploy tasks, same shape as `config/deploy_tasks.yml` |
|
|
2426
|
+
| `deploy_tasks_files` | `nil` | Array of deploy-task YAML files (replaces the default `config/deploy_tasks.yml`) |
|
|
2427
|
+
| `deploy_tasks_enabled` | `true` | Fork the scheduler for deploy tasks; set `false` to turn them off |
|
|
2428
|
+
| `deploy_task_run_retention` | `90.days` | How long to keep deploy task run history. Accepts seconds, Duration, or `nil` to keep forever |
|
|
2391
2429
|
| `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). |
|
|
2392
2430
|
| `base_controller_class` | `"::ActionController::Base"` | Base class for dashboard controllers (string, constantized at load time) |
|
|
2393
2431
|
| `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
|
|
@@ -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
|
|
@@ -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>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<turbo-frame id="deploy-tasks" data-auto-refresh data-turbo-action="advance"
|
|
2
|
+
data-src="<%= pgbus.deploy_tasks_path(request.query_parameters.merge(frame: "deploy_tasks")) %>">
|
|
3
|
+
<%= render "pgbus/deploy_tasks/rollover_banner" %>
|
|
4
|
+
<div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
|
|
5
|
+
<table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
6
|
+
<thead class="bg-gray-50 dark:bg-gray-900">
|
|
7
|
+
<tr>
|
|
8
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.deploy_tasks.tasks_table.headers.task") %></th>
|
|
9
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.deploy_tasks.tasks_table.headers.version") %></th>
|
|
10
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.deploy_tasks.tasks_table.headers.queue") %></th>
|
|
11
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.deploy_tasks.tasks_table.headers.last_run") %></th>
|
|
12
|
+
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.deploy_tasks.tasks_table.headers.state") %></th>
|
|
13
|
+
<th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.deploy_tasks.tasks_table.headers.actions") %></th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
|
|
17
|
+
<% @deploy_tasks.each do |task| %>
|
|
18
|
+
<% result = Pgbus::Web::DeployTaskState.present(task, @rollover) %>
|
|
19
|
+
<% last_run = pgbus_deploy_last_run(task) %>
|
|
20
|
+
<tr data-state="<%= result.state %>" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
21
|
+
<td data-label="<%= t("pgbus.deploy_tasks.tasks_table.headers.task") %>" class="px-4 py-3">
|
|
22
|
+
<div>
|
|
23
|
+
<%= pgbus_link_to task[:key], pgbus.deploy_task_path(task[:id]), data: { turbo_frame: "_top" } %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="text-xs text-gray-500 dark:text-gray-400"><%= task[:class_name] || task[:command]&.truncate(40) %></div>
|
|
26
|
+
<% if task[:description] %>
|
|
27
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5"><%= task[:description] %></div>
|
|
28
|
+
<% end %>
|
|
29
|
+
</td>
|
|
30
|
+
<td data-label="<%= t("pgbus.deploy_tasks.tasks_table.headers.version") %>" class="px-4 py-3 text-sm text-gray-900 dark:text-white">
|
|
31
|
+
<%= pgbus_deploy_version(task) %>
|
|
32
|
+
</td>
|
|
33
|
+
<td data-label="<%= t("pgbus.deploy_tasks.tasks_table.headers.queue") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
|
|
34
|
+
<%= task[:queue_name] || t("pgbus.deploy_tasks.default_queue") %>
|
|
35
|
+
</td>
|
|
36
|
+
<td data-label="<%= t("pgbus.deploy_tasks.tasks_table.headers.last_run") %>" class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
|
|
37
|
+
<% if last_run %>
|
|
38
|
+
<%= pgbus_time(last_run[:run_at]) %>
|
|
39
|
+
<span class="block text-xs text-gray-500 dark:text-gray-400"><%= t("pgbus.deploy_tasks.triggered_by.#{last_run[:triggered_by]}") %></span>
|
|
40
|
+
<% else %>
|
|
41
|
+
<%= t("pgbus.deploy_tasks.tasks_table.never") %>
|
|
42
|
+
<% end %>
|
|
43
|
+
</td>
|
|
44
|
+
<td data-label="<%= t("pgbus.deploy_tasks.tasks_table.headers.state") %>" class="px-4 py-3">
|
|
45
|
+
<%= pgbus_deploy_state_badge(result) %>
|
|
46
|
+
<p class="mt-1 text-xs text-gray-600 dark:text-gray-300"><%= pgbus_deploy_task_reason(result) %></p>
|
|
47
|
+
</td>
|
|
48
|
+
<td data-label="<%= t("pgbus.deploy_tasks.tasks_table.headers.actions") %>" class="px-4 py-3 text-right space-x-1">
|
|
49
|
+
<%= pgbus_button_to task[:enabled] ? t("pgbus.deploy_tasks.disable") : t("pgbus.deploy_tasks.enable"),
|
|
50
|
+
pgbus.toggle_deploy_task_path(task[:id]),
|
|
51
|
+
variant: task[:enabled] ? :warning : :success, size: :sm, form: { data: { turbo_frame: "_top" } } %>
|
|
52
|
+
<% if task[:enabled] %>
|
|
53
|
+
<%= pgbus_button_to t("pgbus.deploy_tasks.run_now"), pgbus.enqueue_deploy_task_path(task[:id]),
|
|
54
|
+
variant: :primary, size: :sm, form: { data: { turbo_frame: "_top" } } %>
|
|
55
|
+
<% end %>
|
|
56
|
+
</td>
|
|
57
|
+
</tr>
|
|
58
|
+
<% end %>
|
|
59
|
+
<% if @deploy_tasks.empty? %>
|
|
60
|
+
<%= render "pgbus/shared/empty_row", colspan: 6,
|
|
61
|
+
message: safe_join([content_tag(:p, t("pgbus.deploy_tasks.tasks_table.empty_title"), class: "text-lg font-medium"),
|
|
62
|
+
content_tag(:p, t("pgbus.deploy_tasks.tasks_table.empty_hint_html"), class: "mt-1")]) %>
|
|
63
|
+
<% end %>
|
|
64
|
+
</tbody>
|
|
65
|
+
</table>
|
|
66
|
+
<%= render "pgbus/shared/pager", page: @page, per_page: @per_page, shown: @deploy_tasks.size,
|
|
67
|
+
total_count: @deploy_tasks_count.total,
|
|
68
|
+
has_more: @deploy_tasks_count.capped? && @deploy_tasks.size == @per_page,
|
|
69
|
+
page_path: ->(n) { pgbus.deploy_tasks_path(page: n) } %>
|
|
70
|
+
</div>
|
|
71
|
+
</turbo-frame>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<div class="flex items-center justify-between mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.deploy_tasks.index.title") %></h1>
|
|
3
|
+
<span class="text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.deploy_tasks.index.task_count", count: @deploy_tasks_count.total) %></span>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= render "pgbus/deploy_tasks/tasks_table" %>
|