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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +345 -142
  3. data/README.md +49 -0
  4. data/app/controllers/pgbus/deploy_tasks_controller.rb +38 -0
  5. data/app/controllers/pgbus/frontends_controller.rb +10 -8
  6. data/app/helpers/pgbus/application_helper.rb +4 -3
  7. data/app/helpers/pgbus/deploy_tasks_helper.rb +77 -0
  8. data/app/helpers/pgbus/processes_helper.rb +1 -1
  9. data/app/models/pgbus/deploy_task.rb +104 -0
  10. data/app/models/pgbus/deploy_task_run.rb +45 -0
  11. data/app/models/pgbus/job_stat.rb +4 -0
  12. data/app/models/pgbus/processed_event.rb +3 -1
  13. data/app/models/pgbus/stream_queue.rb +34 -3
  14. data/app/models/pgbus/stream_stat.rb +3 -0
  15. data/app/views/layouts/pgbus/application.html.erb +2 -0
  16. data/app/views/pgbus/deploy_tasks/_rollover_banner.html.erb +25 -0
  17. data/app/views/pgbus/deploy_tasks/_tasks_table.html.erb +71 -0
  18. data/app/views/pgbus/deploy_tasks/index.html.erb +6 -0
  19. data/app/views/pgbus/deploy_tasks/show.html.erb +134 -0
  20. data/config/i18n-tasks.yml +2 -0
  21. data/config/locales/da.yml +97 -0
  22. data/config/locales/de.yml +97 -0
  23. data/config/locales/en.yml +97 -0
  24. data/config/locales/es.yml +97 -0
  25. data/config/locales/fi.yml +97 -0
  26. data/config/locales/fr.yml +97 -0
  27. data/config/locales/it.yml +97 -0
  28. data/config/locales/ja.yml +97 -0
  29. data/config/locales/nb.yml +97 -0
  30. data/config/locales/nl.yml +97 -0
  31. data/config/locales/pt.yml +97 -0
  32. data/config/locales/sv.yml +97 -0
  33. data/config/routes.rb +7 -0
  34. data/lib/generators/pgbus/add_deploy_tasks_generator.rb +51 -0
  35. data/lib/generators/pgbus/templates/add_deploy_tasks_tables.rb.erb +43 -0
  36. data/lib/generators/pgbus/templates/deploy_tasks.yml.erb +42 -0
  37. data/lib/generators/pgbus/templates/migration.rb.erb +43 -0
  38. data/lib/pgbus/active_job/current_attributes.rb +3 -1
  39. data/lib/pgbus/batch.rb +17 -9
  40. data/lib/pgbus/cli.rb +3 -0
  41. data/lib/pgbus/client/read_after.rb +10 -1
  42. data/lib/pgbus/client.rb +54 -9
  43. data/lib/pgbus/concurrency.rb +3 -0
  44. data/lib/pgbus/configuration/deploy_settings.rb +46 -0
  45. data/lib/pgbus/configuration.rb +87 -9
  46. data/lib/pgbus/current_attributes.rb +3 -1
  47. data/lib/pgbus/dedup_cache.rb +15 -7
  48. data/lib/pgbus/deploy/revision.rb +59 -0
  49. data/lib/pgbus/deploy/rollover.rb +65 -0
  50. data/lib/pgbus/deploy/runner.rb +259 -0
  51. data/lib/pgbus/deploy/task.rb +105 -0
  52. data/lib/pgbus/engine.rb +28 -0
  53. data/lib/pgbus/event_bus/handler.rb +16 -4
  54. data/lib/pgbus/execution_pools/async_pool.rb +23 -2
  55. data/lib/pgbus/execution_pools/thread_pool.rb +22 -0
  56. data/lib/pgbus/execution_pools.rb +26 -0
  57. data/lib/pgbus/generators/migration_detector.rb +11 -0
  58. data/lib/pgbus/instrumentation.rb +3 -0
  59. data/lib/pgbus/integrations/appsignal/probe.rb +17 -9
  60. data/lib/pgbus/integrations/appsignal/subscriber.rb +30 -18
  61. data/lib/pgbus/integrations/appsignal.rb +26 -16
  62. data/lib/pgbus/mcp/rack_app.rb +1 -1
  63. data/lib/pgbus/mcp/server.rb +2 -1
  64. data/lib/pgbus/mcp/tools/deploy_tasks_tool.rb +86 -0
  65. data/lib/pgbus/mcp.rb +1 -0
  66. data/lib/pgbus/metrics/subscriber.rb +35 -21
  67. data/lib/pgbus/outbox/poller.rb +1 -0
  68. data/lib/pgbus/process/consumer.rb +2 -1
  69. data/lib/pgbus/process/consumer_priority.rb +1 -1
  70. data/lib/pgbus/process/dispatcher.rb +18 -1
  71. data/lib/pgbus/process/heartbeat.rb +7 -2
  72. data/lib/pgbus/process/lifecycle.rb +5 -5
  73. data/lib/pgbus/process/notify_listener.rb +32 -18
  74. data/lib/pgbus/process/supervisor.rb +35 -3
  75. data/lib/pgbus/process/wake_pipe.rb +4 -1
  76. data/lib/pgbus/process/worker.rb +5 -2
  77. data/lib/pgbus/recurring/scheduler.rb +54 -20
  78. data/lib/pgbus/retry_backoff.rb +3 -0
  79. data/lib/pgbus/shareable.rb +70 -0
  80. data/lib/pgbus/streams/broadcastable_override.rb +3 -0
  81. data/lib/pgbus/streams/presence.rb +6 -7
  82. data/lib/pgbus/streams.rb +38 -27
  83. data/lib/pgbus/testing.rb +19 -3
  84. data/lib/pgbus/uniqueness.rb +3 -0
  85. data/lib/pgbus/version.rb +1 -1
  86. data/lib/pgbus/visibility_heartbeat.rb +51 -34
  87. data/lib/pgbus/web/authentication.rb +19 -2
  88. data/lib/pgbus/web/data_source/deploy_task_list.rb +114 -0
  89. data/lib/pgbus/web/data_source/list_counts.rb +10 -7
  90. data/lib/pgbus/web/data_source.rb +1 -0
  91. data/lib/pgbus/web/deploy_task_state.rb +51 -0
  92. data/lib/pgbus/web/health_server.rb +4 -1
  93. data/lib/pgbus/web/process_activity.rb +1 -1
  94. data/lib/pgbus/web/streamer/heartbeat.rb +4 -1
  95. data/lib/pgbus/web/streamer/hub_client.rb +4 -1
  96. data/lib/pgbus/web/streamer/listener.rb +10 -1
  97. data/lib/pgbus/web/streamer/master_hub.rb +11 -5
  98. data/lib/pgbus/web/streamer/master_hub_boot.rb +3 -1
  99. data/lib/pgbus/web/streamer/outbound_pump.rb +4 -1
  100. data/lib/pgbus/web/streamer/peer_close_watcher.rb +4 -1
  101. data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +13 -1
  102. data/lib/pgbus.rb +92 -30
  103. data/sig/pgbus/active_job/adapter.rbs +57 -0
  104. data/sig/pgbus/client.rbs +78 -0
  105. data/sig/pgbus/configuration.rbs +326 -0
  106. data/sig/pgbus/streams/stream.rbs +25 -0
  107. data/sig/pgbus/streams.rbs +22 -0
  108. data/sig/pgbus.rbs +73 -0
  109. metadata +27 -1
@@ -236,6 +236,102 @@ nl:
236
236
  payload: Inhoud
237
237
  retry: Opnieuw proberen
238
238
  title: 'DLQ Bericht #%{id}'
239
+ deploy_tasks:
240
+ banner:
241
+ heading: Uitrol
242
+ lagging_html:
243
+ one: 'Wacht op uitrol: 1 proces draait nog niet op <code>%{revision}</code>.'
244
+ other: 'Wacht op uitrol: %{count} processen draaien nog niet op <code>%{revision}</code>.'
245
+ lagging_more: "…en nog %{count}"
246
+ lagging_process: "%{kind}@%{hostname} pid %{pid} op %{revision}"
247
+ live_count: "%{count} op %{revision}"
248
+ live_heading: 'Actieve processen:'
249
+ no_revision_hint_html: Stel <code>config.app_revision</code> in, of een van <code>PGBUS_REVISION</code>, <code>DASH_VERSION</code> of <code>KAMAL_VERSION</code>, of lever een <code>REVISION</code>-bestand mee. Nu uitvoeren werkt nog steeds.
250
+ no_revision_html: 'App-revisie niet geconfigureerd: deploy-taken wachten tot die er is.'
251
+ no_revision_label: geen revisie
252
+ no_scheduler_html: 'Er draait geen scheduler: deploy-taken worden alleen door de scheduler gestart. Dit oordeel gebruikt de eigen revisie van het dashboard.'
253
+ no_workers_html: 'Wacht op uitrol: nog geen worker op <code>%{revision}</code>.'
254
+ ready_html: 'Uitgerold: elk actief proces draait <code>%{revision}</code>. Klaargezette taken draaien bij de volgende tick van de scheduler.'
255
+ unavailable_html: De uitrolstatus kon niet worden gelezen.
256
+ default_queue: default
257
+ disable: Uitschakelen
258
+ enable: Inschakelen
259
+ enqueue:
260
+ enqueued: Deploy-taak in de wachtrij gezet
261
+ failed: 'De deploy-taak is niet in de wachtrij gezet: zie het log voor de reden'
262
+ every_deploy: Bij elke deploy
263
+ index:
264
+ task_count:
265
+ one: "%{count} taak gedeclareerd"
266
+ other: "%{count} taken gedeclareerd"
267
+ title: Deploy-taken
268
+ not_found: Deploy-taak niet gevonden
269
+ reasons:
270
+ disabled_armed_html: 'Uitgeschakeld met een uitrolrun in afwachting: schakel hem in om hem te laten draaien.'
271
+ disabled_html: 'Uitgeschakeld: de scheduler slaat hem over.'
272
+ due_html: 'Klaar: draait bij de volgende tick van de scheduler.'
273
+ manual_html: "· draaide %{ago} (handmatig)"
274
+ never_html: Nooit gedraaid na een uitrol.
275
+ ran_html: Draaide %{ago} op <code>%{revision}</code> (uitrol).
276
+ waiting_lagging_html:
277
+ one: 'Wacht op uitrol: 1 proces draait nog niet op <code>%{revision}</code>.'
278
+ other: 'Wacht op uitrol: %{count} processen draaien nog niet op <code>%{revision}</code>.'
279
+ waiting_no_revision_html: 'Wacht: de app-revisie is niet geconfigureerd.'
280
+ waiting_no_scheduler_html: 'Wacht: er draait geen scheduler.'
281
+ waiting_no_workers_html: Wacht op een worker op <code>%{revision}</code>.
282
+ waiting_unknown_html: 'Wacht: de uitrolstatus kon niet worden gelezen.'
283
+ run_now: Nu uitvoeren
284
+ show:
285
+ armed_html: "%{armed_for}, klaargezet %{ago}"
286
+ back: Terug
287
+ configuration: Configuratie
288
+ labels:
289
+ arguments: Argumenten
290
+ armed: Openstaande run
291
+ command: Commando
292
+ description: Beschrijving
293
+ job_class: Jobklasse
294
+ priority: Prioriteit
295
+ queue: Wachtrij
296
+ source: Bron
297
+ last_run: Laatste run
298
+ no_runs: Nog geen runs vastgelegd
299
+ recent_runs: Recente runs
300
+ run_headers:
301
+ enqueued_at: In wachtrij gezet
302
+ queue: Wachtrij
303
+ revision: Revisie
304
+ triggered_by: Gestart door
305
+ version: Versie
306
+ source_config: Configuratiebestand
307
+ source_dynamic: Dynamisch
308
+ state: Status
309
+ version: Versie
310
+ states:
311
+ disabled: Uitgeschakeld
312
+ due: Klaar
313
+ never: Nooit gedraaid
314
+ ran: Gedraaid
315
+ waiting: Wacht
316
+ tasks_table:
317
+ empty_hint_html: Declareer eenmalige taken in <code>config/deploy_tasks.yml</code>; <code>rails generate pgbus:add_deploy_tasks</code> schrijft een startbestand.
318
+ empty_title: Geen deploy-taken gedeclareerd
319
+ headers:
320
+ actions: Acties
321
+ last_run: Laatste run
322
+ queue: Wachtrij
323
+ state: Status
324
+ task: Taak
325
+ version: Versie
326
+ never: Nooit
327
+ toggle:
328
+ disabled: Deploy-taak uitgeschakeld
329
+ enabled: Deploy-taak ingeschakeld
330
+ failed: Omschakelen van de deploy-taak mislukt
331
+ triggered_by:
332
+ manual: Handmatig
333
+ rollover: Uitrol
334
+ version: v%{version}
239
335
  dialogs:
240
336
  cancel: Annuleren
241
337
  confirm: Bevestigen
@@ -617,6 +713,7 @@ nl:
617
713
  nav:
618
714
  batches: Groepen
619
715
  dashboard: Dashboard
716
+ deploy_tasks: Deploy
620
717
  dlq: DLQ
621
718
  events: Evenementen
622
719
  insights: Inzichten
@@ -236,6 +236,102 @@ pt:
236
236
  payload: Carga útil
237
237
  retry: Tentar novamente
238
238
  title: 'Mensagem DLQ #%{id}'
239
+ deploy_tasks:
240
+ banner:
241
+ heading: Transição
242
+ lagging_html:
243
+ one: 'Aguardando a transição: 1 processo ainda não executa <code>%{revision}</code>.'
244
+ other: 'Aguardando a transição: %{count} processos ainda não executam <code>%{revision}</code>.'
245
+ lagging_more: "…e mais %{count}"
246
+ lagging_process: "%{kind}@%{hostname} pid %{pid} em %{revision}"
247
+ live_count: "%{count} em %{revision}"
248
+ live_heading: 'Processos ativos:'
249
+ no_revision_hint_html: Defina <code>config.app_revision</code>, ou uma de <code>PGBUS_REVISION</code>, <code>DASH_VERSION</code> ou <code>KAMAL_VERSION</code>, ou inclua um arquivo <code>REVISION</code>. Executar agora continua funcionando.
250
+ no_revision_html: 'Revisão da aplicação não configurada: as tarefas de deploy aguardam até que esteja.'
251
+ no_revision_label: sem revisão
252
+ no_scheduler_html: 'Nenhum agendador em execução: as tarefas de deploy só são disparadas pelo agendador. Este veredito usa a revisão do próprio painel.'
253
+ no_workers_html: 'Aguardando a transição: ainda nenhum worker em <code>%{revision}</code>.'
254
+ ready_html: 'Transição concluída: cada processo ativo executa <code>%{revision}</code>. Tarefas armadas executam no próximo tick do agendador.'
255
+ unavailable_html: Não foi possível ler o estado da transição.
256
+ default_queue: default
257
+ disable: Desativar
258
+ enable: Ativar
259
+ enqueue:
260
+ enqueued: Tarefa de deploy enfileirada
261
+ failed: 'A tarefa de deploy não foi enfileirada: veja o log para o motivo'
262
+ every_deploy: Em todo deploy
263
+ index:
264
+ task_count:
265
+ one: "%{count} tarefa declarada"
266
+ other: "%{count} tarefas declaradas"
267
+ title: Tarefas de deploy
268
+ not_found: Tarefa de deploy não encontrada
269
+ reasons:
270
+ disabled_armed_html: 'Desativada com uma execução de transição pendente: ative-a para deixá-la executar.'
271
+ disabled_html: 'Desativada: o agendador a ignora.'
272
+ due_html: 'Pronta: executa no próximo tick do agendador.'
273
+ manual_html: "· executada %{ago} (manual)"
274
+ never_html: Nunca executada após uma transição.
275
+ ran_html: Executada %{ago} em <code>%{revision}</code> (transição).
276
+ waiting_lagging_html:
277
+ one: 'Aguardando a transição: 1 processo ainda não executa <code>%{revision}</code>.'
278
+ other: 'Aguardando a transição: %{count} processos ainda não executam <code>%{revision}</code>.'
279
+ waiting_no_revision_html: 'Aguardando: a revisão da aplicação não está configurada.'
280
+ waiting_no_scheduler_html: 'Aguardando: nenhum agendador em execução.'
281
+ waiting_no_workers_html: Aguardando um worker em <code>%{revision}</code>.
282
+ waiting_unknown_html: 'Aguardando: não foi possível ler o estado da transição.'
283
+ run_now: Executar agora
284
+ show:
285
+ armed_html: "%{armed_for}, armada %{ago}"
286
+ back: Voltar
287
+ configuration: Configuração
288
+ labels:
289
+ arguments: Argumentos
290
+ armed: Execução pendente
291
+ command: Comando
292
+ description: Descrição
293
+ job_class: Classe do job
294
+ priority: Prioridade
295
+ queue: Fila
296
+ source: Origem
297
+ last_run: Última execução
298
+ no_runs: Nenhuma execução registrada ainda
299
+ recent_runs: Execuções recentes
300
+ run_headers:
301
+ enqueued_at: Enfileirada em
302
+ queue: Fila
303
+ revision: Revisão
304
+ triggered_by: Disparada por
305
+ version: Versão
306
+ source_config: Arquivo de configuração
307
+ source_dynamic: Dinâmica
308
+ state: Estado
309
+ version: Versão
310
+ states:
311
+ disabled: Desativada
312
+ due: Pronta
313
+ never: Nunca executada
314
+ ran: Executada
315
+ waiting: Aguardando
316
+ tasks_table:
317
+ empty_hint_html: Declare tarefas pontuais em <code>config/deploy_tasks.yml</code>; <code>rails generate pgbus:add_deploy_tasks</code> cria um arquivo inicial.
318
+ empty_title: Nenhuma tarefa de deploy declarada
319
+ headers:
320
+ actions: Ações
321
+ last_run: Última execução
322
+ queue: Fila
323
+ state: Estado
324
+ task: Tarefa
325
+ version: Versão
326
+ never: Nunca
327
+ toggle:
328
+ disabled: Tarefa de deploy desativada
329
+ enabled: Tarefa de deploy ativada
330
+ failed: Falha ao alternar a tarefa de deploy
331
+ triggered_by:
332
+ manual: Manual
333
+ rollover: Transição
334
+ version: v%{version}
239
335
  dialogs:
240
336
  cancel: Cancelar
241
337
  confirm: Confirmar
@@ -617,6 +713,7 @@ pt:
617
713
  nav:
618
714
  batches: Lotes
619
715
  dashboard: Painel
716
+ deploy_tasks: Deploy
620
717
  dlq: DLQ
621
718
  events: Eventos
622
719
  insights: Insights
@@ -236,6 +236,102 @@ sv:
236
236
  payload: Innehåll
237
237
  retry: Försök igen
238
238
  title: 'DLQ-meddelande #%{id}'
239
+ deploy_tasks:
240
+ banner:
241
+ heading: Utrullning
242
+ lagging_html:
243
+ one: 'Väntar på utrullning: 1 process kör ännu inte <code>%{revision}</code>.'
244
+ other: 'Väntar på utrullning: %{count} processer kör ännu inte <code>%{revision}</code>.'
245
+ lagging_more: "…och %{count} till"
246
+ lagging_process: "%{kind}@%{hostname} pid %{pid} på %{revision}"
247
+ live_count: "%{count} på %{revision}"
248
+ live_heading: 'Aktiva processer:'
249
+ no_revision_hint_html: Sätt <code>config.app_revision</code>, eller någon av <code>PGBUS_REVISION</code>, <code>DASH_VERSION</code> eller <code>KAMAL_VERSION</code>, eller leverera en <code>REVISION</code>-fil. Kör nu fungerar fortfarande.
250
+ no_revision_html: 'Appens revision är inte konfigurerad: deploy-uppgifter väntar tills den är det.'
251
+ no_revision_label: ingen revision
252
+ no_scheduler_html: 'Ingen schemaläggare körs: deploy-uppgifter startas bara av schemaläggaren. Bedömningen använder instrumentpanelens egen revision.'
253
+ no_workers_html: 'Väntar på utrullning: ingen worker på <code>%{revision}</code> ännu.'
254
+ ready_html: 'Utrullad: varje aktiv process kör <code>%{revision}</code>. Väntande uppgifter körs vid schemaläggarens nästa tick.'
255
+ unavailable_html: Utrullningsstatusen kunde inte läsas.
256
+ default_queue: default
257
+ disable: Inaktivera
258
+ enable: Aktivera
259
+ enqueue:
260
+ enqueued: Deploy-uppgiften köad
261
+ failed: 'Deploy-uppgiften köades inte: se loggen för orsaken'
262
+ every_deploy: Vid varje deploy
263
+ index:
264
+ task_count:
265
+ one: "%{count} uppgift deklarerad"
266
+ other: "%{count} uppgifter deklarerade"
267
+ title: Deploy-uppgifter
268
+ not_found: Deploy-uppgiften hittades inte
269
+ reasons:
270
+ disabled_armed_html: 'Inaktiverad med en väntande utrullningskörning: aktivera den för att låta den köras.'
271
+ disabled_html: 'Inaktiverad: schemaläggaren hoppar över den.'
272
+ due_html: 'Redo: körs vid schemaläggarens nästa tick.'
273
+ manual_html: "· kördes %{ago} (manuellt)"
274
+ never_html: Har aldrig körts efter en utrullning.
275
+ ran_html: Kördes %{ago} på <code>%{revision}</code> (utrullning).
276
+ waiting_lagging_html:
277
+ one: 'Väntar på utrullning: 1 process kör ännu inte <code>%{revision}</code>.'
278
+ other: 'Väntar på utrullning: %{count} processer kör ännu inte <code>%{revision}</code>.'
279
+ waiting_no_revision_html: 'Väntar: appens revision är inte konfigurerad.'
280
+ waiting_no_scheduler_html: 'Väntar: ingen schemaläggare körs.'
281
+ waiting_no_workers_html: Väntar på en worker på <code>%{revision}</code>.
282
+ waiting_unknown_html: 'Väntar: utrullningsstatusen kunde inte läsas.'
283
+ run_now: Kör nu
284
+ show:
285
+ armed_html: "%{armed_for}, förberedd %{ago}"
286
+ back: Tillbaka
287
+ configuration: Konfiguration
288
+ labels:
289
+ arguments: Argument
290
+ armed: Väntande körning
291
+ command: Kommando
292
+ description: Beskrivning
293
+ job_class: Jobbklass
294
+ priority: Prioritet
295
+ queue: Kö
296
+ source: Källa
297
+ last_run: Senaste körning
298
+ no_runs: Inga körningar registrerade ännu
299
+ recent_runs: Senaste körningar
300
+ run_headers:
301
+ enqueued_at: Köad
302
+ queue: Kö
303
+ revision: Revision
304
+ triggered_by: Startad av
305
+ version: Version
306
+ source_config: Konfigurationsfil
307
+ source_dynamic: Dynamisk
308
+ state: Tillstånd
309
+ version: Version
310
+ states:
311
+ disabled: Inaktiverad
312
+ due: Redo
313
+ never: Aldrig körd
314
+ ran: Körd
315
+ waiting: Väntar
316
+ tasks_table:
317
+ empty_hint_html: Deklarera engångsuppgifter i <code>config/deploy_tasks.yml</code>; <code>rails generate pgbus:add_deploy_tasks</code> skapar en startfil.
318
+ empty_title: Inga deploy-uppgifter deklarerade
319
+ headers:
320
+ actions: Åtgärder
321
+ last_run: Senaste körning
322
+ queue: Kö
323
+ state: Tillstånd
324
+ task: Uppgift
325
+ version: Version
326
+ never: Aldrig
327
+ toggle:
328
+ disabled: Deploy-uppgiften inaktiverad
329
+ enabled: Deploy-uppgiften aktiverad
330
+ failed: Det gick inte att växla deploy-uppgiften
331
+ triggered_by:
332
+ manual: Manuellt
333
+ rollover: Utrullning
334
+ version: v%{version}
239
335
  dialogs:
240
336
  cancel: Avbryt
241
337
  confirm: Bekräfta
@@ -617,6 +713,7 @@ sv:
617
713
  nav:
618
714
  batches: Grupper
619
715
  dashboard: Instrumentpanel
716
+ deploy_tasks: Deploy
620
717
  dlq: DLQ
621
718
  events: Händelser
622
719
  insights: Insikter
data/config/routes.rb CHANGED
@@ -45,6 +45,13 @@ Pgbus::Engine.routes.draw do
45
45
  end
46
46
  end
47
47
 
48
+ resources :deploy_tasks, only: %i[index show] do
49
+ member do
50
+ post :toggle
51
+ post :enqueue
52
+ end
53
+ end
54
+
48
55
  resources :batches, only: %i[index show]
49
56
  resources :processes, only: [:index]
50
57
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+ require_relative "migration_path"
6
+
7
+ module Pgbus
8
+ module Generators
9
+ class AddDeployTasksGenerator < Rails::Generators::Base
10
+ include ActiveRecord::Generators::Migration
11
+ include MigrationPath
12
+
13
+ source_root File.expand_path("templates", __dir__)
14
+
15
+ desc "Add deploy tasks tables (one-time jobs run after a rollover) to an existing Pgbus installation"
16
+
17
+ class_option :database,
18
+ type: :string,
19
+ default: nil,
20
+ desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
21
+
22
+ def create_migration_file
23
+ migration_template "add_deploy_tasks_tables.rb.erb",
24
+ File.join(pgbus_migrate_path, "add_pgbus_deploy_tasks_tables.rb")
25
+ end
26
+
27
+ def create_deploy_tasks_config
28
+ template "deploy_tasks.yml.erb", "config/deploy_tasks.yml"
29
+ end
30
+
31
+ def display_post_install
32
+ say ""
33
+ say "Pgbus deploy tasks installed!", :green
34
+ say ""
35
+ say "Next steps:"
36
+ say " 1. Run: rails db:migrate#{migrate_command_suffix}"
37
+ say " 2. Edit config/deploy_tasks.yml to declare your one-time tasks"
38
+ say " 3. Make sure every pgbus process knows the app revision (config.app_revision,"
39
+ say " PGBUS_REVISION, DASH_VERSION, KAMAL_VERSION or a REVISION file)"
40
+ say " 4. Restart pgbus: bin/pgbus start"
41
+ say ""
42
+ end
43
+
44
+ private
45
+
46
+ def migration_version
47
+ "[#{ActiveRecord::Migration.current_version}]"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ class AddPgbusDeployTasksTables < ActiveRecord::Migration<%= migration_version %>
2
+ # if_not_exists throughout: pgbus:update queues this migration when either
3
+ # table is missing, so a half-present schema must not fail it.
4
+ def change
5
+ create_table :pgbus_deploy_tasks, if_not_exists: true do |t|
6
+ t.string :key, null: false
7
+ t.string :class_name
8
+ t.string :command, limit: 2048
9
+ t.text :arguments
10
+ t.string :queue_name
11
+ t.integer :priority, default: 0
12
+ t.string :version, null: false, default: "1"
13
+ t.boolean :every_deploy, null: false, default: false
14
+ t.boolean :static, null: false, default: true
15
+ t.boolean :enabled, null: false, default: true
16
+ t.text :description
17
+ t.datetime :armed_at
18
+ t.string :armed_for
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :pgbus_deploy_tasks, :key,
23
+ unique: true, name: "idx_pgbus_deploy_tasks_key", if_not_exists: true
24
+ add_index :pgbus_deploy_tasks, :armed_at,
25
+ where: "armed_at IS NOT NULL", name: "idx_pgbus_deploy_tasks_armed", if_not_exists: true
26
+
27
+ create_table :pgbus_deploy_task_runs, if_not_exists: true do |t|
28
+ t.string :task_key, null: false
29
+ t.string :version, null: false
30
+ t.string :revision
31
+ t.string :triggered_by, null: false
32
+ t.string :queue_name
33
+ t.bigint :msg_id
34
+ t.datetime :run_at, null: false
35
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
36
+ end
37
+
38
+ add_index :pgbus_deploy_task_runs, [:task_key, :run_at],
39
+ name: "idx_pgbus_deploy_task_runs_task", if_not_exists: true
40
+ add_index :pgbus_deploy_task_runs, :run_at,
41
+ name: "idx_pgbus_deploy_task_runs_cleanup", if_not_exists: true
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ # Pgbus Deploy Tasks Configuration
2
+ #
3
+ # One-time jobs that run once the whole fleet has rolled over to a new app
4
+ # revision: a backfill, a data fix, a cache warm. Same shape as
5
+ # config/recurring.yml, without schedule:. Supports environment-scoped or
6
+ # flat configuration.
7
+ #
8
+ # The scheduler enqueues a task only when every live pgbus process reports
9
+ # the scheduler's own app revision and at least one worker on it is alive,
10
+ # so a task never runs on old code during the deploy overlap window. The
11
+ # revision comes from config.app_revision, then PGBUS_REVISION, DASH_VERSION,
12
+ # KAMAL_VERSION, then the REVISION file at Rails.root. Without one, deploy
13
+ # tasks wait (the dashboard says so) and only "Run now" enqueues them.
14
+ #
15
+ # Format:
16
+ # task_key:
17
+ # class: JobClassName # ActiveJob class to run (or use command:)
18
+ # version: 1 # Runs once per version; bump it to run again after the next deploy
19
+ # every_deploy: false # true: run after every rollover instead (version is ignored)
20
+ # queue: default # Optional: queue name (default: pgbus default queue)
21
+ # args: [arg1, arg2] # Optional: job arguments
22
+ # priority: 0 # Optional: numeric priority
23
+ # description: "text" # Optional: human-readable description
24
+ #
25
+ # Command-based tasks (no job class needed):
26
+ # warm_fragment_caches:
27
+ # command: "FragmentCacheWarmer.run"
28
+ # every_deploy: true
29
+ #
30
+ # Examples:
31
+ #
32
+ # production:
33
+ # backfill_order_totals:
34
+ # class: BackfillOrderTotalsJob
35
+ # version: 2
36
+ # queue: maintenance
37
+ # args: [{ batch_size: 500 }]
38
+ # description: Recompute totals after the currency migration
39
+ #
40
+ # warm_fragment_caches:
41
+ # command: "FragmentCacheWarmer.run"
42
+ # every_deploy: true
@@ -170,6 +170,47 @@ class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
170
170
  add_index :pgbus_recurring_executions, :run_at,
171
171
  name: "idx_pgbus_recurring_executions_cleanup"
172
172
 
173
+ # Deploy tasks (synced from config/deploy_tasks.yml): one-time jobs the
174
+ # scheduler enqueues once the fleet has rolled over to a new revision.
175
+ # armed_at/armed_for is the compare-and-set claim; runs are the history.
176
+ create_table :pgbus_deploy_tasks do |t|
177
+ t.string :key, null: false
178
+ t.string :class_name
179
+ t.string :command, limit: 2048
180
+ t.text :arguments
181
+ t.string :queue_name
182
+ t.integer :priority, default: 0
183
+ t.string :version, null: false, default: "1"
184
+ t.boolean :every_deploy, null: false, default: false
185
+ t.boolean :static, null: false, default: true
186
+ t.boolean :enabled, null: false, default: true
187
+ t.text :description
188
+ t.datetime :armed_at
189
+ t.string :armed_for
190
+ t.timestamps
191
+ end
192
+
193
+ add_index :pgbus_deploy_tasks, :key,
194
+ unique: true, name: "idx_pgbus_deploy_tasks_key"
195
+ add_index :pgbus_deploy_tasks, :armed_at,
196
+ where: "armed_at IS NOT NULL", name: "idx_pgbus_deploy_tasks_armed"
197
+
198
+ create_table :pgbus_deploy_task_runs do |t|
199
+ t.string :task_key, null: false
200
+ t.string :version, null: false
201
+ t.string :revision
202
+ t.string :triggered_by, null: false
203
+ t.string :queue_name
204
+ t.bigint :msg_id
205
+ t.datetime :run_at, null: false
206
+ t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
207
+ end
208
+
209
+ add_index :pgbus_deploy_task_runs, [:task_key, :run_at],
210
+ name: "idx_pgbus_deploy_task_runs_task"
211
+ add_index :pgbus_deploy_task_runs, :run_at,
212
+ name: "idx_pgbus_deploy_task_runs_cleanup"
213
+
173
214
  # Stream queue registry — records which PGMQ queues back streams (vs jobs).
174
215
  # Stream and job queues share the pgbus_ namespace, so maintenance
175
216
  # (per-stream retention, orphan sweep) and wildcard workers rely on this
@@ -204,6 +245,8 @@ class CreatePgbusTables < ActiveRecord::Migration<%= migration_version %>
204
245
  execute "SELECT pgmq.drop_queue('pgbus_default')"
205
246
 
206
247
  drop_table :pgbus_stream_queues
248
+ drop_table :pgbus_deploy_task_runs
249
+ drop_table :pgbus_deploy_tasks
207
250
  drop_table :pgbus_recurring_executions
208
251
  drop_table :pgbus_recurring_tasks
209
252
  drop_table :pgbus_batch_executions
@@ -28,7 +28,9 @@ module Pgbus
28
28
  included do
29
29
  attr_accessor :pgbus_current_attributes
30
30
 
31
- class_attribute :pgbus_persist_current_attributes, instance_writer: false, default: nil
31
+ # A per-class setting the job's own class body writes at load; jobs
32
+ # only read it while they serialize.
33
+ class_attribute :pgbus_persist_current_attributes, instance_writer: false, default: nil # rubocop:disable ThreadSafety/ClassAndModuleAttributes
32
34
  end
33
35
 
34
36
  def serialize
data/lib/pgbus/batch.rb CHANGED
@@ -209,22 +209,28 @@ module Pgbus
209
209
  0
210
210
  end
211
211
 
212
+ # Schema-probe memos (a true answer is kept for the life of the process)
213
+ # and the warn-once flag, read and written under @schema_lock (issue
214
+ # #527). The probes run outside it: a duplicate probe on a race is
215
+ # harmless, a lock held across a query is not.
216
+ @schema_lock = Mutex.new
217
+
212
218
  def self.executions_migrated?
213
- return true if @executions_migrated
219
+ return true if @schema_lock.synchronize { @executions_migrated }
214
220
 
215
221
  result = begin
216
222
  BatchExecution.table_exists?
217
223
  rescue StandardError
218
224
  false
219
225
  end
220
- @executions_migrated = true if result
226
+ @schema_lock.synchronize { @executions_migrated = true } if result
221
227
  result
222
228
  end
223
229
 
224
230
  def self.warn_callback_jobs_unmigrated
225
- return if @warned_callback_jobs_unmigrated
231
+ first = @schema_lock.synchronize { @warned_callback_jobs_unmigrated ? false : (@warned_callback_jobs_unmigrated = true) }
232
+ return unless first
226
233
 
227
- @warned_callback_jobs_unmigrated = true
228
234
  Pgbus.logger.warn do
229
235
  "[Pgbus] Batch callback configured as an ActiveJob instance, but pgbus_batches has no " \
230
236
  "on_*_job columns yet — .set options (queue, wait, priority) are ignored until " \
@@ -233,23 +239,25 @@ module Pgbus
233
239
  end
234
240
 
235
241
  def self.reset_executions_migrated_cache!
236
- @executions_migrated = nil
237
- @callback_jobs_migrated = nil
238
- @warned_callback_jobs_unmigrated = nil
242
+ @schema_lock.synchronize do
243
+ @executions_migrated = nil
244
+ @callback_jobs_migrated = nil
245
+ @warned_callback_jobs_unmigrated = nil
246
+ end
239
247
  end
240
248
 
241
249
  # True once the on_finish_job / on_success_job / on_failure_job jsonb
242
250
  # columns exist (issue #415). Until then, configured callback instances
243
251
  # have nowhere to live and only bare classes are stored.
244
252
  def self.callback_jobs_migrated?
245
- return true if @callback_jobs_migrated
253
+ return true if @schema_lock.synchronize { @callback_jobs_migrated }
246
254
 
247
255
  result = begin
248
256
  BatchEntry.column_names.include?("on_finish_job")
249
257
  rescue StandardError
250
258
  false
251
259
  end
252
- @callback_jobs_migrated = true if result
260
+ @schema_lock.synchronize { @callback_jobs_migrated = true } if result
253
261
  result
254
262
  end
255
263
 
data/lib/pgbus/cli.rb CHANGED
@@ -37,6 +37,9 @@ module Pgbus
37
37
 
38
38
  def start_supervisor(args = [])
39
39
  apply_start_options(args)
40
+ # The flags go through setters, after boot finalized the configuration:
41
+ # finalize their values too before the supervisor forks.
42
+ Pgbus.finalize!
40
43
  Pgbus.logger.info { "[Pgbus] Starting Pgbus #{Pgbus::VERSION}..." }
41
44
 
42
45
  supervisor = Process::Supervisor.new
@@ -9,7 +9,16 @@ module Pgbus
9
9
  # The cursor is the highest msg_id the client has already seen. Replay returns
10
10
  # everything strictly greater, ordered by msg_id ASC, capped by `limit`.
11
11
  module ReadAfter
12
- Envelope = Data.define(:msg_id, :enqueued_at, :payload, :source)
12
+ # Ractor-shareable like the streamer's other dispatch values (issue
13
+ # #527): the Strings are frozen in place, they are the read's own.
14
+ Envelope = Data.define(:msg_id, :enqueued_at, :payload, :source) do
15
+ def initialize(msg_id:, enqueued_at:, payload:, source:)
16
+ enqueued_at&.freeze
17
+ payload&.freeze
18
+ source&.freeze
19
+ super
20
+ end
21
+ end
13
22
 
14
23
  DEFAULT_LIMIT = 500
15
24