From a2b07b07ddf5e6395efdd6bdc37852ccab952da9 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:03:33 +0000 Subject: [PATCH 1/7] Rust #325: run the repair pass from native's maintenance ticker The published image's scheduler loop runs workflow:v2:repair-pass every few seconds unless DW_V2_MATCHING_ROLE_QUEUE_WAKE is false or 0, and the parity reference runs it every second. Native's ticker fired timers and schedules and enforced activity and cooperative deadlines, but ran the repair pass only on request. So an idle run past its execution or run deadline was never given the task that closes it, and overdue ready tasks and due service calls waited for someone to ask. The ticker now also runs the repair pass with PHP's loop throttle (at most once per 5 seconds, shared with requested passes that respect it). DW_V2_MATCHING_ROLE_QUEUE_WAKE=false or 0 turns it off, as it does PHP's. Runtime::set_maintenance_repair does the same for one runtime. A new test waits for the loop alone to give an idle run past its deadline its workflow task. The two tests that count one requested pass, or keep expired leases until they retry, turn the loop off. The execution suite passes with the loop on (558), and native passes the shared fixtures most exposed to background repair (repair-pass, the lease, cancellation and deadline fixtures, worker sessions, service calls). Co-Authored-By: Claude Opus 5.5 --- .../2026-10-26-maintenance-repair.md | 35 +++++++++++++++++++ rust/src/runtime/execution.rs | 31 +++++++++++++--- rust/tests/execution/repair_pass.rs | 28 ++++++++++++++- rust/tests/execution/worker_pressure.rs | 2 ++ 4 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 docs/rust-server-port-checkpoints/2026-10-26-maintenance-repair.md diff --git a/docs/rust-server-port-checkpoints/2026-10-26-maintenance-repair.md b/docs/rust-server-port-checkpoints/2026-10-26-maintenance-repair.md new file mode 100644 index 000000000..050f0fb9e --- /dev/null +++ b/docs/rust-server-port-checkpoints/2026-10-26-maintenance-repair.md @@ -0,0 +1,35 @@ +# October 26, the repair pass runs on its own + +- **Found by.** A review of what native does without a request. The published + image's `scheduler` service (`docker-compose.yml`) loops every 10 seconds + through: + - `schedule:evaluate`; + - `workflow:v2:repair-pass`, unless `DW_V2_MATCHING_ROLE_QUEUE_WAKE` is false + or 0; + - activity timeout enforcement; + - external payload cleanup; + - history pruning. + + The parity reference runs the same loop every second. Native's ticker + already fired due timers and schedules every 250 ms, and enforced activity + and cooperative deadlines. The repair pass ran only when someone called + `POST /api/system/repair/pass`. So an idle run past its execution or run + deadline was never given the task that closes it, and overdue ready tasks + and due service calls waited for a request. +- **Change.** The ticker now also runs the repair pass with PHP's loop + throttle: at most once per 5 seconds, shared with requested passes that + respect it. `DW_V2_MATCHING_ROLE_QUEUE_WAKE=false` (or `0`) turns it off, as + it does PHP's. `Runtime::set_maintenance_repair` does the same for one + runtime. +- **Tests.** + - `the_maintenance_loop_gives_an_overdue_idle_run_its_deadline_task` waits + for the loop alone to give an idle run past its deadline its workflow task. + - The two tests that count one requested pass, or keep expired leases until + they retry, turn the loop off. + - The execution suite passes with the loop on (558). + - Native passes, with the loop on, the shared fixtures most exposed to + background repair: repair-pass, the lease, cancellation and deadline + fixtures, worker sessions and service calls. +- **Still to do.** History pruning and external payload cleanup also run in + PHP's loop; native runs them only on request (`POST + /api/system/retention/pass` and its payload cleanup). diff --git a/rust/src/runtime/execution.rs b/rust/src/runtime/execution.rs index a5678ad41..a75b78606 100644 --- a/rust/src/runtime/execution.rs +++ b/rust/src/runtime/execution.rs @@ -52,6 +52,15 @@ struct TimerScheduler { stop: watch::Sender, task: Mutex>>, healthy: Arc, + /// Whether the ticker also runs the repair pass, as PHP's maintenance + /// loop does unless `DW_V2_MATCHING_ROLE_QUEUE_WAKE` is false or 0. + repair: Arc, +} + +/// PHP's maintenance loop skips `workflow:v2:repair-pass` when +/// `DW_V2_MATCHING_ROLE_QUEUE_WAKE` is `false` or `0`. +fn maintenance_repair_enabled() -> bool { + !matches!(std::env::var("DW_V2_MATCHING_ROLE_QUEUE_WAKE").as_deref(), Ok("false" | "0")) } impl TimerScheduler { @@ -61,11 +70,13 @@ impl TimerScheduler { stop, task: Mutex::new(None), healthy: Arc::new(AtomicBool::new(true)), + repair: Arc::new(AtomicBool::new(maintenance_repair_enabled())), } } fn start(&self, storage: Arc) { let mut stop = self.stop.subscribe(); let healthy = self.healthy.clone(); + let repair = self.repair.clone(); let task = tokio::spawn(async move { loop { tokio::select! { @@ -75,10 +86,15 @@ impl TimerScheduler { // retried from durable state, without logging payloads. // Timer and schedule transactions share the cross-node lock with // completion. CPU/deadline performance is unqualified. - let result = async { match &*storage { - Storage::Sqlite(store) => { store.fire_due_timers().await?; store.fire_due_schedules().await }, - Storage::Postgres(store) => { store.fire_due_timers().await?; store.fire_due_schedules().await }, - Storage::MySql(store) => { store.fire_due_timers().await?; store.fire_due_schedules().await }, + // Then PHP's maintenance loop (`workflow:v2:repair-pass`), + // at most once per repair throttle: run deadlines, task + // repair, activity and service-call timeouts. + let pass = repair.load(Ordering::Relaxed) + .then(|| super::repair_pass::PassRequest { respect_throttle: true, ..Default::default() }); + let result: Result<()> = async { match &*storage { + Storage::Sqlite(store) => { store.fire_due_timers().await?; store.fire_due_schedules().await?; if let Some(pass) = &pass { store.repair_pass(pass).await?; } Ok(()) }, + Storage::Postgres(store) => { store.fire_due_timers().await?; store.fire_due_schedules().await?; if let Some(pass) = &pass { store.repair_pass(pass).await?; } Ok(()) }, + Storage::MySql(store) => { store.fire_due_timers().await?; store.fire_due_schedules().await?; if let Some(pass) = &pass { store.repair_pass(pass).await?; } Ok(()) }, }}.await; let success = result.is_ok(); if healthy.swap(success,Ordering::Relaxed) && !success { @@ -188,6 +204,13 @@ impl Runtime { Ok(()) } + /// Turns the maintenance loop's repair pass on or off for this runtime, + /// as `DW_V2_MATCHING_ROLE_QUEUE_WAKE` does at startup. Timers, schedules + /// and deadlines still fire. + pub fn set_maintenance_repair(&self, enabled: bool) { + self.scheduler.repair.store(enabled, Ordering::Relaxed); + } + pub async fn open(database: &str, token: String) -> Result { Self::validate_token(&token)?; let external = super::ExternalPayloadConfig::from_env()?; diff --git a/rust/tests/execution/repair_pass.rs b/rust/tests/execution/repair_pass.rs index e4597687c..30fe24a28 100644 --- a/rust/tests/execution/repair_pass.rs +++ b/rust/tests/execution/repair_pass.rs @@ -102,7 +102,10 @@ async fn repair_pass_recovers_tasks_and_runs_like_php() { #[tokio::test] async fn repair_pass_creates_deadline_tasks_settles_leases_and_reports_failures() { let database = TestDatabase::new().await; - let app = router(database.open().await.unwrap()); + let runtime = database.open().await.unwrap(); + // This test counts what one requested pass repairs. + runtime.set_maintenance_repair(false); + let app = router(runtime); register(&app, "worker", json!(["echo"]), json!(["act"])).await; let deadline = started_on(&app, "deadline", "repair-d").await; @@ -173,3 +176,26 @@ async fn repair_pass_creates_deadline_tasks_settles_leases_and_reports_failures( assert_eq!(invalid["errors"]["run_ids.0"], json!(["The run_ids.0 field must be a string.", "The run_ids.0 field must be at least 1 characters."])); assert_eq!(invalid["errors"]["queue"], json!(["The queue field must not be greater than 128 characters."])); } + +// PHP's maintenance loop runs the repair pass every few seconds; native's +// ticker does the same unless the pass is turned off. An idle run past its +// execution deadline gets the workflow task that closes it without anyone +// asking for a pass. +#[tokio::test] +async fn the_maintenance_loop_gives_an_overdue_idle_run_its_deadline_task() { + let database = TestDatabase::new().await; + let app = router(database.open().await.unwrap()); + let deadline = started_on(&app, "maintained", "repair-m").await; + let deadline = deadline["run_id"].as_str().unwrap(); + execute(&database, format!("UPDATE workflow_tasks SET status='completed' WHERE workflow_run_id='{deadline}'")).await; + execute(&database, format!("UPDATE workflow_runs SET execution_deadline_at={LONG_AGO} WHERE id='{deadline}'")).await; + let ready = format!("SELECT COUNT(*) FROM workflow_tasks WHERE workflow_run_id='{deadline}' AND status='ready'"); + // The loop's throttle is five seconds; allow two turns. + for _ in 0..48 { + if count(&database, ready.clone()).await == 1 { + return; + } + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + } + panic!("no deadline task after twelve seconds of maintenance"); +} diff --git a/rust/tests/execution/worker_pressure.rs b/rust/tests/execution/worker_pressure.rs index 5e06dd0fb..55c8dc1bd 100644 --- a/rust/tests/execution/worker_pressure.rs +++ b/rust/tests/execution/worker_pressure.rs @@ -92,6 +92,8 @@ impl TestDatabase { async fn real_shutdown_lock_pressure_preserves_original_leases_and_retries_same_token() { let database = TestDatabase::new().await; let runtime = database.open().await.unwrap(); + // Expired leases must stay as the shutdown left them until the test retries. + runtime.set_maintenance_repair(false); let app = router(runtime.clone()); let worker = "pressure-worker"; let accepted = registration(&app, worker).await; From fe2090bc8bedc8423eeb294226e416aa97cc18a2 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:14:13 +0000 Subject: [PATCH 2/7] Keep PHP runtime credentials at takeover Native serves runtime credentials from PHP's own table and checks the same SHA-256 token digests, so the rows need no conversion and PHP-issued worker tokens keep working after cutover. Takeover no longer refuses them. Shared PHP rows are checked on SQLite, PostgreSQL, MySQL and MariaDB: the live token authenticates, the revoked one stays refused. Co-Authored-By: Claude Opus 5.5 --- .../2026-10-26-php-2111-takeover.md | 5 ++-- ...2026-10-26-takeover-runtime-credentials.md | 23 ++++++++++++++++ rust/src/runtime/mysql.rs | 6 +++++ rust/src/runtime/postgres.rs | 6 +++++ rust/src/runtime/runtime_credentials.rs | 27 +++++++++++++++++++ rust/src/runtime/upgrade/takeover.rs | 4 +-- rust/src/runtime/upgrade_tests.rs | 21 ++++++++++++--- 7 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 docs/rust-server-port-checkpoints/2026-10-26-takeover-runtime-credentials.md diff --git a/docs/rust-server-port-checkpoints/2026-10-26-php-2111-takeover.md b/docs/rust-server-port-checkpoints/2026-10-26-php-2111-takeover.md index a44d8478d..631a54615 100644 --- a/docs/rust-server-port-checkpoints/2026-10-26-php-2111-takeover.md +++ b/docs/rust-server-port-checkpoints/2026-10-26-php-2111-takeover.md @@ -82,5 +82,6 @@ Before, it reported the three service-call counters as zeros. `the_repair_pass_times_out_calls_past_their_deadline` covers it. - **Still refused.** Takeover still refuses a source holding rows native - cannot carry over yet: runtime credentials, message streams, update - validation tasks and external payloads. + cannot carry over yet: message streams, update validation tasks and + external payloads. Runtime credentials carried over later the same day + ([2026-10-26-takeover-runtime-credentials.md](2026-10-26-takeover-runtime-credentials.md)). diff --git a/docs/rust-server-port-checkpoints/2026-10-26-takeover-runtime-credentials.md b/docs/rust-server-port-checkpoints/2026-10-26-takeover-runtime-credentials.md new file mode 100644 index 000000000..d096e904d --- /dev/null +++ b/docs/rust-server-port-checkpoints/2026-10-26-takeover-runtime-credentials.md @@ -0,0 +1,23 @@ +# October 26, takeover keeps runtime credentials + +- **Before.** Takeover refused any PHP database holding a + `runtime_credentials` row (`upgrade_source_runtime_credentials_unsupported`). + An operator who had issued worker tokens through + `/api/runtime-credentials` could not cut over without + revoking them all and reissuing tokens to every worker. +- **Why it is safe to keep them.** Native serves runtime credentials from the + same `runtime_credentials` table, with the same columns. PHP stores + `hash('sha256', $token)` as lowercase hex; native looks a token up by the same + digest. Expiry, revocation, roles, tenant and claims are read from the same + columns. So the rows need no conversion, and a token PHP issued keeps + working after takeover. +- **Change.** The refusal is removed. Takeover leaves the table as it is. +- **Tests.** `PHP_CREDENTIAL_ROWS` stores one live and one revoked credential + the way PHP does, with digests computed outside native. After takeover the + live PHP token authenticates with its subject, roles, tenant and claims, + and the revoked one is refused: + - SQLite: `takeover_keeps_php_runtime_credentials_that_native_then_accepts`; + - PostgreSQL, MySQL and MariaDB: the 2.11.1 takeover qualification, for + both the fresh and the upgraded-from-2.9.7 layouts. +- **Still refused.** Message streams, update validation tasks and external + payloads. diff --git a/rust/src/runtime/mysql.rs b/rust/src/runtime/mysql.rs index e7eaa662a..161dd0ad6 100644 --- a/rust/src/runtime/mysql.rs +++ b/rust/src/runtime/mysql.rs @@ -1395,6 +1395,9 @@ mod tests { "INSERT INTO workflow_worker_sessions(namespace,session_id,queue,status,lease_owner,lease_expires_at,ttl_expires_at,created_at,updated_at) VALUES ('default','held','q','active','current','2030-01-01 00:00:00','2030-01-01 00:00:00','2026-10-20 09:00:00','2026-10-20 09:00:00');", "INSERT INTO workflow_service_calls(id,namespace,endpoint_name,service_name,operation_name,caller_namespace,target_namespace,status,outcome,operation_mode,accepted_at,call_deadline_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','default','pay','billing','charge','default','default','accepted','accepted','async','2026-10-26 10:00:00.000000','2027-02-03 04:05:06.500000','2026-10-26 10:00:00','2026-10-26 10:00:00');", )).await.unwrap(); + for statement in crate::runtime::runtime_credentials::PHP_CREDENTIAL_ROWS { + sqlx::raw_sql(statement).execute(&mut connection).await.unwrap(); + } connection.close().await.unwrap(); let fingerprint = backup_fingerprint(&db).await; let report = takeover::take_over_mysql(db.options.clone(), &fingerprint, &TAKEOVER_QUEUES) @@ -1422,6 +1425,9 @@ mod tests { takeover::take_over_mysql(db.options.clone(), &fingerprint, &TAKEOVER_QUEUES).await, "upgrade_source_already_owned", ); + let runtime = crate::runtime::Runtime::open_mysql(db.options.clone(), "test-token".into()).await.unwrap(); + crate::runtime::runtime_credentials::assert_php_credentials_served(&runtime).await; + runtime.close().await; db.remove().await; } } diff --git a/rust/src/runtime/postgres.rs b/rust/src/runtime/postgres.rs index 9eb4b02e1..6fe0ae14a 100644 --- a/rust/src/runtime/postgres.rs +++ b/rust/src/runtime/postgres.rs @@ -882,6 +882,9 @@ mod tests { .execute(&mut connection) .await .unwrap(); + for statement in crate::runtime::runtime_credentials::PHP_CREDENTIAL_ROWS { + sqlx::raw_sql(statement).execute(&mut connection).await.unwrap(); + } connection.close().await.unwrap(); let fingerprint = backup_fingerprint(&db).await; let report = takeover::take_over_postgres(db.options.clone(), &fingerprint, &TAKEOVER_QUEUES) @@ -906,6 +909,9 @@ mod tests { let storage = PostgresStorage::open(db.options.clone()).await.unwrap(); storage.close().await; PostgresStorage::check(db.options.clone()).await.unwrap(); + let runtime = crate::runtime::Runtime::open_postgres(db.options.clone(), "test-token".into()).await.unwrap(); + crate::runtime::runtime_credentials::assert_php_credentials_served(&runtime).await; + runtime.close().await; db.remove().await; } } diff --git a/rust/src/runtime/runtime_credentials.rs b/rust/src/runtime/runtime_credentials.rs index 8034ee416..2643c773e 100644 --- a/rust/src/runtime/runtime_credentials.rs +++ b/rust/src/runtime/runtime_credentials.rs @@ -393,6 +393,33 @@ where } } +/// Credentials as published PHP stores them: `hash('sha256', $token)` of +/// each token. The first is live until 2030, the second is revoked. +#[cfg(test)] +pub(crate) const PHP_CREDENTIAL_ROWS: [&str; 2] = [ + r#"INSERT INTO runtime_credentials(id,name,subject,roles,tenant,claims,token_prefix,token_hash,expires_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcred','worker key','worker-a','["worker"]','default','{"region":"eu"}','dw_live_abc','5ac9039987e0c53110302d98235eafd5b48403cc74f32a9dc365418eab15bfa2','2030-01-01 00:00:00','2026-10-26 10:00:00','2026-10-26 10:00:00')"#, + r#"INSERT INTO runtime_credentials(id,name,subject,roles,tenant,claims,token_prefix,token_hash,revoked_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kgone','old key','worker-b','["worker"]','default','[]','dw_live_old','2b968f18b9fe6bbd5de84bd3dfa0cf8e2eaf41be6a2e9a8ca860cae3a28321e9','2026-10-26 11:00:00','2026-10-26 10:00:00','2026-10-26 11:00:00')"#, +]; + +/// What native accepts over `PHP_CREDENTIAL_ROWS` after a takeover: the live +/// PHP-issued token authenticates as its subject, and the revoked one does not. +#[cfg(test)] +pub(crate) async fn assert_php_credentials_served(runtime: &super::Runtime) { + let principal = runtime + .runtime_credential_principal("dw_live_abcdefghijklmnopqrstuvwxyz") + .await + .unwrap() + .expect("the PHP-issued token authenticates"); + assert_eq!( + (principal.subject.as_str(), principal.roles.as_slice(), principal.method, principal.tenant.as_deref(), &principal.claims), + ("worker-a", ["worker".to_owned()].as_slice(), "runtime-token", Some("default"), &json!({"region": "eu"})) + ); + assert!( + runtime.runtime_credential_principal("dw_live_oldoldoldoldoldoldoldold").await.unwrap().is_none(), + "a revoked key stays refused" + ); +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/src/runtime/upgrade/takeover.rs b/rust/src/runtime/upgrade/takeover.rs index 3d8aec310..c7a6155c1 100644 --- a/rust/src/runtime/upgrade/takeover.rs +++ b/rust/src/runtime/upgrade/takeover.rs @@ -221,9 +221,7 @@ fn report(backend: &str, layout: &str, digest: &Value, inventory: &Value, conver /// Published PHP features native does not implement yet. A source holding any /// of their rows is refused, rather than taken over without them. Derived /// projections and PHP's own queue and poll bookkeeping are left in place. -const UNSUPPORTED_FEATURES: [(&str, &[&str]); 3] = [ - ("upgrade_source_runtime_credentials_unsupported", &["runtime_credentials"]), - ( +const UNSUPPORTED_FEATURES: [(&str, &[&str]); 2] = [ ( "upgrade_source_message_streams_unsupported", &[ "workflow_messages", diff --git a/rust/src/runtime/upgrade_tests.rs b/rust/src/runtime/upgrade_tests.rs index 2acef0699..4bb4f1651 100644 --- a/rust/src/runtime/upgrade_tests.rs +++ b/rust/src/runtime/upgrade_tests.rs @@ -1141,10 +1141,6 @@ async fn takeover_refusals_leave_the_php_database_unchanged() { "upgrade_source_external_payloads_unsupported", ), // Features native lacks: their rows would be left behind unread. - ( - "INSERT INTO runtime_credentials(id,subject,roles,tenant,token_prefix,token_hash) VALUES ('c','worker','[]','default','dw_','h')", - "upgrade_source_runtime_credentials_unsupported", - ), ( "INSERT INTO workflow_messages(workflow_instance_id,direction,channel,stream_key,sequence) VALUES ('w','inbound','c','k',1)", "upgrade_source_message_streams_unsupported", @@ -1271,6 +1267,23 @@ async fn takeover_keeps_php_service_calls_and_their_deadlines() { runtime.close().await; } +/// PHP's runtime credentials carry over: native stores and checks the same +/// SHA-256 token hashes in the same table, so a worker's PHP-issued token +/// still authenticates after takeover, and a revoked one still does not. +#[tokio::test] +async fn takeover_keeps_php_runtime_credentials_that_native_then_accepts() { + let (_directory, path) = php_release_layout(&[PHP_280_CHANGES, TRACE_CARRIERS, QUEUE_DELIVERY]).await; + for statement in crate::runtime::runtime_credentials::PHP_CREDENTIAL_ROWS { + mutate(&path, statement).await; + } + let fingerprint = backup_fingerprint(&path).await; + let report = takeover::take_over_sqlite(&path, &fingerprint, &queue_config()).await.unwrap(); + assert_eq!(report["status"], "taken_over", "{report}"); + let runtime = crate::runtime::Runtime::open(&path, "test-token".into()).await.unwrap(); + crate::runtime::runtime_credentials::assert_php_credentials_served(&runtime).await; + runtime.close().await; +} + /// The frozen layout in PHP Server's default WAL mode, copied while its /// writer was still open: one committed row is only in the WAL, as a crashed /// PHP would leave it. From 84c5085e469c114d03c61e996d986fe140e939da Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:40:18 +0000 Subject: [PATCH 3/7] Take over published PHP Server 2.11.3 databases Server 2.11.3 (Core 2.14.2) was published today, and native refused every 2.11.3 database as unqualified. Each recorded layout now names its parent, so 2.11.3 descends from both 2.11.1 variants. Deltas were captured from real published-image databases on SQLite, PostgreSQL, MySQL and MariaDB along five upgrade paths; all gave the same delta from their parent. The recipe copies PHP's recorded session holders, counts and drops full failure messages, completed dispatches, the idempotency digest and the stream reclaim deadline, and refuses while a dispatch is still pending (native cannot resume it). MySQL runs it as resumable journal step 10. A php2113 CI cohort takes over a published 2.11.3 source end to end. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/server-parity.yml | 50 +++ .../2026-10-26-php-2113-takeover.md | 60 +++ docs/rust-server-port.md | 10 + docs/rust-server-upgrade-takeover.md | 10 + rust/src/runtime/mysql.rs | 94 +++-- rust/src/runtime/postgres.rs | 60 +++ rust/src/runtime/schema.rs | 2 + rust/src/runtime/upgrade.rs | 72 +++- rust/src/runtime/upgrade/dispatch_scan.rs | 31 ++ rust/src/runtime/upgrade/takeover.rs | 78 +++- .../runtime/upgrade/takeover/mysql_journal.rs | 32 +- rust/src/runtime/upgrade_tests.rs | 135 +++++++ rust/upgrade/php-2.11.3-from-2.9.7/README.md | 21 + .../php-2.11.3-from-2.9.7/mariadb.json | 42 ++ rust/upgrade/php-2.11.3-from-2.9.7/mysql.json | 42 ++ .../php-2.11.3-from-2.9.7/postgres.json | 382 ++++++++++++++++++ .../upgrade/php-2.11.3-from-2.9.7/sqlite.json | 90 +++++ rust/upgrade/php-2.11.3/README.md | 48 +++ rust/upgrade/php-2.11.3/mariadb.json | 42 ++ rust/upgrade/php-2.11.3/mysql.json | 42 ++ rust/upgrade/php-2.11.3/postgres.json | 382 ++++++++++++++++++ rust/upgrade/php-2.11.3/sqlite.json | 90 +++++ 22 files changed, 1766 insertions(+), 49 deletions(-) create mode 100644 docs/rust-server-port-checkpoints/2026-10-26-php-2113-takeover.md create mode 100644 rust/src/runtime/upgrade/dispatch_scan.rs create mode 100644 rust/upgrade/php-2.11.3-from-2.9.7/README.md create mode 100644 rust/upgrade/php-2.11.3-from-2.9.7/mariadb.json create mode 100644 rust/upgrade/php-2.11.3-from-2.9.7/mysql.json create mode 100644 rust/upgrade/php-2.11.3-from-2.9.7/postgres.json create mode 100644 rust/upgrade/php-2.11.3-from-2.9.7/sqlite.json create mode 100644 rust/upgrade/php-2.11.3/README.md create mode 100644 rust/upgrade/php-2.11.3/mariadb.json create mode 100644 rust/upgrade/php-2.11.3/mysql.json create mode 100644 rust/upgrade/php-2.11.3/postgres.json create mode 100644 rust/upgrade/php-2.11.3/sqlite.json diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 79ce72269..281b88f36 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -867,6 +867,22 @@ jobs: DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.1 DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:bf8bed13f5f809ee3425233180a581353eac4683adbee5c4ecbf63bb18bbd102 run: bash scripts/conformance/server-parity/upgrade-source-cleanup.sh mysql + - name: Back up, take over and resume a published PHP 2.11.3 upgrade source + if: ${{ matrix.part == 'cohorts' }} + timeout-minutes: 8 + env: + DW_PARITY_UPGRADE_LABEL: php2113 + DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.3 + DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e + run: bash scripts/conformance/server-parity/upgrade-source-profile.sh mysql + - name: Retire PHP 2.11.3 upgrade source cohort + if: ${{ always() && matrix.part == 'cohorts' }} + timeout-minutes: 3 + env: + DW_PARITY_UPGRADE_LABEL: php2113 + DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.3 + DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e + run: bash scripts/conformance/server-parity/upgrade-source-cleanup.sh mysql - name: Start the native node the corrected marker cohort uses id: prepare_corrected if: ${{ matrix.part == 'corrected-markers' }} @@ -1517,6 +1533,22 @@ jobs: DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.1 DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:bf8bed13f5f809ee3425233180a581353eac4683adbee5c4ecbf63bb18bbd102 run: bash scripts/conformance/server-parity/upgrade-source-cleanup.sh pgsql + - name: Back up, take over and resume a published PHP 2.11.3 upgrade source + if: ${{ matrix.part == 'cohorts' }} + timeout-minutes: 8 + env: + DW_PARITY_UPGRADE_LABEL: php2113 + DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.3 + DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e + run: bash scripts/conformance/server-parity/upgrade-source-profile.sh pgsql + - name: Retire PHP 2.11.3 upgrade source cohort + if: ${{ always() && matrix.part == 'cohorts' }} + timeout-minutes: 3 + env: + DW_PARITY_UPGRADE_LABEL: php2113 + DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.3 + DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e + run: bash scripts/conformance/server-parity/upgrade-source-cleanup.sh pgsql - name: Start the native node the corrected marker cohort uses id: prepare_corrected if: ${{ matrix.part == 'corrected-markers' }} @@ -2182,6 +2214,24 @@ jobs: DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.1 DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:bf8bed13f5f809ee3425233180a581353eac4683adbee5c4ecbf63bb18bbd102 run: bash scripts/conformance/server-parity/upgrade-source-cleanup.sh sqlite + - name: Back up, take over and resume a published PHP 2.11.3 upgrade source + if: ${{ matrix.part == 'cohorts' }} + timeout-minutes: 8 + env: + RESOURCE_SCOPE: dw-parity-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + DW_PARITY_UPGRADE_LABEL: php2113 + DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.3 + DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e + run: bash scripts/conformance/server-parity/upgrade-source-profile.sh sqlite + - name: Retire PHP 2.11.3 upgrade source cohort + if: ${{ always() && matrix.part == 'cohorts' }} + timeout-minutes: 3 + env: + RESOURCE_SCOPE: dw-parity-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + DW_PARITY_UPGRADE_LABEL: php2113 + DW_PARITY_UPGRADE_EXPECTED_LAYOUT: php-2.11.3 + DW_PARITY_UPGRADE_PHP_IMAGE: ghcr.io/durable-workflow/server@sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e + run: bash scripts/conformance/server-parity/upgrade-source-cleanup.sh sqlite - name: Start the native node the corrected marker cohort uses id: prepare_corrected if: ${{ matrix.part == 'corrected-markers' }} diff --git a/docs/rust-server-port-checkpoints/2026-10-26-php-2113-takeover.md b/docs/rust-server-port-checkpoints/2026-10-26-php-2113-takeover.md new file mode 100644 index 000000000..f842744c2 --- /dev/null +++ b/docs/rust-server-port-checkpoints/2026-10-26-php-2113-takeover.md @@ -0,0 +1,60 @@ +# October 26, takeover of published Server 2.11.3 + +- **Before.** Native refused every published 2.11.3 database as + `upgrade_source_catalog_unqualified`. Server 2.11.3 (Core 2.14.2) was + published today, image index + `sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e`. +- **Layouts name their parent.** `SOURCE_LAYOUTS` was a chain: each delta + applied to the layout before it. 2.11.3 has to apply to both 2.11.1 + variants, so each layout now names the layout its delta applies to. + `layout_lineage` gives the path from the frozen layout, and + `every_recorded_layout_descends_from_an_earlier_one` checks the order. + - `php-2.11.3` descends from fresh `php-2.11.1`. + - `php-2.11.3-from-2.9.7` descends from `php-2.11.1-from-2.9.7`. +- **Captured from real databases.** The published images bootstrapped and + migrated SQLite, PostgreSQL 17, MySQL 8.0 and MariaDB 10.11 databases along + five paths: + - fresh 2.11.3; + - 2.11.1 then 2.11.3; + - 2.8.0 then 2.11.3; + - 2.9.7 then 2.11.3; + - 2.9.7, 2.11.1, then 2.11.3. + + Each path gave the same delta from its parent on every backend. The first + three end at `php-2.11.3` and the last two at `php-2.11.3-from-2.9.7`. The + native binary then inspected, fingerprinted and took over all five real + SQLite databases. +- **Recipe** (details in [the layout README](../../rust/upgrade/php-2.11.3/README.md)). + - Each active session's recorded holder incarnation is copied to + `dw_worker_session_holders`. Earlier releases record none, so takeover + derives the holder from the current registration. A 2.11.3 session that + PHP already orphaned stays orphaned. + - `message_full` is counted (`full_failure_messages_removed`) and dropped. + - The idempotency digest and its index are dropped. + - Completed dispatches are counted (`service_call_dispatches_removed`) and + the outbox is dropped. A scheduled or claimed dispatch refuses takeover + before any change (`upgrade_source_service_call_dispatches_pending`), and + inspection reports `service_call_dispatch_inspection`. + - `reclaim_at` and its index are dropped. + - MySQL and MariaDB run this as journal step 10. Each part checks what is + left to do, so a resumed step repeats safely. +- **Tests.** + - SQLite: `takeover_converts_php_2113_databases` covers both layouts, with + a held session, a replaced holder, a full failure message, a completed + dispatch and an idempotency digest. + `takeover_refuses_a_php_2113_dispatch_still_pending` checks that a + pending dispatch refuses and leaves the file byte-identical. + - PostgreSQL, MySQL and MariaDB: + `qualification_takeover_converts_php_2113_and_native_opens_it`. + - MySQL and MariaDB: the step-kill test also kills a 2.11.3 takeover + after step 9 and resumes it. + - CI: a `php2113` upgrade cohort backs up, takes over and resumes a + published 2.11.3 source on SQLite, PostgreSQL and MySQL. +- **Not yet native.** The parity reference is still 2.11.1. Core 2.14 also + changed behavior that native does not follow yet: + - full failure-message storage on write; + - service-call scheduling through the outbox; + - the idempotency scope of target namespace, names and key, with + `service_call_idempotency_scope_conflict`. + + These belong to the parity move to 2.11.3. diff --git a/docs/rust-server-port.md b/docs/rust-server-port.md index e51ed7834..5c83a58a6 100644 --- a/docs/rust-server-port.md +++ b/docs/rust-server-port.md @@ -489,6 +489,16 @@ contract fixture before its correction. drops the three columns; the verified backup keeps them ([#325 decision](https://github.com/durable-workflow/server/issues/325#issuecomment-6306545520), [checkpoint](rust-server-port-checkpoints/2026-10-26-php-2111-takeover.md)). +- **Server 2.11.3 (Core 2.14.2) takeover.** Takeover recognizes both + published 2.11.3 layouts, fresh and created by 2.9.x. It copies each active + session's recorded holder, drops full failure messages (counted; native + reads the labelled preview, as PHP's older readers do), the service-call + idempotency digest, completed dispatches and the stream reclaim deadline, + and refuses while a dispatch is pending + ([checkpoint](rust-server-port-checkpoints/2026-10-26-php-2113-takeover.md)). + The parity reference is still 2.11.1. Core 2.14's new behavior (full + failure-message storage, service-call scheduling and idempotency scope) + is native work for the move to 2.11.3. - **Stop signal in the published image.** The published Server image declares `STOPSIGNAL SIGWINCH`, Apache's graceful stop. Native stops gracefully on `SIGTERM` or `SIGINT` and ignores `SIGWINCH`, so `docker stop` on the diff --git a/docs/rust-server-upgrade-takeover.md b/docs/rust-server-upgrade-takeover.md index 15dbfed31..7439f085a 100644 --- a/docs/rust-server-upgrade-takeover.md +++ b/docs/rust-server-upgrade-takeover.md @@ -77,6 +77,16 @@ inspection is `unqualified` and takeover refuses with `upgrade_source_external_payload_storage_unsupported` before any change. A disabled policy does not block takeover. +PHP 2.11.3 (Core 2.14) schedules a service call's handler in the +`workflow_service_call_dispatches` outbox and dispatches it in a later pass. +Native runs the handler when it accepts the call, so it cannot resume a +dispatch that is still scheduled or claimed. Every inspection reports +`service_call_dispatch_inspection`: the number of dispatches and of pending +ones, without naming calls. A pending dispatch makes the inspection +`unqualified`, and takeover refuses with +`upgrade_source_service_call_dispatches_pending` before any change. Let PHP +finish them before the stop. Completed dispatches do not block takeover. + ## Exclusive transaction per backend The marker, the fingerprint check and the schema changes must not interleave diff --git a/rust/src/runtime/mysql.rs b/rust/src/runtime/mysql.rs index 161dd0ad6..80c0669a4 100644 --- a/rust/src/runtime/mysql.rs +++ b/rust/src/runtime/mysql.rs @@ -328,20 +328,20 @@ async fn source_layout( canonical(include_str!("../../upgrade/php-2.9.7/mysql.json")), canonical(include_str!("../../upgrade/php-2.11.1/mysql.json")), canonical(include_str!("../../upgrade/php-2.11.1-from-2.9.7/mysql.json")), + canonical(include_str!("../../upgrade/php-2.11.3/mysql.json")), + canonical(include_str!("../../upgrade/php-2.11.3-from-2.9.7/mysql.json")), ], Flavor::MariaDb => [ canonical(include_str!("../../upgrade/php-2.8.0/mariadb.json")), canonical(include_str!("../../upgrade/php-2.9.7/mariadb.json")), canonical(include_str!("../../upgrade/php-2.11.1/mariadb.json")), canonical(include_str!("../../upgrade/php-2.11.1-from-2.9.7/mariadb.json")), + canonical(include_str!("../../upgrade/php-2.11.3/mariadb.json")), + canonical(include_str!("../../upgrade/php-2.11.3-from-2.9.7/mariadb.json")), ], }; - let layout = super::upgrade::source_layout( - &actual, - expected.clone(), - [&deltas[0], &deltas[1], &deltas[2], &deltas[3]], - |row| row.0.clone(), - ); + let deltas = deltas.each_ref().map(String::as_str); + let layout = super::upgrade::source_layout(&actual, expected.clone(), deltas, |row| row.0.clone()); // Laravel's separate mariadb connection stores the backup hold owner as // uuid rather than char(36); every release layout is otherwise the same. let layout = match (layout, flavor) { @@ -350,17 +350,14 @@ async fn source_layout( &expected, &canonical(include_str!("../../upgrade/mariadb-driver.json")), )?; - match super::upgrade::source_layout( - &actual, - driver, - [&deltas[0], &deltas[1], &deltas[2], &deltas[3]], - |row| row.0.clone(), - )? { + match super::upgrade::source_layout(&actual, driver, deltas, |row| row.0.clone())? { "frozen" => "frozen+mariadb-driver", "php-2.8.0" => "php-2.8.0+mariadb-driver", "php-2.9.7" => "php-2.9.7+mariadb-driver", "php-2.11.1" => "php-2.11.1+mariadb-driver", "php-2.11.1-from-2.9.7" => "php-2.11.1-from-2.9.7+mariadb-driver", + "php-2.11.3" => "php-2.11.3+mariadb-driver", + "php-2.11.3-from-2.9.7" => "php-2.11.3-from-2.9.7+mariadb-driver", _ => { return Err(refuse( StatusCode::CONFLICT, @@ -1246,6 +1243,8 @@ mod tests { include_str!("../../upgrade/php-2.9.7/mysql.json"), include_str!("../../upgrade/php-2.11.1/mysql.json"), include_str!("../../upgrade/php-2.11.1-from-2.9.7/mysql.json"), + include_str!("../../upgrade/php-2.11.3/mysql.json"), + include_str!("../../upgrade/php-2.11.3-from-2.9.7/mysql.json"), ], ), Flavor::MariaDb => ( @@ -1255,6 +1254,8 @@ mod tests { include_str!("../../upgrade/php-2.9.7/mariadb.json"), include_str!("../../upgrade/php-2.11.1/mariadb.json"), include_str!("../../upgrade/php-2.11.1-from-2.9.7/mariadb.json"), + include_str!("../../upgrade/php-2.11.3/mariadb.json"), + include_str!("../../upgrade/php-2.11.3-from-2.9.7/mariadb.json"), ], ), }; @@ -1264,17 +1265,12 @@ mod tests { .execute("SET FOREIGN_KEY_CHECKS=1; ALTER TABLE workflow_tasks DROP KEY dw_workflow_tasks_poll") .await .unwrap(); - let releases = match release { - None => 0, - Some("php-2.8.0") => 1, - Some("php-2.9.7") => 2, - Some("php-2.11.1") => 3, - Some(_) => 4, - }; + let lineage = super::super::upgrade::layout_lineage(release.unwrap_or("frozen")); + assert_eq!(lineage.is_empty(), release.is_none(), "{release:?} is a recorded layout"); // A replaced table can have inbound foreign keys (2.11.1's runs); they // name the table, so they hold again once it is recreated. connection.execute("SET FOREIGN_KEY_CHECKS=0").await.unwrap(); - for delta in &deltas[..releases] { + for delta in lineage.into_iter().map(|index| deltas[index]) { let delta: Value = serde_json::from_str(delta).unwrap(); for added in delta["add"].as_array().unwrap() { let (table, ddl) = (added[0].as_str().unwrap(), added[1].as_str().unwrap()); @@ -1432,6 +1428,48 @@ mod tests { } } + #[tokio::test] + #[ignore = "requires an isolated MySQL/MariaDB create-database role"] + async fn qualification_takeover_converts_php_2113_and_native_opens_it() { + for release in ["php-2.11.3", "php-2.11.3-from-2.9.7"] { + let db = Database::new().await; + php_layout(&db, Some(release)).await; + registrations(&db).await; + let recorded = super::super::worker_sessions::incarnation_digest("0123456789abcdef0123456789abcdef"); + let mut connection = db.connection().await; + for statement in [ + format!("INSERT INTO workflow_worker_sessions(namespace,session_id,queue,status,lease_owner,lease_owner_incarnation,lease_expires_at,ttl_expires_at,created_at,updated_at) VALUES ('default','held','q','active','current','{recorded}','2030-01-01 00:00:00','2030-01-01 00:00:00','2026-10-20 09:00:00','2026-10-20 09:00:00')"), + "INSERT INTO workflow_failures(id,workflow_run_id,source_kind,source_id,propagation_kind,exception_class,message,file,message_full,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kfail','01jrun0000000000000000000a','activity_execution','a','activity','RuntimeException','preview','','base64:ZnVsbA==','2026-10-26 10:00:00','2026-10-26 10:00:00')".to_owned(), + "INSERT INTO workflow_service_calls(id,namespace,endpoint_name,service_name,operation_name,caller_namespace,target_namespace,status,outcome,operation_mode,idempotency_key,idempotency_scope_hash,accepted_at,completed_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','default','pay','billing','charge','default','default','completed','succeeded','async','order-1','0000000000000000000000000000000000000000000000000000000000000001','2026-10-26 10:00:00','2026-10-26 10:00:01','2026-10-26 10:00:00','2026-10-26 10:00:01')".to_owned(), + "INSERT INTO workflow_service_call_dispatches(service_call_id,state,dispatch_options,available_at,completed_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','completed','{}','2026-10-26 10:00:00','2026-10-26 10:00:01','2026-10-26 10:00:00','2026-10-26 10:00:01')".to_owned(), + ] { + connection.execute(sqlx::AssertSqlSafe(statement)).await.unwrap(); + } + connection.close().await.unwrap(); + let fingerprint = backup_fingerprint(&db).await; + let report = takeover::take_over_mysql(db.options.clone(), &fingerprint, &TAKEOVER_QUEUES) + .await + .unwrap(); + assert_eq!(report["status"], "taken_over", "{report}"); + assert_eq!(report["source_layout"], release); + assert_eq!(report["converted"]["session_holders_recorded"], 1, "{report}"); + assert_eq!(report["converted"]["full_failure_messages_removed"], 1, "{report}"); + assert_eq!(report["converted"]["service_call_dispatches_removed"], 1, "{report}"); + converted(&db).await; + let mut connection = db.connection().await; + let holder: String = sqlx::query_scalar("SELECT incarnation FROM dw_worker_session_holders WHERE namespace='default' AND session_id='held'") + .fetch_one(&mut connection).await.unwrap(); + assert_eq!(holder, recorded); + let left: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND (TABLE_NAME,COLUMN_NAME) IN (('workflow_failures','message_full'),('workflow_service_calls','idempotency_scope_hash'),('workflow_worker_sessions','lease_owner_incarnation'),('workflow_durable_streams','reclaim_at'))") + .fetch_one(&mut connection).await.unwrap(); + let dispatches: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='workflow_service_call_dispatches'") + .fetch_one(&mut connection).await.unwrap(); + assert_eq!((left, dispatches), (0, 0), "{release}: PHP 2.11.3's additions are gone"); + connection.close().await.unwrap(); + db.remove().await; + } + } + #[tokio::test] #[ignore = "requires an isolated MySQL/MariaDB create-database role"] async fn qualification_takeover_converts_php_297_and_native_opens_it() { @@ -1596,14 +1634,16 @@ mod tests { #[ignore = "requires an isolated MySQL/MariaDB create-database role"] async fn qualification_takeover_killed_at_each_step_resumes_with_the_same_fingerprint() { use std::os::unix::process::ExitStatusExt; - for boundary in [ - "before-marker-row", - "after-step-1", - "after-step-3", - "after-step-8", + for (release, boundary) in [ + ("php-2.9.7", "before-marker-row"), + ("php-2.9.7", "after-step-1"), + ("php-2.9.7", "after-step-3"), + ("php-2.9.7", "after-step-8"), + // Killed before PHP 2.11.3's own step. + ("php-2.11.3", "after-step-9"), ] { let db = Database::new().await; - php_layout(&db, Some("php-2.9.7")).await; + php_layout(&db, Some(release)).await; registrations(&db).await; let fingerprint = backup_fingerprint(&db).await; let directory = tempfile::tempdir().unwrap(); @@ -1656,7 +1696,7 @@ mod tests { .await .unwrap(); assert_eq!(report["status"], "taken_over", "{boundary}: {report}"); - assert_eq!(report["source_layout"], "php-2.9.7", "{boundary}"); + assert_eq!(report["source_layout"], release, "{boundary}"); converted(&db).await; db.remove().await; } diff --git a/rust/src/runtime/postgres.rs b/rust/src/runtime/postgres.rs index 6fe0ae14a..e51964c4e 100644 --- a/rust/src/runtime/postgres.rs +++ b/rust/src/runtime/postgres.rs @@ -119,6 +119,8 @@ pub(super) async fn php_source_catalog(connection: &mut PgConnection) -> Result< include_str!("../../upgrade/php-2.9.7/postgres.json"), include_str!("../../upgrade/php-2.11.1/postgres.json"), include_str!("../../upgrade/php-2.11.1-from-2.9.7/postgres.json"), + include_str!("../../upgrade/php-2.11.3/postgres.json"), + include_str!("../../upgrade/php-2.11.3-from-2.9.7/postgres.json"), ], |row| (row.0.clone(), row.1.clone()), )?; @@ -846,6 +848,17 @@ mod tests { const TRACE_CARRIERS: &str = "ALTER TABLE workflow_runs ADD COLUMN observability_context json; ALTER TABLE workflow_commands ADD COLUMN observability_context json; ALTER TABLE workflow_tasks ADD COLUMN observability_context json;"; + /// PHP 2.11.3's changes (Core 2.14.2 and Server), in its migrations' order. + const RELEASE_2113: &str = "ALTER TABLE workflow_failures ADD COLUMN message_full text; + ALTER TABLE workflow_service_calls ADD COLUMN idempotency_scope_hash character(64); + CREATE UNIQUE INDEX wf_service_calls_idem_scope_unique ON workflow_service_calls (idempotency_scope_hash); + CREATE TABLE workflow_service_call_dispatches (service_call_id varchar(26) NOT NULL, state varchar(32) NOT NULL, dispatch_options json NOT NULL, attempt integer NOT NULL DEFAULT '1', max_attempts integer NOT NULL DEFAULT '1', available_at timestamp(6) without time zone NOT NULL, claim_generation bigint NOT NULL DEFAULT '0', claim_token character(64), claimed_at timestamp(6) without time zone, claim_expires_at timestamp(6) without time zone, authorized_catalog_revision character(64), authorized_policy_revision character(64), dispatch_attempted_at timestamp(6) without time zone, repair_available_at timestamp(6) without time zone, completed_at timestamp(6) without time zone, last_error text, created_at timestamp(6) without time zone, updated_at timestamp(6) without time zone, PRIMARY KEY (service_call_id)); + CREATE INDEX wf_service_dispatch_due_idx ON workflow_service_call_dispatches (state, available_at); + CREATE INDEX wf_service_dispatch_claim_idx ON workflow_service_call_dispatches (state, claim_expires_at); + CREATE INDEX wf_service_dispatch_repair_idx ON workflow_service_call_dispatches (state, repair_available_at); + ALTER TABLE workflow_worker_sessions ADD COLUMN lease_owner_incarnation character(64); + ALTER TABLE workflow_durable_streams ADD COLUMN reclaim_at timestamp(6) without time zone; + CREATE INDEX wf_durable_streams_ns_reclaim_idx ON workflow_durable_streams (namespace, reclaim_at, id);"; /// The frozen PHP layout with a release's changes applied in order. async fn php_release(db: &Database, changes: &[&'static str]) -> PgConnection { @@ -916,6 +929,53 @@ mod tests { } } + #[tokio::test] + #[ignore = "requires a disposable PostgreSQL; run the qualification_ tests explicitly"] + async fn qualification_takeover_converts_php_2113_and_native_opens_it() { + for (release, changes) in [ + ("php-2.11.3", [PHP_280_CHANGES, TRACE_CARRIERS, QUEUE_DELIVERY, RELEASE_2113]), + ("php-2.11.3-from-2.9.7", [PHP_280_CHANGES, QUEUE_DELIVERY, TRACE_CARRIERS, RELEASE_2113]), + ] { + let db = Database::new().await; + let mut connection = php_release(&db, &changes).await; + let recorded = super::super::worker_sessions::incarnation_digest("0123456789abcdef0123456789abcdef"); + for statement in [ + "INSERT INTO workflow_worker_registrations(worker_id,namespace,task_queue,runtime,registration_token,created_at,updated_at) VALUES ('current','default','q','php','0123456789abcdef0123456789abcdef','2026-10-20 09:00:00','2026-10-20 09:00:00')".to_owned(), + "INSERT INTO workflow_worker_registration_incarnations VALUES ('0123456789abcdef0123456789abcdef','default','current','active',0,NULL,'2026-10-20 09:00:00','2026-10-20 09:00:00')".to_owned(), + format!("INSERT INTO workflow_worker_sessions(namespace,session_id,queue,status,lease_owner,lease_owner_incarnation,lease_expires_at,ttl_expires_at,created_at,updated_at) VALUES ('default','held','q','active','current','{recorded}','2030-01-01 00:00:00','2030-01-01 00:00:00','2026-10-20 09:00:00','2026-10-20 09:00:00')"), + "INSERT INTO workflow_failures(id,workflow_run_id,source_kind,source_id,propagation_kind,exception_class,message,file,message_full,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kfail','01jrun0000000000000000000a','activity_execution','a','activity','RuntimeException','preview','','base64:ZnVsbA==','2026-10-26 10:00:00','2026-10-26 10:00:00')".to_owned(), + "INSERT INTO workflow_service_calls(id,namespace,endpoint_name,service_name,operation_name,caller_namespace,target_namespace,status,outcome,operation_mode,idempotency_key,idempotency_scope_hash,accepted_at,completed_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','default','pay','billing','charge','default','default','completed','succeeded','async','order-1','0000000000000000000000000000000000000000000000000000000000000001','2026-10-26 10:00:00','2026-10-26 10:00:01','2026-10-26 10:00:00','2026-10-26 10:00:01')".to_owned(), + "INSERT INTO workflow_service_call_dispatches(service_call_id,state,dispatch_options,available_at,completed_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','completed','{}','2026-10-26 10:00:00','2026-10-26 10:00:01','2026-10-26 10:00:00','2026-10-26 10:00:01')".to_owned(), + ] { + sqlx::raw_sql(sqlx::AssertSqlSafe(statement)).execute(&mut connection).await.unwrap(); + } + connection.close().await.unwrap(); + let fingerprint = backup_fingerprint(&db).await; + let report = takeover::take_over_postgres(db.options.clone(), &fingerprint, &TAKEOVER_QUEUES) + .await + .unwrap(); + assert_eq!(report["status"], "taken_over", "{report}"); + assert_eq!(report["source_layout"], release); + assert_eq!(report["converted"]["session_holders_recorded"], 1, "{report}"); + assert_eq!(report["converted"]["full_failure_messages_removed"], 1, "{report}"); + assert_eq!(report["converted"]["service_call_dispatches_removed"], 1, "{report}"); + let mut connection = db.connection().await; + let holder: String = sqlx::query_scalar("SELECT incarnation FROM dw_worker_session_holders WHERE namespace='default' AND session_id='held'") + .fetch_one(&mut connection).await.unwrap(); + assert_eq!(holder, recorded); + let left: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='public' AND (table_name,column_name) IN (('workflow_failures','message_full'),('workflow_service_calls','idempotency_scope_hash'),('workflow_worker_sessions','lease_owner_incarnation'),('workflow_durable_streams','reclaim_at'))") + .fetch_one(&mut connection).await.unwrap(); + let dispatches: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='workflow_service_call_dispatches'") + .fetch_one(&mut connection).await.unwrap(); + assert_eq!((left, dispatches), (0, 0), "{release}: PHP 2.11.3's additions are gone"); + connection.close().await.unwrap(); + let storage = PostgresStorage::open(db.options.clone()).await.unwrap(); + storage.close().await; + PostgresStorage::check(db.options.clone()).await.unwrap(); + db.remove().await; + } + } + async fn backup_fingerprint(db: &Database) -> String { fingerprint::fingerprint_postgres(db.options.clone()) .await diff --git a/rust/src/runtime/schema.rs b/rust/src/runtime/schema.rs index 761260808..c94bd0358 100644 --- a/rust/src/runtime/schema.rs +++ b/rust/src/runtime/schema.rs @@ -124,6 +124,8 @@ pub(super) async fn php_source_catalog(connection: &mut SqliteConnection) -> Res include_str!("../../upgrade/php-2.9.7/sqlite.json"), include_str!("../../upgrade/php-2.11.1/sqlite.json"), include_str!("../../upgrade/php-2.11.1-from-2.9.7/sqlite.json"), + include_str!("../../upgrade/php-2.11.3/sqlite.json"), + include_str!("../../upgrade/php-2.11.3-from-2.9.7/sqlite.json"), ], |row| (row.0.clone(), row.1.clone()), )?; diff --git a/rust/src/runtime/upgrade.rs b/rust/src/runtime/upgrade.rs index 6ad8589c7..cbd0ffa98 100644 --- a/rust/src/runtime/upgrade.rs +++ b/rust/src/runtime/upgrade.rs @@ -2,6 +2,7 @@ pub mod external_reference; pub mod fingerprint; pub mod takeover; +mod dispatch_scan; mod payload_scan; mod schedule_scan; mod storage_scan; @@ -24,17 +25,54 @@ use tokio::time::{Instant, timeout, timeout_at}; const BUDGET: Duration = Duration::from_secs(30); -/// Published PHP layouts after the frozen one, oldest first. Each backend -/// records one delta per layout, applied to the layout before it. -pub(super) const SOURCE_LAYOUTS: [&str; 4] = ["php-2.8.0", "php-2.9.7", "php-2.11.1", "php-2.11.1-from-2.9.7"]; +/// Published PHP layouts after the frozen one, each with the layout its +/// recorded delta applies to. A parent comes before its children. Each backend +/// records one delta per layout, in this order. +/// +/// A release can have several layouts: core migrations run in file order, so +/// a database an older release created can order columns differently from a +/// fresh one. Each such variant descends from the matching older variant. +pub(super) const SOURCE_LAYOUTS: [(&str, &str); 6] = [ + ("php-2.8.0", "frozen"), + ("php-2.9.7", "php-2.8.0"), + ("php-2.11.1", "php-2.9.7"), + ("php-2.11.1-from-2.9.7", "php-2.11.1"), + ("php-2.11.3", "php-2.11.1"), + ("php-2.11.3-from-2.9.7", "php-2.11.1-from-2.9.7"), +]; + +/// The number of recorded layouts after the frozen one. +pub(super) const LAYOUTS: usize = SOURCE_LAYOUTS.len(); + +/// The recorded layouts from the oldest ancestor down to `name`, as indexes +/// into `SOURCE_LAYOUTS`; empty for the frozen layout or an unknown name. +pub(super) fn layout_lineage(name: &str) -> Vec { + let mut lineage = Vec::new(); + let mut current = name; + while let Some(index) = SOURCE_LAYOUTS.iter().position(|(layout, _)| *layout == current) { + lineage.insert(0, index); + current = SOURCE_LAYOUTS[index].1; + } + lineage +} + +/// Whether `layout` (a recorded layout, with or without the MariaDB driver +/// suffix) includes `release`'s schema changes: its lineage passes through +/// that release or one of its variants. +pub(super) fn has_release(layout: &str, release: &str) -> bool { + layout_lineage(layout.trim_end_matches("+mariadb-driver")).into_iter().any(|index| { + let name = SOURCE_LAYOUTS[index].0; + name == release || name.strip_prefix(release).is_some_and(|rest| rest.starts_with("-from-")) + }) +} /// Names the qualified layout `actual` matches exactly: the frozen layout, or -/// one reached by applying the recorded deltas in order. Rows are compared in +/// one reached by applying its lineage's recorded deltas. Rows are compared in /// `order`, so only presentation order, never a definition, is normalized. pub(super) fn source_layout( actual: &[T], frozen: Vec, - deltas: [&str; 4], + deltas: [&str; LAYOUTS], order: impl Fn(&T) -> K, ) -> Result<&'static str> where @@ -46,15 +84,20 @@ where rows }; let actual = sorted(actual); - let mut layout = sorted(&frozen); - if actual == layout { + let mut layouts = vec![sorted(&frozen)]; + if actual == layouts[0] { return Ok("frozen"); } - for (name, delta) in SOURCE_LAYOUTS.into_iter().zip(deltas) { - layout = sorted(&source_catalog_variant(&layout, delta)?); + for ((name, parent), delta) in SOURCE_LAYOUTS.into_iter().zip(deltas) { + let base = match SOURCE_LAYOUTS.iter().position(|(layout, _)| *layout == parent) { + Some(index) => &layouts[index + 1], + None => &layouts[0], + }; + let layout = sorted(&source_catalog_variant(base, delta)?); if actual == layout { return Ok(name); } + layouts.push(layout); } Err(refuse( StatusCode::CONFLICT, @@ -211,6 +254,7 @@ fn report( payload_inspection: Option, schedule_inspection: Value, storage_inspection: Value, + dispatch_inspection: Value, ) -> Result { let digest = Sha256::digest(serde_json::to_vec(catalog)?) .iter() @@ -243,6 +287,10 @@ fn report( report["status"] = json!("unqualified"); } report["storage_inspection"] = storage_inspection; + if dispatch_inspection["pending"] != 0 { + report["status"] = json!("unqualified"); + } + report["service_call_dispatch_inspection"] = dispatch_inspection; Ok(report) } @@ -417,6 +465,7 @@ pub(super) async fn sqlite_inventory( }; let schedule_inspection = schedule_scan::inspect::(&mut *tx).await?; let storage_inspection = storage_scan::inspect::(&mut *tx).await?; + let dispatch_inspection = dispatch_scan::inspect::(&mut *tx, &counts).await?; report( "sqlite", &catalog, @@ -426,6 +475,7 @@ pub(super) async fn sqlite_inventory( payload_inspection, schedule_inspection, storage_inspection, + dispatch_inspection, ) } @@ -690,6 +740,7 @@ pub(super) async fn mysql_inventory( }; let schedule_inspection = schedule_scan::inspect::(&mut *tx).await?; let storage_inspection = storage_scan::inspect::(&mut *tx).await?; + let dispatch_inspection = dispatch_scan::inspect::(&mut *tx, &counts).await?; // MySQL's metadata is not an MVCC snapshot. Refuse a changed visible // catalog rather than joining counts to a different schema. if mysql_source_catalog(&mut *tx, fenced).await?.0 != catalog { @@ -707,6 +758,7 @@ pub(super) async fn mysql_inventory( payload_inspection, schedule_inspection, storage_inspection, + dispatch_inspection, ) } @@ -775,6 +827,7 @@ pub(super) async fn postgres_inventory( }; let schedule_inspection = schedule_scan::inspect::(&mut *tx).await?; let storage_inspection = storage_scan::inspect::(&mut *tx).await?; + let dispatch_inspection = dispatch_scan::inspect::(&mut *tx, &counts).await?; report( "pgsql", &catalog, @@ -784,6 +837,7 @@ pub(super) async fn postgres_inventory( payload_inspection, schedule_inspection, storage_inspection, + dispatch_inspection, ) } diff --git a/rust/src/runtime/upgrade/dispatch_scan.rs b/rust/src/runtime/upgrade/dispatch_scan.rs new file mode 100644 index 000000000..666c8c03b --- /dev/null +++ b/rust/src/runtime/upgrade/dispatch_scan.rs @@ -0,0 +1,31 @@ +//! PHP 2.11.3's service-call dispatch outbox. PHP schedules a call's handler +//! there and a later pass dispatches it; native runs the handler when it +//! accepts the call, so it cannot resume a dispatch that is still scheduled or +//! claimed. A completed dispatch is only PHP's bookkeeping. The report counts +//! rows and never names calls. +use crate::runtime::{Result, backend::Backend}; +use serde_json::{Value, json}; +use sqlx::{Executor, IntoArguments}; +use std::collections::BTreeMap; + +const PENDING: &str = "SELECT COUNT(*) AS n FROM workflow_service_call_dispatches WHERE state<>'completed'"; + +pub(super) async fn inspect( + connection: &mut DB::Connection, + counts: &BTreeMap, +) -> Result +where + for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>, + DB::Arguments: IntoArguments, +{ + let rows = counts.get("workflow_service_call_dispatches").copied().unwrap_or(0); + let pending = if rows == 0 { + 0 + } else { + let row = sqlx::query(sqlx::AssertSqlSafe(DB::statement(PENDING))) + .fetch_one(&mut *connection) + .await?; + DB::number(&row, "n")? + }; + Ok(json!({"dispatches": rows, "pending": pending})) +} diff --git a/rust/src/runtime/upgrade/takeover.rs b/rust/src/runtime/upgrade/takeover.rs index c7a6155c1..2bbb93ce1 100644 --- a/rust/src/runtime/upgrade/takeover.rs +++ b/rust/src/runtime/upgrade/takeover.rs @@ -254,6 +254,14 @@ fn qualified(inventory: &Value) -> Result<&str> { "upgrade_source_external_payload_storage_unsupported", )); } + // A service call PHP 2.11.3 scheduled but has not dispatched would never + // run under native. + if inventory["service_call_dispatch_inspection"]["pending"] != 0 { + return Err(refuse( + StatusCode::CONFLICT, + "upgrade_source_service_call_dispatches_pending", + )); + } // A schedule native cannot run would stop firing after takeover. if inventory["schedule_inspection"]["unsupported"] != 0 { return Err(refuse( @@ -334,7 +342,7 @@ where fn php_incarnations(layout: &str) -> Result { match layout.trim_end_matches("+mariadb-driver") { "frozen" => Ok(false), - "php-2.8.0" | "php-2.9.7" | "php-2.11.1" | "php-2.11.1-from-2.9.7" => Ok(true), + layout if !super::layout_lineage(layout).is_empty() => Ok(true), _ => Err(refuse( StatusCode::CONFLICT, "upgrade_source_layout_unconverted", @@ -356,7 +364,7 @@ where { let php_incarnations = php_incarnations(layout)?; let (copied, created) = adopt_incarnations::(tx, php_incarnations).await?; - let holders = adopt_session_holders::(tx).await?; + let holders = adopt_session_holders::(tx, super::has_release(layout, "php-2.11.3")).await?; // The frozen layout has no call deadlines; every later one does. let deadlines = if php_incarnations { DB::affected(query::(SERVICE_CALL_DEADLINES).execute(&mut *tx).await?) @@ -373,7 +381,7 @@ where .execute(&mut *tx) .await?; } - let delivery_references = if matches!(layout, "php-2.9.7" | "php-2.11.1" | "php-2.11.1-from-2.9.7") { + let delivery_references = if super::has_release(layout, "php-2.9.7") { // PHP's own queue delivery lookup metadata; native does not use PHP's // queue, and the queue inspection bound every job to its task row. let references = count::(tx, DELIVERY_REFERENCES).await?; @@ -384,7 +392,7 @@ where } else { 0 }; - let observability_contexts = if matches!(layout, "php-2.11.1" | "php-2.11.1-from-2.9.7") { + let observability_contexts = if super::has_release(layout, "php-2.11.1") { // Core 2.10's trace carriers on runs, commands and tasks (decided on // #325). Native has no carrier; they are not business state or // history, and the verified backup keeps them. @@ -399,15 +407,53 @@ where } else { 0 }; - Ok(json!({ + let release_2113 = if super::has_release(layout, "php-2.11.3") { + let messages = count::(tx, FULL_FAILURE_MESSAGES).await?; + let dispatches = count::(tx, COMPLETED_DISPATCHES).await?; + sqlx::raw_sql(RELEASE_2113_SQL).execute(&mut *tx).await?; + json!({"full_failure_messages_removed": messages, "service_call_dispatches_removed": dispatches}) + } else { + json!({}) + }; + let mut converted = json!({ "incarnations_copied":copied, "incarnations_created":created, "session_holders_recorded":holders, "service_call_deadlines_copied":deadlines, "queue_delivery_references_removed":delivery_references, "observability_contexts_removed":observability_contexts - })) + }); + if let (Value::Object(converted), Value::Object(release)) = (&mut converted, release_2113) { + converted.extend(release); + } + Ok(converted) } +/// Core 2.14's full failure messages: a message beyond the old TEXT capacity, +/// or with a NUL, keeps a labelled preview in `message` and its full bytes in +/// `message_full`. Native has no second column; it reads the preview, as +/// PHP's own older readers do, and retained history keeps the full failure. +pub(super) const FULL_FAILURE_MESSAGES: &str = + "SELECT COUNT(*) AS n FROM workflow_failures WHERE message_full IS NOT NULL"; + +/// Dispatches PHP 2.11.3 completed; takeover refused any still pending. +pub(super) const COMPLETED_DISPATCHES: &str = + "SELECT COUNT(*) AS n FROM workflow_service_call_dispatches"; + +/// The rest of PHP 2.11.3's additions, which native does not keep: +/// - the service-call idempotency scope digest and its unique index, derived +/// from the call's own target namespace, names and key; +/// - the completed dispatch outbox; +/// - each session's holder incarnation, copied to native's holders first; +/// - each durable stream's reclaim deadline, which only a stream has, and +/// takeover refuses any stream. +const RELEASE_2113_SQL: &str = "ALTER TABLE workflow_failures DROP COLUMN message_full; \ + DROP INDEX wf_service_calls_idem_scope_unique; \ + ALTER TABLE workflow_service_calls DROP COLUMN idempotency_scope_hash; \ + DROP TABLE workflow_service_call_dispatches; \ + ALTER TABLE workflow_worker_sessions DROP COLUMN lease_owner_incarnation; \ + DROP INDEX wf_durable_streams_ns_reclaim_idx; \ + ALTER TABLE workflow_durable_streams DROP COLUMN reclaim_at;"; + /// The tables Core 2.10 gave a trace carrier, with the count of carried values. pub(super) const OBSERVABILITY_CONTEXTS: [(&str, &str); 3] = [ ("workflow_runs", "SELECT COUNT(*) AS n FROM workflow_runs WHERE observability_context IS NOT NULL"), @@ -418,16 +464,26 @@ pub(super) const OBSERVABILITY_CONTEXTS: [(&str, &str); 3] = [ const DELIVERY_REFERENCES: &str = "SELECT COUNT(*) AS n FROM workflow_tasks WHERE queue_delivery_reference IS NOT NULL"; -/// Records each active PHP worker session's holder as native records one: -/// the lease owner's current registration incarnation, which native compares -/// on every read. Without it native would read the session as held by a -/// replaced registration and orphan it. Returns how many were recorded. -pub(super) async fn adopt_session_holders(tx: &mut DB::Connection) -> Result +/// Records each active PHP worker session's holder as native records one, +/// which native compares on every read. Without it native would read the +/// session as held by a replaced registration and orphan it. PHP 2.11.3 +/// records the holder itself (`lease_owner_incarnation`, the same SHA-256 of +/// the registration token), so a session PHP already orphaned stays orphaned. +/// Earlier releases record none: the lease owner's current registration +/// holds it. Returns how many were recorded. +pub(super) async fn adopt_session_holders(tx: &mut DB::Connection, recorded: bool) -> Result where for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>, DB::Arguments: IntoArguments, for<'q> String: Encode<'q, DB> + Type, { + if recorded { + let copied = query::("INSERT INTO dw_worker_session_holders(namespace,session_id,incarnation,updated_at) SELECT namespace,session_id,lease_owner_incarnation,$1 FROM workflow_worker_sessions WHERE status='active' AND lease_owner IS NOT NULL AND lease_owner_incarnation IS NOT NULL") + .bind(DB::bind_time(super::super::store::now())) + .execute(&mut *tx) + .await?; + return Ok(DB::affected(copied) as usize); + } // Newest active incarnation first, as native's current-token read picks. let rows = query::("SELECT s.namespace,s.session_id,i.token FROM workflow_worker_sessions s JOIN dw_worker_registration_incarnations i ON i.namespace=s.namespace AND i.worker_id=s.lease_owner AND i.status='active' WHERE s.status='active' AND s.lease_owner IS NOT NULL ORDER BY s.namespace,s.session_id,i.created_at DESC") .fetch_all(&mut *tx) diff --git a/rust/src/runtime/upgrade/takeover/mysql_journal.rs b/rust/src/runtime/upgrade/takeover/mysql_journal.rs index dd40f08e3..fad95eab0 100644 --- a/rust/src/runtime/upgrade/takeover/mysql_journal.rs +++ b/rust/src/runtime/upgrade/takeover/mysql_journal.rs @@ -27,7 +27,7 @@ const MARKED: i64 = 0; /// Sessions rechecked and the source fingerprinted again after the marker: /// nothing PHP wrote can be missing from the backup. const FENCED: i64 = 1; -const LAST: i64 = 9; +const LAST: i64 = 10; /// One takeover run: what its steps need and what its report says. struct Run<'a> { @@ -315,7 +315,7 @@ async fn perform(connection: &mut MySqlConnection, run: &mut Run<'_>, step: i64) // Rows, unlike DDL, commit with the step that records them. let mut tx = connection.begin().await?; let (copied, created) = adopt_incarnations::(&mut tx, php_incarnations).await?; - let holders = super::adopt_session_holders::(&mut tx).await?; + let holders = super::adopt_session_holders::(&mut tx, super::super::has_release(&run.layout, "php-2.11.3")).await?; advance(&mut *tx, run, step).await?; tx.commit().await?; run.converted @@ -389,6 +389,34 @@ async fn perform(connection: &mut MySqlConnection, run: &mut Run<'_>, step: i64) .await?; } } + 10 => { + // PHP 2.11.3's additions. Count before dropping, as step 9 does. + if column_exists(connection, "workflow_failures", "message_full").await? { + let messages = count::(connection, super::FULL_FAILURE_MESSAGES).await?; + run.converted.insert("full_failure_messages_removed".into(), json!(messages)); + connection.execute("ALTER TABLE workflow_failures DROP COLUMN message_full").await?; + } + if column_exists(connection, "workflow_service_calls", "idempotency_scope_hash").await? { + connection + .execute("ALTER TABLE workflow_service_calls DROP INDEX wf_service_calls_idem_scope_unique, DROP COLUMN idempotency_scope_hash") + .await?; + } + if column_exists(connection, "workflow_service_call_dispatches", "state").await? { + let dispatches = count::(connection, super::COMPLETED_DISPATCHES).await?; + run.converted.insert("service_call_dispatches_removed".into(), json!(dispatches)); + connection.execute("DROP TABLE workflow_service_call_dispatches").await?; + } + if column_exists(connection, "workflow_worker_sessions", "lease_owner_incarnation").await? { + connection + .execute("ALTER TABLE workflow_worker_sessions DROP COLUMN lease_owner_incarnation") + .await?; + } + if column_exists(connection, "workflow_durable_streams", "reclaim_at").await? { + connection + .execute("ALTER TABLE workflow_durable_streams DROP INDEX wf_durable_streams_ns_reclaim_idx, DROP COLUMN reclaim_at") + .await?; + } + } _ => {} } advance(&mut *connection, run, step).await diff --git a/rust/src/runtime/upgrade_tests.rs b/rust/src/runtime/upgrade_tests.rs index 4bb4f1651..b1ca83a22 100644 --- a/rust/src/runtime/upgrade_tests.rs +++ b/rust/src/runtime/upgrade_tests.rs @@ -848,6 +848,8 @@ fn source_layout_names_the_release_each_recorded_delta_reaches() { r#"{"remove":[],"add":[["c","3"]]}"#, r#"{"remove":[["c","3"]],"add":[["c","3 changed"]]}"#, r#"{"remove":[["b","2"]],"add":[["b","2 reordered"]]}"#, + r#"{"remove":[],"add":[["d","4"]]}"#, + r#"{"remove":[],"add":[["d","4 after reorder"]]}"#, ]; let layout = |actual: &[(&str, &str)]| { source_layout(&rows(actual), frozen.clone(), deltas, |row| row.0.clone()) @@ -867,6 +869,19 @@ fn source_layout_names_the_release_each_recorded_delta_reaches() { layout(&[("c", "3 changed"), ("a", "1 changed"), ("b", "2 reordered")]).unwrap(), "php-2.11.1-from-2.9.7" ); + // 2.11.3 descends from each 2.11.1 variant separately. + assert_eq!( + layout(&[("c", "3 changed"), ("a", "1 changed"), ("b", "2"), ("d", "4")]).unwrap(), + "php-2.11.3" + ); + assert_eq!( + layout(&[("c", "3 changed"), ("a", "1 changed"), ("b", "2 reordered"), ("d", "4 after reorder")]).unwrap(), + "php-2.11.3-from-2.9.7" + ); + reason( + layout(&[("c", "3 changed"), ("a", "1 changed"), ("b", "2"), ("d", "4 after reorder")]), + "upgrade_source_catalog_unqualified", + ); // A later release's addition without the earlier change is no recorded layout. reason( layout(&[("a", "1"), ("b", "2"), ("c", "3")]), @@ -875,6 +890,19 @@ fn source_layout_names_the_release_each_recorded_delta_reaches() { reason(layout(&[("a", "1")]), "upgrade_source_catalog_unqualified"); } +#[test] +fn every_recorded_layout_descends_from_an_earlier_one() { + for (index, (name, parent)) in SOURCE_LAYOUTS.iter().enumerate() { + assert!( + *parent == "frozen" || SOURCE_LAYOUTS[..index].iter().any(|(layout, _)| layout == parent), + "{name} names a parent recorded before it" + ); + } + assert_eq!(layout_lineage("php-2.11.3"), [0, 1, 2, 4]); + assert_eq!(layout_lineage("php-2.11.3-from-2.9.7"), [0, 1, 2, 3, 5]); + assert!(layout_lineage("frozen").is_empty()); +} + #[tokio::test] async fn frozen_layout_is_reported_by_inspection_and_fingerprint() { let (_directory, path) = php_layout().await; @@ -911,6 +939,24 @@ const TRACE_CARRIERS: &[&str] = &[ r#"ALTER TABLE "workflow_tasks" ADD COLUMN "observability_context" text"#, ]; +/// PHP 2.11.3's changes (Core 2.14.2 and Server), in its migrations' order. +const RELEASE_2113: &[&str] = &[ + // Core 2.14: full failure messages. + r#"ALTER TABLE "workflow_failures" ADD COLUMN "message_full" text"#, + // Core 2.14: the service-call idempotency scope. + r#"ALTER TABLE "workflow_service_calls" ADD COLUMN "idempotency_scope_hash" varchar"#, + r#"CREATE UNIQUE INDEX "wf_service_calls_idem_scope_unique" on "workflow_service_calls" ("idempotency_scope_hash")"#, + // Core 2.14: the service-call dispatch outbox. + r#"CREATE TABLE "workflow_service_call_dispatches" ("service_call_id" varchar not null, "state" varchar not null, "dispatch_options" text not null, "attempt" integer not null default '1', "max_attempts" integer not null default '1', "available_at" datetime not null, "claim_generation" integer not null default '0', "claim_token" varchar, "claimed_at" datetime, "claim_expires_at" datetime, "authorized_catalog_revision" varchar, "authorized_policy_revision" varchar, "dispatch_attempted_at" datetime, "repair_available_at" datetime, "completed_at" datetime, "last_error" text, "created_at" datetime, "updated_at" datetime, primary key ("service_call_id"))"#, + r#"CREATE INDEX "wf_service_dispatch_due_idx" on "workflow_service_call_dispatches" ("state", "available_at")"#, + r#"CREATE INDEX "wf_service_dispatch_claim_idx" on "workflow_service_call_dispatches" ("state", "claim_expires_at")"#, + r#"CREATE INDEX "wf_service_dispatch_repair_idx" on "workflow_service_call_dispatches" ("state", "repair_available_at")"#, + // Server: each session's holder incarnation and each stream's reclaim deadline. + r#"ALTER TABLE "workflow_worker_sessions" ADD COLUMN "lease_owner_incarnation" varchar"#, + r#"ALTER TABLE "workflow_durable_streams" ADD COLUMN "reclaim_at" datetime"#, + r#"CREATE INDEX "wf_durable_streams_ns_reclaim_idx" on "workflow_durable_streams" ("namespace", "reclaim_at", "id")"#, +]; + /// The frozen test layout with a release's changes applied in order. async fn php_release_layout(changes: &[&[&'static str]]) -> (tempfile::TempDir, String) { let (directory, path) = php_layout().await; @@ -1030,6 +1076,95 @@ async fn takeover_converts_a_php_2111_database(release: &str, changes: &[&[&'sta runtime.close().await; } +/// PHP 2.11.3 databases, fresh or created by 2.9.x, convert. Native keeps +/// each active session's recorded holder, so a session PHP already orphaned +/// stays orphaned, and drops what it does not keep: full failure messages +/// (the labelled preview and retained history remain), the idempotency scope +/// digest, completed dispatches and the stream reclaim deadline. +#[tokio::test] +async fn takeover_converts_php_2113_databases() { + for (release, changes) in [ + ("php-2.11.3", [PHP_280_CHANGES, TRACE_CARRIERS, QUEUE_DELIVERY, RELEASE_2113]), + ("php-2.11.3-from-2.9.7", [PHP_280_CHANGES, QUEUE_DELIVERY, TRACE_CARRIERS, RELEASE_2113]), + ] { + takeover_converts_a_php_2113_database(release, &changes).await; + } +} + +async fn takeover_converts_a_php_2113_database(release: &str, changes: &[&[&'static str]]) { + use sha2::{Digest, Sha256}; + let (_directory, path) = php_release_layout(changes).await; + assert_eq!(inspect_sqlite(&path).await.unwrap()["source_layout"], release); + let digest = |token: &str| -> String { Sha256::digest(token.as_bytes()).iter().map(|byte| format!("{byte:02x}")).collect() }; + let at = |offset: i64| (chrono::Utc::now() + chrono::Duration::seconds(offset)).format("%Y-%m-%d %H:%M:%S").to_string(); + let token = "0123456789abcdef0123456789abcdef"; + let statements = [ + format!("INSERT INTO workflow_worker_registrations(worker_id,namespace,task_queue,runtime,registration_token,status,last_heartbeat_at,created_at,updated_at) VALUES ('worker-a','default','q','php','{token}','active','{}','{}','{}')", at(0), at(-60), at(0)), + format!("INSERT INTO workflow_worker_registration_incarnations VALUES ('{token}','default','worker-a','active',0,NULL,'{}','{}')", at(-60), at(-60)), + // Held by the current registration, as PHP recorded it. + format!("INSERT INTO workflow_worker_sessions(namespace,session_id,queue,status,lease_owner,lease_owner_incarnation,lease_expires_at,ttl_expires_at,last_heartbeat_at,created_at,updated_at) VALUES ('default','held','q','active','worker-a','{}','{}','{}','{}','{}','{}')", digest(token), at(600), at(1800), at(-5), at(-60), at(-5)), + // Held by a registration the worker has since replaced. + format!("INSERT INTO workflow_worker_sessions(namespace,session_id,queue,status,lease_owner,lease_owner_incarnation,lease_expires_at,ttl_expires_at,last_heartbeat_at,created_at,updated_at) VALUES ('default','replaced','q','active','worker-a','{}','{}','{}','{}','{}','{}')", digest("an earlier registration token"), at(600), at(1800), at(-5), at(-60), at(-5)), + "INSERT INTO workflow_failures(id,workflow_run_id,source_kind,source_id,propagation_kind,exception_class,message,file,message_full,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kfail','01jrun0000000000000000000a','activity_execution','a','activity','RuntimeException','preview\n[full message stored separately]','','base64:ZnVsbA==','2026-10-26 10:00:00','2026-10-26 10:00:00')".to_owned(), + "INSERT INTO workflow_service_calls(id,namespace,endpoint_name,service_name,operation_name,caller_namespace,target_namespace,status,outcome,operation_mode,idempotency_key,idempotency_scope_hash,accepted_at,completed_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','default','pay','billing','charge','default','default','completed','succeeded','async','order-1','0000000000000000000000000000000000000000000000000000000000000001','2026-10-26 10:00:00','2026-10-26 10:00:01','2026-10-26 10:00:00','2026-10-26 10:00:01')".to_owned(), + "INSERT INTO workflow_service_call_dispatches(service_call_id,state,dispatch_options,available_at,completed_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','completed','{}','2026-10-26 10:00:00','2026-10-26 10:00:01','2026-10-26 10:00:00','2026-10-26 10:00:01')".to_owned(), + ]; + for statement in &statements { + mutate(&path, Box::leak(statement.clone().into_boxed_str())).await; + } + let fingerprint = backup_fingerprint(&path).await; + let report = takeover::take_over_sqlite(&path, &fingerprint, &queue_config()).await.unwrap(); + assert_eq!(report["status"], "taken_over", "{report}"); + assert_eq!(report["source_layout"], release); + assert_eq!( + [&report["converted"]["session_holders_recorded"], &report["converted"]["full_failure_messages_removed"], &report["converted"]["service_call_dispatches_removed"]], + [&json!(2), &json!(1), &json!(1)], + "{report}" + ); + let mut connection = SqliteConnection::connect(&format!("sqlite://{path}")).await.unwrap(); + let message: String = sqlx::query_scalar("SELECT message FROM workflow_failures").fetch_one(&mut connection).await.unwrap(); + assert_eq!(message, "preview\n[full message stored separately]", "the labelled preview stays"); + let left: i64 = sqlx::query_scalar(concat!( + "SELECT (SELECT COUNT(*) FROM pragma_table_info('workflow_failures') WHERE name='message_full')", + "+(SELECT COUNT(*) FROM pragma_table_info('workflow_service_calls') WHERE name='idempotency_scope_hash')", + "+(SELECT COUNT(*) FROM pragma_table_info('workflow_worker_sessions') WHERE name='lease_owner_incarnation')", + "+(SELECT COUNT(*) FROM pragma_table_info('workflow_durable_streams') WHERE name='reclaim_at')", + "+(SELECT COUNT(*) FROM sqlite_master WHERE name='workflow_service_call_dispatches')")) + .fetch_one(&mut connection).await.unwrap(); + assert_eq!(left, 0, "PHP 2.11.3's additions are gone"); + connection.close().await.unwrap(); + let runtime = crate::runtime::Runtime::open(&path, "test-token".into()).await.unwrap(); + let held = runtime.show_worker_session("held").await.unwrap(); + assert_eq!((held["session"]["status"].clone(), held["session"]["failure_reason"].clone()), (json!("active"), Value::Null), "{held}"); + let replaced = runtime.show_worker_session("replaced").await.unwrap(); + assert_eq!( + (replaced["session"]["status"].clone(), replaced["session"]["failure_reason"].clone()), + (json!("orphaned"), json!("worker_registration_replaced")), + "{replaced}" + ); + runtime.close().await; +} + +/// Native runs a service call's handler when it accepts the call, so it +/// cannot resume a dispatch PHP 2.11.3 scheduled or claimed. Inspection +/// reports it and takeover refuses before any change. +#[tokio::test] +async fn takeover_refuses_a_php_2113_dispatch_still_pending() { + let (_directory, path) = php_release_layout(&[PHP_280_CHANGES, TRACE_CARRIERS, QUEUE_DELIVERY, RELEASE_2113]).await; + mutate(&path, "INSERT INTO workflow_service_calls(id,namespace,endpoint_name,service_name,operation_name,caller_namespace,target_namespace,status,outcome,operation_mode,accepted_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','default','pay','billing','charge','default','default','accepted','accepted','async','2026-10-26 10:00:00','2026-10-26 10:00:00','2026-10-26 10:00:00')").await; + mutate(&path, "INSERT INTO workflow_service_call_dispatches(service_call_id,state,dispatch_options,available_at,created_at,updated_at) VALUES ('01m5wvy8d1j3dtr13es05kcall','scheduled','{}','2026-10-26 10:00:00','2026-10-26 10:00:00','2026-10-26 10:00:00')").await; + let inspection = inspect_sqlite(&path).await.unwrap(); + assert_eq!(inspection["status"], "unqualified", "{inspection}"); + assert_eq!(inspection["service_call_dispatch_inspection"], json!({"dispatches": 1, "pending": 1})); + let before = std::fs::read(&path).unwrap(); + let fingerprint = backup_fingerprint(&path).await; + reason( + takeover::take_over_sqlite(&path, &fingerprint, &queue_config()).await, + "upgrade_source_service_call_dispatches_pending", + ); + assert_eq!(std::fs::read(&path).unwrap(), before, "the PHP database is unchanged"); +} + #[tokio::test] #[ignore = "only invoked as a real subprocess by the takeover interruption test"] async fn sqlite_takeover_child() { diff --git a/rust/upgrade/php-2.11.3-from-2.9.7/README.md b/rust/upgrade/php-2.11.3-from-2.9.7/README.md new file mode 100644 index 000000000..e47c90f0b --- /dev/null +++ b/rust/upgrade/php-2.11.3-from-2.9.7/README.md @@ -0,0 +1,21 @@ +# Exact PHP source-catalog variant: Server 2.11.3 migrated from 2.9.7 + +These deltas apply to the [PHP 2.11.1 layout migrated from +2.9.7](../php-2.11.1-from-2.9.7/README.md). They make the same changes as the +fresh [PHP 2.11.3 delta](../php-2.11.3/README.md); only the parent differs. +2.9.7 had already added `workflow_tasks.queue_delivery_reference`, so its +task column order survives every later release. + +The same delta was captured from two databases: one that published Server +2.9.7 created (image index +`sha256:30c34c5f3256a812fbccbe505a068309b58b0c14d01ffff0465d88604cb36d50`) +and published Server 2.11.3 (image index +`sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e`) +then migrated, and one that 2.9.7 created, 2.11.1 (image +`sha256:bf8bed13f5f809ee3425233180a581353eac4683adbee5c4ecbf63bb18bbd102`) +migrated and 2.11.3 migrated again. + +Definitions were read from actual published-image databases: SQLite, MySQL +8.0, MariaDB 10.11 through the `mysql` connection, and PostgreSQL 17. No +application row values or credentials are recorded. Takeover converts it as +it converts fresh 2.11.3. diff --git a/rust/upgrade/php-2.11.3-from-2.9.7/mariadb.json b/rust/upgrade/php-2.11.3-from-2.9.7/mariadb.json new file mode 100644 index 000000000..71db587f2 --- /dev/null +++ b/rust/upgrade/php-2.11.3-from-2.9.7/mariadb.json @@ -0,0 +1,42 @@ +{ + "remove": [ + [ + "workflow_durable_streams", + "CREATE TABLE `workflow_durable_streams` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) NOT NULL,\n `workflow_instance_id` varchar(191) NOT NULL,\n `workflow_run_id` varchar(26) NOT NULL,\n `stream_name` varchar(191) NOT NULL,\n `status` varchar(16) NOT NULL DEFAULT 'open',\n `last_offset` bigint(20) NOT NULL DEFAULT -1,\n `total_items` bigint(20) unsigned NOT NULL DEFAULT 0,\n `retention_seconds` int(10) unsigned DEFAULT NULL,\n `opened_at` timestamp(6) NULL DEFAULT NULL,\n `last_appended_at` timestamp(6) NULL DEFAULT NULL,\n `closed_at` timestamp(6) NULL DEFAULT NULL,\n `error_reason` varchar(191) DEFAULT NULL,\n `pending_items` bigint(20) unsigned NOT NULL DEFAULT 0,\n `metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_durable_streams_run_name_unique` (`workflow_run_id`,`stream_name`),\n KEY `wf_durable_streams_ns_instance_name_idx` (`namespace`,`workflow_instance_id`,`stream_name`),\n KEY `wf_durable_streams_ns_status_idx` (`namespace`,`status`),\n KEY `workflow_durable_streams_namespace_index` (`namespace`),\n KEY `workflow_durable_streams_workflow_instance_id_index` (`workflow_instance_id`),\n KEY `workflow_durable_streams_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_durable_streams_status_index` (`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_failures", + "CREATE TABLE `workflow_failures` (\n `id` varchar(26) NOT NULL,\n `workflow_run_id` varchar(26) NOT NULL,\n `source_kind` varchar(255) NOT NULL,\n `source_id` varchar(255) NOT NULL,\n `propagation_kind` varchar(255) NOT NULL,\n `failure_category` varchar(255) DEFAULT NULL,\n `non_retryable` tinyint(1) NOT NULL DEFAULT 0,\n `handled` tinyint(1) NOT NULL DEFAULT 0,\n `exception_class` varchar(255) NOT NULL,\n `message` text NOT NULL,\n `file` text NOT NULL,\n `line` int(10) unsigned DEFAULT NULL,\n `trace_preview` longtext DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `workflow_failures_source_kind_source_id_index` (`source_kind`,`source_id`),\n KEY `workflow_failures_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_failures_failure_category_index` (`failure_category`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_worker_sessions", + "CREATE TABLE `workflow_worker_sessions` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) NOT NULL,\n `session_id` varchar(255) NOT NULL,\n `connection` varchar(255) DEFAULT NULL,\n `queue` varchar(255) DEFAULT NULL,\n `requirements` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`requirements`)),\n `status` varchar(32) NOT NULL DEFAULT 'active',\n `lease_owner` varchar(255) DEFAULT NULL,\n `lease_expires_at` timestamp NULL DEFAULT NULL,\n `ttl_expires_at` timestamp NULL DEFAULT NULL,\n `closed_at` timestamp NULL DEFAULT NULL,\n `failure_reason` varchar(128) DEFAULT NULL,\n `lease_seconds` int(10) unsigned NOT NULL DEFAULT 120,\n `ttl_seconds` int(10) unsigned NOT NULL DEFAULT 1800,\n `max_concurrent_activities` int(10) unsigned NOT NULL DEFAULT 1,\n `create_if_missing` tinyint(1) NOT NULL DEFAULT 1,\n `allow_reacquire_after_failure` tinyint(1) NOT NULL DEFAULT 1,\n `last_heartbeat_at` timestamp NULL DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `workflow_worker_sessions_namespace_session_id_unique` (`namespace`,`session_id`),\n KEY `workflow_worker_sessions_namespace_status_index` (`namespace`,`status`),\n KEY `workflow_worker_sessions_namespace_lease_owner_status_index` (`namespace`,`lease_owner`,`status`),\n KEY `workflow_worker_sessions_namespace_queue_status_index` (`namespace`,`queue`,`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_calls", + "CREATE TABLE `workflow_service_calls` (\n `id` varchar(26) NOT NULL,\n `workflow_service_endpoint_id` varchar(26) DEFAULT NULL,\n `workflow_service_id` varchar(26) DEFAULT NULL,\n `workflow_service_operation_id` varchar(26) DEFAULT NULL,\n `namespace` varchar(255) DEFAULT NULL,\n `endpoint_name` varchar(191) NOT NULL,\n `service_name` varchar(191) NOT NULL,\n `operation_name` varchar(191) NOT NULL,\n `caller_namespace` varchar(255) DEFAULT NULL,\n `caller_workflow_instance_id` varchar(191) DEFAULT NULL,\n `caller_workflow_run_id` varchar(26) DEFAULT NULL,\n `target_namespace` varchar(255) DEFAULT NULL,\n `linked_workflow_instance_id` varchar(191) DEFAULT NULL,\n `linked_workflow_run_id` varchar(26) DEFAULT NULL,\n `linked_workflow_update_id` varchar(26) DEFAULT NULL,\n `status` varchar(32) NOT NULL,\n `outcome` varchar(64) DEFAULT NULL,\n `operation_mode` varchar(32) NOT NULL,\n `resolved_binding_kind` varchar(64) DEFAULT NULL,\n `resolved_target_reference` varchar(191) DEFAULT NULL,\n `payload_codec` varchar(255) DEFAULT NULL,\n `input_payload_reference` varchar(191) DEFAULT NULL,\n `output_payload_reference` varchar(191) DEFAULT NULL,\n `failure_payload_reference` varchar(191) DEFAULT NULL,\n `failure_message` text DEFAULT NULL,\n `idempotency_key` varchar(191) DEFAULT NULL,\n `deadline_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`deadline_policy`)),\n `idempotency_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`idempotency_policy`)),\n `cancellation_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`cancellation_policy`)),\n `retry_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`retry_policy`)),\n `boundary_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`boundary_policy`)),\n `metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),\n `outcome_category` varchar(32) DEFAULT NULL,\n `outcome_reason` varchar(191) DEFAULT NULL,\n `outcome_message` text DEFAULT NULL,\n `outcome_metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`outcome_metadata`)),\n `policy_name` varchar(191) DEFAULT NULL,\n `retry_after_seconds` int(10) unsigned DEFAULT NULL,\n `caller_principal_subject` varchar(191) DEFAULT NULL,\n `caller_principal_method` varchar(64) DEFAULT NULL,\n `caller_principal_roles` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`caller_principal_roles`)),\n `caller_principal_tenant` varchar(191) DEFAULT NULL,\n `caller_principal_claims` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`caller_principal_claims`)),\n `accepted_at` timestamp(6) NULL DEFAULT NULL,\n `started_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `failed_at` timestamp(6) NULL DEFAULT NULL,\n `cancelled_at` timestamp(6) NULL DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `call_deadline_at` datetime(6) DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `wf_service_calls_namespace_status_idx` (`namespace`,`status`),\n KEY `wf_service_calls_caller_status_idx` (`caller_namespace`,`status`),\n KEY `wf_service_calls_target_status_idx` (`target_namespace`,`status`),\n KEY `wf_service_calls_namespace_outcome_idx` (`namespace`,`outcome`),\n KEY `wf_service_calls_caller_outcome_idx` (`caller_namespace`,`outcome`),\n KEY `wf_service_calls_target_outcome_idx` (`target_namespace`,`outcome`),\n KEY `wf_service_calls_endpoint_idx` (`workflow_service_endpoint_id`),\n KEY `wf_service_calls_service_idx` (`workflow_service_id`),\n KEY `wf_service_calls_operation_idx` (`workflow_service_operation_id`),\n KEY `wf_service_calls_namespace_idx` (`namespace`),\n KEY `wf_service_calls_caller_namespace_idx` (`caller_namespace`),\n KEY `wf_service_calls_caller_instance_idx` (`caller_workflow_instance_id`),\n KEY `wf_service_calls_caller_run_idx` (`caller_workflow_run_id`),\n KEY `wf_service_calls_target_namespace_idx` (`target_namespace`),\n KEY `wf_service_calls_linked_instance_idx` (`linked_workflow_instance_id`),\n KEY `wf_service_calls_linked_run_idx` (`linked_workflow_run_id`),\n KEY `wf_service_calls_linked_update_idx` (`linked_workflow_update_id`),\n KEY `wf_service_calls_status_idx` (`status`),\n KEY `wf_service_calls_outcome_idx` (`outcome`),\n KEY `wf_service_calls_mode_idx` (`operation_mode`),\n KEY `wf_service_calls_binding_kind_idx` (`resolved_binding_kind`),\n KEY `wf_service_calls_idempotency_idx` (`idempotency_key`),\n KEY `wf_service_calls_outcome_category_idx` (`outcome_category`),\n KEY `wf_service_calls_policy_idx` (`policy_name`),\n KEY `wf_service_calls_principal_subject_idx` (`caller_principal_subject`),\n KEY `wf_service_calls_accepted_at_idx` (`accepted_at`),\n KEY `wf_service_calls_deadline_idx` (`status`,`call_deadline_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ] + ], + "add": [ + [ + "workflow_durable_streams", + "CREATE TABLE `workflow_durable_streams` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) NOT NULL,\n `workflow_instance_id` varchar(191) NOT NULL,\n `workflow_run_id` varchar(26) NOT NULL,\n `stream_name` varchar(191) NOT NULL,\n `status` varchar(16) NOT NULL DEFAULT 'open',\n `last_offset` bigint(20) NOT NULL DEFAULT -1,\n `total_items` bigint(20) unsigned NOT NULL DEFAULT 0,\n `retention_seconds` int(10) unsigned DEFAULT NULL,\n `opened_at` timestamp(6) NULL DEFAULT NULL,\n `last_appended_at` timestamp(6) NULL DEFAULT NULL,\n `closed_at` timestamp(6) NULL DEFAULT NULL,\n `error_reason` varchar(191) DEFAULT NULL,\n `pending_items` bigint(20) unsigned NOT NULL DEFAULT 0,\n `metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `reclaim_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_durable_streams_run_name_unique` (`workflow_run_id`,`stream_name`),\n KEY `wf_durable_streams_ns_instance_name_idx` (`namespace`,`workflow_instance_id`,`stream_name`),\n KEY `wf_durable_streams_ns_status_idx` (`namespace`,`status`),\n KEY `workflow_durable_streams_namespace_index` (`namespace`),\n KEY `workflow_durable_streams_workflow_instance_id_index` (`workflow_instance_id`),\n KEY `workflow_durable_streams_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_durable_streams_status_index` (`status`),\n KEY `wf_durable_streams_ns_reclaim_idx` (`namespace`,`reclaim_at`,`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_failures", + "CREATE TABLE `workflow_failures` (\n `id` varchar(26) NOT NULL,\n `workflow_run_id` varchar(26) NOT NULL,\n `source_kind` varchar(255) NOT NULL,\n `source_id` varchar(255) NOT NULL,\n `propagation_kind` varchar(255) NOT NULL,\n `failure_category` varchar(255) DEFAULT NULL,\n `non_retryable` tinyint(1) NOT NULL DEFAULT 0,\n `handled` tinyint(1) NOT NULL DEFAULT 0,\n `exception_class` varchar(255) NOT NULL,\n `message` text NOT NULL,\n `file` text NOT NULL,\n `line` int(10) unsigned DEFAULT NULL,\n `trace_preview` longtext DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `message_full` longtext DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `workflow_failures_source_kind_source_id_index` (`source_kind`,`source_id`),\n KEY `workflow_failures_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_failures_failure_category_index` (`failure_category`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_call_dispatches", + "CREATE TABLE `workflow_service_call_dispatches` (\n `service_call_id` varchar(26) NOT NULL,\n `state` varchar(32) NOT NULL,\n `dispatch_options` longtext COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`dispatch_options`)),\n `attempt` int(10) unsigned NOT NULL DEFAULT 1,\n `max_attempts` int(10) unsigned NOT NULL DEFAULT 1,\n `available_at` timestamp(6) NOT NULL,\n `claim_generation` bigint(20) unsigned NOT NULL DEFAULT 0,\n `claim_token` char(64) DEFAULT NULL,\n `claimed_at` timestamp(6) NULL DEFAULT NULL,\n `claim_expires_at` timestamp(6) NULL DEFAULT NULL,\n `authorized_catalog_revision` char(64) DEFAULT NULL,\n `authorized_policy_revision` char(64) DEFAULT NULL,\n `dispatch_attempted_at` timestamp(6) NULL DEFAULT NULL,\n `repair_available_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `last_error` text DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`service_call_id`),\n KEY `wf_service_dispatch_due_idx` (`state`,`available_at`),\n KEY `wf_service_dispatch_claim_idx` (`state`,`claim_expires_at`),\n KEY `wf_service_dispatch_repair_idx` (`state`,`repair_available_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_calls", + "CREATE TABLE `workflow_service_calls` (\n `id` varchar(26) NOT NULL,\n `workflow_service_endpoint_id` varchar(26) DEFAULT NULL,\n `workflow_service_id` varchar(26) DEFAULT NULL,\n `workflow_service_operation_id` varchar(26) DEFAULT NULL,\n `namespace` varchar(255) DEFAULT NULL,\n `endpoint_name` varchar(191) NOT NULL,\n `service_name` varchar(191) NOT NULL,\n `operation_name` varchar(191) NOT NULL,\n `caller_namespace` varchar(255) DEFAULT NULL,\n `caller_workflow_instance_id` varchar(191) DEFAULT NULL,\n `caller_workflow_run_id` varchar(26) DEFAULT NULL,\n `target_namespace` varchar(255) DEFAULT NULL,\n `linked_workflow_instance_id` varchar(191) DEFAULT NULL,\n `linked_workflow_run_id` varchar(26) DEFAULT NULL,\n `linked_workflow_update_id` varchar(26) DEFAULT NULL,\n `status` varchar(32) NOT NULL,\n `outcome` varchar(64) DEFAULT NULL,\n `operation_mode` varchar(32) NOT NULL,\n `resolved_binding_kind` varchar(64) DEFAULT NULL,\n `resolved_target_reference` varchar(191) DEFAULT NULL,\n `payload_codec` varchar(255) DEFAULT NULL,\n `input_payload_reference` varchar(191) DEFAULT NULL,\n `output_payload_reference` varchar(191) DEFAULT NULL,\n `failure_payload_reference` varchar(191) DEFAULT NULL,\n `failure_message` text DEFAULT NULL,\n `idempotency_key` varchar(191) DEFAULT NULL,\n `deadline_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`deadline_policy`)),\n `idempotency_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`idempotency_policy`)),\n `cancellation_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`cancellation_policy`)),\n `retry_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`retry_policy`)),\n `boundary_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`boundary_policy`)),\n `metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),\n `outcome_category` varchar(32) DEFAULT NULL,\n `outcome_reason` varchar(191) DEFAULT NULL,\n `outcome_message` text DEFAULT NULL,\n `outcome_metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`outcome_metadata`)),\n `policy_name` varchar(191) DEFAULT NULL,\n `retry_after_seconds` int(10) unsigned DEFAULT NULL,\n `caller_principal_subject` varchar(191) DEFAULT NULL,\n `caller_principal_method` varchar(64) DEFAULT NULL,\n `caller_principal_roles` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`caller_principal_roles`)),\n `caller_principal_tenant` varchar(191) DEFAULT NULL,\n `caller_principal_claims` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`caller_principal_claims`)),\n `accepted_at` timestamp(6) NULL DEFAULT NULL,\n `started_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `failed_at` timestamp(6) NULL DEFAULT NULL,\n `cancelled_at` timestamp(6) NULL DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `call_deadline_at` datetime(6) DEFAULT NULL,\n `idempotency_scope_hash` char(64) DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_service_calls_idem_scope_unique` (`idempotency_scope_hash`),\n KEY `wf_service_calls_namespace_status_idx` (`namespace`,`status`),\n KEY `wf_service_calls_caller_status_idx` (`caller_namespace`,`status`),\n KEY `wf_service_calls_target_status_idx` (`target_namespace`,`status`),\n KEY `wf_service_calls_namespace_outcome_idx` (`namespace`,`outcome`),\n KEY `wf_service_calls_caller_outcome_idx` (`caller_namespace`,`outcome`),\n KEY `wf_service_calls_target_outcome_idx` (`target_namespace`,`outcome`),\n KEY `wf_service_calls_endpoint_idx` (`workflow_service_endpoint_id`),\n KEY `wf_service_calls_service_idx` (`workflow_service_id`),\n KEY `wf_service_calls_operation_idx` (`workflow_service_operation_id`),\n KEY `wf_service_calls_namespace_idx` (`namespace`),\n KEY `wf_service_calls_caller_namespace_idx` (`caller_namespace`),\n KEY `wf_service_calls_caller_instance_idx` (`caller_workflow_instance_id`),\n KEY `wf_service_calls_caller_run_idx` (`caller_workflow_run_id`),\n KEY `wf_service_calls_target_namespace_idx` (`target_namespace`),\n KEY `wf_service_calls_linked_instance_idx` (`linked_workflow_instance_id`),\n KEY `wf_service_calls_linked_run_idx` (`linked_workflow_run_id`),\n KEY `wf_service_calls_linked_update_idx` (`linked_workflow_update_id`),\n KEY `wf_service_calls_status_idx` (`status`),\n KEY `wf_service_calls_outcome_idx` (`outcome`),\n KEY `wf_service_calls_mode_idx` (`operation_mode`),\n KEY `wf_service_calls_binding_kind_idx` (`resolved_binding_kind`),\n KEY `wf_service_calls_idempotency_idx` (`idempotency_key`),\n KEY `wf_service_calls_outcome_category_idx` (`outcome_category`),\n KEY `wf_service_calls_policy_idx` (`policy_name`),\n KEY `wf_service_calls_principal_subject_idx` (`caller_principal_subject`),\n KEY `wf_service_calls_accepted_at_idx` (`accepted_at`),\n KEY `wf_service_calls_deadline_idx` (`status`,`call_deadline_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_worker_sessions", + "CREATE TABLE `workflow_worker_sessions` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) NOT NULL,\n `session_id` varchar(255) NOT NULL,\n `connection` varchar(255) DEFAULT NULL,\n `queue` varchar(255) DEFAULT NULL,\n `requirements` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`requirements`)),\n `status` varchar(32) NOT NULL DEFAULT 'active',\n `lease_owner` varchar(255) DEFAULT NULL,\n `lease_owner_incarnation` char(64) DEFAULT NULL,\n `lease_expires_at` timestamp NULL DEFAULT NULL,\n `ttl_expires_at` timestamp NULL DEFAULT NULL,\n `closed_at` timestamp NULL DEFAULT NULL,\n `failure_reason` varchar(128) DEFAULT NULL,\n `lease_seconds` int(10) unsigned NOT NULL DEFAULT 120,\n `ttl_seconds` int(10) unsigned NOT NULL DEFAULT 1800,\n `max_concurrent_activities` int(10) unsigned NOT NULL DEFAULT 1,\n `create_if_missing` tinyint(1) NOT NULL DEFAULT 1,\n `allow_reacquire_after_failure` tinyint(1) NOT NULL DEFAULT 1,\n `last_heartbeat_at` timestamp NULL DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `workflow_worker_sessions_namespace_session_id_unique` (`namespace`,`session_id`),\n KEY `workflow_worker_sessions_namespace_status_index` (`namespace`,`status`),\n KEY `workflow_worker_sessions_namespace_lease_owner_status_index` (`namespace`,`lease_owner`,`status`),\n KEY `workflow_worker_sessions_namespace_queue_status_index` (`namespace`,`queue`,`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ] + ] +} diff --git a/rust/upgrade/php-2.11.3-from-2.9.7/mysql.json b/rust/upgrade/php-2.11.3-from-2.9.7/mysql.json new file mode 100644 index 000000000..1a69d1df3 --- /dev/null +++ b/rust/upgrade/php-2.11.3-from-2.9.7/mysql.json @@ -0,0 +1,42 @@ +{ + "remove": [ + [ + "workflow_durable_streams", + "CREATE TABLE `workflow_durable_streams` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `stream_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open',\n `last_offset` bigint NOT NULL DEFAULT '-1',\n `total_items` bigint unsigned NOT NULL DEFAULT '0',\n `retention_seconds` int unsigned DEFAULT NULL,\n `opened_at` timestamp(6) NULL DEFAULT NULL,\n `last_appended_at` timestamp(6) NULL DEFAULT NULL,\n `closed_at` timestamp(6) NULL DEFAULT NULL,\n `error_reason` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `pending_items` bigint unsigned NOT NULL DEFAULT '0',\n `metadata` json DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_durable_streams_run_name_unique` (`workflow_run_id`,`stream_name`),\n KEY `wf_durable_streams_ns_instance_name_idx` (`namespace`,`workflow_instance_id`,`stream_name`),\n KEY `wf_durable_streams_ns_status_idx` (`namespace`,`status`),\n KEY `workflow_durable_streams_namespace_index` (`namespace`),\n KEY `workflow_durable_streams_workflow_instance_id_index` (`workflow_instance_id`),\n KEY `workflow_durable_streams_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_durable_streams_status_index` (`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_failures", + "CREATE TABLE `workflow_failures` (\n `id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `source_kind` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `source_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `propagation_kind` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `failure_category` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `non_retryable` tinyint(1) NOT NULL DEFAULT '0',\n `handled` tinyint(1) NOT NULL DEFAULT '0',\n `exception_class` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `message` text COLLATE utf8mb4_unicode_ci NOT NULL,\n `file` text COLLATE utf8mb4_unicode_ci NOT NULL,\n `line` int unsigned DEFAULT NULL,\n `trace_preview` longtext COLLATE utf8mb4_unicode_ci,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `workflow_failures_source_kind_source_id_index` (`source_kind`,`source_id`),\n KEY `workflow_failures_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_failures_failure_category_index` (`failure_category`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_worker_sessions", + "CREATE TABLE `workflow_worker_sessions` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,\n `session_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `connection` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `queue` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `requirements` json DEFAULT NULL,\n `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',\n `lease_owner` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_expires_at` timestamp NULL DEFAULT NULL,\n `ttl_expires_at` timestamp NULL DEFAULT NULL,\n `closed_at` timestamp NULL DEFAULT NULL,\n `failure_reason` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_seconds` int unsigned NOT NULL DEFAULT '120',\n `ttl_seconds` int unsigned NOT NULL DEFAULT '1800',\n `max_concurrent_activities` int unsigned NOT NULL DEFAULT '1',\n `create_if_missing` tinyint(1) NOT NULL DEFAULT '1',\n `allow_reacquire_after_failure` tinyint(1) NOT NULL DEFAULT '1',\n `last_heartbeat_at` timestamp NULL DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `workflow_worker_sessions_namespace_session_id_unique` (`namespace`,`session_id`),\n KEY `workflow_worker_sessions_namespace_status_index` (`namespace`,`status`),\n KEY `workflow_worker_sessions_namespace_lease_owner_status_index` (`namespace`,`lease_owner`,`status`),\n KEY `workflow_worker_sessions_namespace_queue_status_index` (`namespace`,`queue`,`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_calls", + "CREATE TABLE `workflow_service_calls` (\n `id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_service_endpoint_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `workflow_service_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `workflow_service_operation_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `endpoint_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `service_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `operation_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `caller_namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `target_namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_update_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `outcome` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `operation_mode` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `resolved_binding_kind` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `resolved_target_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `payload_codec` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `input_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `output_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `failure_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `failure_message` text COLLATE utf8mb4_unicode_ci,\n `idempotency_key` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `deadline_policy` json DEFAULT NULL,\n `idempotency_policy` json DEFAULT NULL,\n `cancellation_policy` json DEFAULT NULL,\n `retry_policy` json DEFAULT NULL,\n `boundary_policy` json DEFAULT NULL,\n `metadata` json DEFAULT NULL,\n `outcome_category` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `outcome_reason` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `outcome_message` text COLLATE utf8mb4_unicode_ci,\n `outcome_metadata` json DEFAULT NULL,\n `policy_name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `retry_after_seconds` int unsigned DEFAULT NULL,\n `caller_principal_subject` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_method` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_roles` json DEFAULT NULL,\n `caller_principal_tenant` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_claims` json DEFAULT NULL,\n `accepted_at` timestamp(6) NULL DEFAULT NULL,\n `started_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `failed_at` timestamp(6) NULL DEFAULT NULL,\n `cancelled_at` timestamp(6) NULL DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `call_deadline_at` datetime(6) DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `wf_service_calls_namespace_status_idx` (`namespace`,`status`),\n KEY `wf_service_calls_caller_status_idx` (`caller_namespace`,`status`),\n KEY `wf_service_calls_target_status_idx` (`target_namespace`,`status`),\n KEY `wf_service_calls_namespace_outcome_idx` (`namespace`,`outcome`),\n KEY `wf_service_calls_caller_outcome_idx` (`caller_namespace`,`outcome`),\n KEY `wf_service_calls_target_outcome_idx` (`target_namespace`,`outcome`),\n KEY `wf_service_calls_endpoint_idx` (`workflow_service_endpoint_id`),\n KEY `wf_service_calls_service_idx` (`workflow_service_id`),\n KEY `wf_service_calls_operation_idx` (`workflow_service_operation_id`),\n KEY `wf_service_calls_namespace_idx` (`namespace`),\n KEY `wf_service_calls_caller_namespace_idx` (`caller_namespace`),\n KEY `wf_service_calls_caller_instance_idx` (`caller_workflow_instance_id`),\n KEY `wf_service_calls_caller_run_idx` (`caller_workflow_run_id`),\n KEY `wf_service_calls_target_namespace_idx` (`target_namespace`),\n KEY `wf_service_calls_linked_instance_idx` (`linked_workflow_instance_id`),\n KEY `wf_service_calls_linked_run_idx` (`linked_workflow_run_id`),\n KEY `wf_service_calls_linked_update_idx` (`linked_workflow_update_id`),\n KEY `wf_service_calls_status_idx` (`status`),\n KEY `wf_service_calls_outcome_idx` (`outcome`),\n KEY `wf_service_calls_mode_idx` (`operation_mode`),\n KEY `wf_service_calls_binding_kind_idx` (`resolved_binding_kind`),\n KEY `wf_service_calls_idempotency_idx` (`idempotency_key`),\n KEY `wf_service_calls_outcome_category_idx` (`outcome_category`),\n KEY `wf_service_calls_policy_idx` (`policy_name`),\n KEY `wf_service_calls_principal_subject_idx` (`caller_principal_subject`),\n KEY `wf_service_calls_accepted_at_idx` (`accepted_at`),\n KEY `wf_service_calls_deadline_idx` (`status`,`call_deadline_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ] + ], + "add": [ + [ + "workflow_durable_streams", + "CREATE TABLE `workflow_durable_streams` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `stream_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open',\n `last_offset` bigint NOT NULL DEFAULT '-1',\n `total_items` bigint unsigned NOT NULL DEFAULT '0',\n `retention_seconds` int unsigned DEFAULT NULL,\n `opened_at` timestamp(6) NULL DEFAULT NULL,\n `last_appended_at` timestamp(6) NULL DEFAULT NULL,\n `closed_at` timestamp(6) NULL DEFAULT NULL,\n `error_reason` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `pending_items` bigint unsigned NOT NULL DEFAULT '0',\n `metadata` json DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `reclaim_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_durable_streams_run_name_unique` (`workflow_run_id`,`stream_name`),\n KEY `wf_durable_streams_ns_instance_name_idx` (`namespace`,`workflow_instance_id`,`stream_name`),\n KEY `wf_durable_streams_ns_status_idx` (`namespace`,`status`),\n KEY `workflow_durable_streams_namespace_index` (`namespace`),\n KEY `workflow_durable_streams_workflow_instance_id_index` (`workflow_instance_id`),\n KEY `workflow_durable_streams_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_durable_streams_status_index` (`status`),\n KEY `wf_durable_streams_ns_reclaim_idx` (`namespace`,`reclaim_at`,`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_failures", + "CREATE TABLE `workflow_failures` (\n `id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `source_kind` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `source_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `propagation_kind` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `failure_category` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `non_retryable` tinyint(1) NOT NULL DEFAULT '0',\n `handled` tinyint(1) NOT NULL DEFAULT '0',\n `exception_class` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `message` text COLLATE utf8mb4_unicode_ci NOT NULL,\n `file` text COLLATE utf8mb4_unicode_ci NOT NULL,\n `line` int unsigned DEFAULT NULL,\n `trace_preview` longtext COLLATE utf8mb4_unicode_ci,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `message_full` longtext COLLATE utf8mb4_unicode_ci,\n PRIMARY KEY (`id`),\n KEY `workflow_failures_source_kind_source_id_index` (`source_kind`,`source_id`),\n KEY `workflow_failures_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_failures_failure_category_index` (`failure_category`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_call_dispatches", + "CREATE TABLE `workflow_service_call_dispatches` (\n `service_call_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `state` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `dispatch_options` json NOT NULL,\n `attempt` int unsigned NOT NULL DEFAULT '1',\n `max_attempts` int unsigned NOT NULL DEFAULT '1',\n `available_at` timestamp(6) NOT NULL,\n `claim_generation` bigint unsigned NOT NULL DEFAULT '0',\n `claim_token` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `claimed_at` timestamp(6) NULL DEFAULT NULL,\n `claim_expires_at` timestamp(6) NULL DEFAULT NULL,\n `authorized_catalog_revision` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `authorized_policy_revision` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `dispatch_attempted_at` timestamp(6) NULL DEFAULT NULL,\n `repair_available_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `last_error` text COLLATE utf8mb4_unicode_ci,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`service_call_id`),\n KEY `wf_service_dispatch_due_idx` (`state`,`available_at`),\n KEY `wf_service_dispatch_claim_idx` (`state`,`claim_expires_at`),\n KEY `wf_service_dispatch_repair_idx` (`state`,`repair_available_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_calls", + "CREATE TABLE `workflow_service_calls` (\n `id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_service_endpoint_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `workflow_service_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `workflow_service_operation_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `endpoint_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `service_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `operation_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `caller_namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `target_namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_update_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `outcome` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `operation_mode` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `resolved_binding_kind` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `resolved_target_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `payload_codec` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `input_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `output_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `failure_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `failure_message` text COLLATE utf8mb4_unicode_ci,\n `idempotency_key` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `deadline_policy` json DEFAULT NULL,\n `idempotency_policy` json DEFAULT NULL,\n `cancellation_policy` json DEFAULT NULL,\n `retry_policy` json DEFAULT NULL,\n `boundary_policy` json DEFAULT NULL,\n `metadata` json DEFAULT NULL,\n `outcome_category` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `outcome_reason` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `outcome_message` text COLLATE utf8mb4_unicode_ci,\n `outcome_metadata` json DEFAULT NULL,\n `policy_name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `retry_after_seconds` int unsigned DEFAULT NULL,\n `caller_principal_subject` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_method` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_roles` json DEFAULT NULL,\n `caller_principal_tenant` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_claims` json DEFAULT NULL,\n `accepted_at` timestamp(6) NULL DEFAULT NULL,\n `started_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `failed_at` timestamp(6) NULL DEFAULT NULL,\n `cancelled_at` timestamp(6) NULL DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `call_deadline_at` datetime(6) DEFAULT NULL,\n `idempotency_scope_hash` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_service_calls_idem_scope_unique` (`idempotency_scope_hash`),\n KEY `wf_service_calls_namespace_status_idx` (`namespace`,`status`),\n KEY `wf_service_calls_caller_status_idx` (`caller_namespace`,`status`),\n KEY `wf_service_calls_target_status_idx` (`target_namespace`,`status`),\n KEY `wf_service_calls_namespace_outcome_idx` (`namespace`,`outcome`),\n KEY `wf_service_calls_caller_outcome_idx` (`caller_namespace`,`outcome`),\n KEY `wf_service_calls_target_outcome_idx` (`target_namespace`,`outcome`),\n KEY `wf_service_calls_endpoint_idx` (`workflow_service_endpoint_id`),\n KEY `wf_service_calls_service_idx` (`workflow_service_id`),\n KEY `wf_service_calls_operation_idx` (`workflow_service_operation_id`),\n KEY `wf_service_calls_namespace_idx` (`namespace`),\n KEY `wf_service_calls_caller_namespace_idx` (`caller_namespace`),\n KEY `wf_service_calls_caller_instance_idx` (`caller_workflow_instance_id`),\n KEY `wf_service_calls_caller_run_idx` (`caller_workflow_run_id`),\n KEY `wf_service_calls_target_namespace_idx` (`target_namespace`),\n KEY `wf_service_calls_linked_instance_idx` (`linked_workflow_instance_id`),\n KEY `wf_service_calls_linked_run_idx` (`linked_workflow_run_id`),\n KEY `wf_service_calls_linked_update_idx` (`linked_workflow_update_id`),\n KEY `wf_service_calls_status_idx` (`status`),\n KEY `wf_service_calls_outcome_idx` (`outcome`),\n KEY `wf_service_calls_mode_idx` (`operation_mode`),\n KEY `wf_service_calls_binding_kind_idx` (`resolved_binding_kind`),\n KEY `wf_service_calls_idempotency_idx` (`idempotency_key`),\n KEY `wf_service_calls_outcome_category_idx` (`outcome_category`),\n KEY `wf_service_calls_policy_idx` (`policy_name`),\n KEY `wf_service_calls_principal_subject_idx` (`caller_principal_subject`),\n KEY `wf_service_calls_accepted_at_idx` (`accepted_at`),\n KEY `wf_service_calls_deadline_idx` (`status`,`call_deadline_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_worker_sessions", + "CREATE TABLE `workflow_worker_sessions` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,\n `session_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `connection` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `queue` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `requirements` json DEFAULT NULL,\n `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',\n `lease_owner` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_owner_incarnation` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_expires_at` timestamp NULL DEFAULT NULL,\n `ttl_expires_at` timestamp NULL DEFAULT NULL,\n `closed_at` timestamp NULL DEFAULT NULL,\n `failure_reason` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_seconds` int unsigned NOT NULL DEFAULT '120',\n `ttl_seconds` int unsigned NOT NULL DEFAULT '1800',\n `max_concurrent_activities` int unsigned NOT NULL DEFAULT '1',\n `create_if_missing` tinyint(1) NOT NULL DEFAULT '1',\n `allow_reacquire_after_failure` tinyint(1) NOT NULL DEFAULT '1',\n `last_heartbeat_at` timestamp NULL DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `workflow_worker_sessions_namespace_session_id_unique` (`namespace`,`session_id`),\n KEY `workflow_worker_sessions_namespace_status_index` (`namespace`,`status`),\n KEY `workflow_worker_sessions_namespace_lease_owner_status_index` (`namespace`,`lease_owner`,`status`),\n KEY `workflow_worker_sessions_namespace_queue_status_index` (`namespace`,`queue`,`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ] + ] +} diff --git a/rust/upgrade/php-2.11.3-from-2.9.7/postgres.json b/rust/upgrade/php-2.11.3-from-2.9.7/postgres.json new file mode 100644 index 000000000..dae058de7 --- /dev/null +++ b/rust/upgrade/php-2.11.3-from-2.9.7/postgres.json @@ -0,0 +1,382 @@ +{ + "remove": [], + "add": [ + [ + "column", + "workflow_durable_streams.18", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "reclaim_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_failures.16", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "message_full", + "not_null": false, + "type": "text" + } + ], + [ + "column", + "workflow_service_call_dispatches.1", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "service_call_id", + "not_null": true, + "type": "character varying(26)" + } + ], + [ + "column", + "workflow_service_call_dispatches.10", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "claim_expires_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.11", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "authorized_catalog_revision", + "not_null": false, + "type": "character(64)" + } + ], + [ + "column", + "workflow_service_call_dispatches.12", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "authorized_policy_revision", + "not_null": false, + "type": "character(64)" + } + ], + [ + "column", + "workflow_service_call_dispatches.13", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "dispatch_attempted_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.14", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "repair_available_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.15", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "completed_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.16", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "last_error", + "not_null": false, + "type": "text" + } + ], + [ + "column", + "workflow_service_call_dispatches.17", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "created_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.18", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "updated_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.2", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "state", + "not_null": true, + "type": "character varying(32)" + } + ], + [ + "column", + "workflow_service_call_dispatches.3", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "dispatch_options", + "not_null": true, + "type": "json" + } + ], + [ + "column", + "workflow_service_call_dispatches.4", + { + "collation": null, + "default": "1", + "generated": "", + "identity": "", + "name": "attempt", + "not_null": true, + "type": "integer" + } + ], + [ + "column", + "workflow_service_call_dispatches.5", + { + "collation": null, + "default": "1", + "generated": "", + "identity": "", + "name": "max_attempts", + "not_null": true, + "type": "integer" + } + ], + [ + "column", + "workflow_service_call_dispatches.6", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "available_at", + "not_null": true, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.7", + { + "collation": null, + "default": "'0'::bigint", + "generated": "", + "identity": "", + "name": "claim_generation", + "not_null": true, + "type": "bigint" + } + ], + [ + "column", + "workflow_service_call_dispatches.8", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "claim_token", + "not_null": false, + "type": "character(64)" + } + ], + [ + "column", + "workflow_service_call_dispatches.9", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "claimed_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_calls.52", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "idempotency_scope_hash", + "not_null": false, + "type": "character(64)" + } + ], + [ + "column", + "workflow_worker_sessions.21", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "lease_owner_incarnation", + "not_null": false, + "type": "character(64)" + } + ], + [ + "constraint", + "workflow_service_call_dispatches.workflow_service_call_dispatches_pkey", + { + "deferrable": false, + "deferred": false, + "definition": "PRIMARY KEY (service_call_id)", + "validated": true + } + ], + [ + "index", + "wf_durable_streams_ns_reclaim_idx", + { + "definition": "CREATE INDEX wf_durable_streams_ns_reclaim_idx ON public.workflow_durable_streams USING btree (namespace, reclaim_at, id)", + "primary": false, + "ready": true, + "unique": false, + "valid": true + } + ], + [ + "index", + "wf_service_calls_idem_scope_unique", + { + "definition": "CREATE UNIQUE INDEX wf_service_calls_idem_scope_unique ON public.workflow_service_calls USING btree (idempotency_scope_hash)", + "primary": false, + "ready": true, + "unique": true, + "valid": true + } + ], + [ + "index", + "wf_service_dispatch_claim_idx", + { + "definition": "CREATE INDEX wf_service_dispatch_claim_idx ON public.workflow_service_call_dispatches USING btree (state, claim_expires_at)", + "primary": false, + "ready": true, + "unique": false, + "valid": true + } + ], + [ + "index", + "wf_service_dispatch_due_idx", + { + "definition": "CREATE INDEX wf_service_dispatch_due_idx ON public.workflow_service_call_dispatches USING btree (state, available_at)", + "primary": false, + "ready": true, + "unique": false, + "valid": true + } + ], + [ + "index", + "wf_service_dispatch_repair_idx", + { + "definition": "CREATE INDEX wf_service_dispatch_repair_idx ON public.workflow_service_call_dispatches USING btree (state, repair_available_at)", + "primary": false, + "ready": true, + "unique": false, + "valid": true + } + ], + [ + "index", + "workflow_service_call_dispatches_pkey", + { + "definition": "CREATE UNIQUE INDEX workflow_service_call_dispatches_pkey ON public.workflow_service_call_dispatches USING btree (service_call_id)", + "primary": true, + "ready": true, + "unique": true, + "valid": true + } + ], + [ + "relation", + "workflow_service_call_dispatches", + { + "access_method": "heap", + "force_row_security": false, + "kind": "r", + "options": null, + "partition": false, + "partition_key": null, + "persistence": "p", + "row_security": false, + "view": null + } + ] + ] +} diff --git a/rust/upgrade/php-2.11.3-from-2.9.7/sqlite.json b/rust/upgrade/php-2.11.3-from-2.9.7/sqlite.json new file mode 100644 index 000000000..aeb8845b3 --- /dev/null +++ b/rust/upgrade/php-2.11.3-from-2.9.7/sqlite.json @@ -0,0 +1,90 @@ +{ + "remove": [ + [ + "table", + "workflow_durable_streams", + "workflow_durable_streams", + "CREATE TABLE \"workflow_durable_streams\" (\"id\" integer primary key autoincrement not null, \"namespace\" varchar not null, \"workflow_instance_id\" varchar not null, \"workflow_run_id\" varchar not null, \"stream_name\" varchar not null, \"status\" varchar not null default 'open', \"last_offset\" integer not null default '-1', \"total_items\" integer not null default '0', \"retention_seconds\" integer, \"opened_at\" datetime, \"last_appended_at\" datetime, \"closed_at\" datetime, \"error_reason\" varchar, \"pending_items\" integer not null default '0', \"metadata\" text, \"created_at\" datetime, \"updated_at\" datetime)" + ], + [ + "table", + "workflow_failures", + "workflow_failures", + "CREATE TABLE \"workflow_failures\" (\"id\" varchar not null, \"workflow_run_id\" varchar not null, \"source_kind\" varchar not null, \"source_id\" varchar not null, \"propagation_kind\" varchar not null, \"failure_category\" varchar, \"non_retryable\" tinyint(1) not null default '0', \"handled\" tinyint(1) not null default '0', \"exception_class\" varchar not null, \"message\" text not null, \"file\" text not null, \"line\" integer, \"trace_preview\" text, \"created_at\" datetime, \"updated_at\" datetime, primary key (\"id\"))" + ], + [ + "table", + "workflow_worker_sessions", + "workflow_worker_sessions", + "CREATE TABLE \"workflow_worker_sessions\" (\"id\" integer primary key autoincrement not null, \"namespace\" varchar not null, \"session_id\" varchar not null, \"connection\" varchar, \"queue\" varchar, \"requirements\" text, \"status\" varchar not null default 'active', \"lease_owner\" varchar, \"lease_expires_at\" datetime, \"ttl_expires_at\" datetime, \"closed_at\" datetime, \"failure_reason\" varchar, \"lease_seconds\" integer not null default '120', \"ttl_seconds\" integer not null default '1800', \"max_concurrent_activities\" integer not null default '1', \"create_if_missing\" tinyint(1) not null default '1', \"allow_reacquire_after_failure\" tinyint(1) not null default '1', \"last_heartbeat_at\" datetime, \"created_at\" datetime, \"updated_at\" datetime)" + ], + [ + "table", + "workflow_service_calls", + "workflow_service_calls", + "CREATE TABLE \"workflow_service_calls\" (\"id\" varchar not null, \"workflow_service_endpoint_id\" varchar, \"workflow_service_id\" varchar, \"workflow_service_operation_id\" varchar, \"namespace\" varchar, \"endpoint_name\" varchar not null, \"service_name\" varchar not null, \"operation_name\" varchar not null, \"caller_namespace\" varchar, \"caller_workflow_instance_id\" varchar, \"caller_workflow_run_id\" varchar, \"target_namespace\" varchar, \"linked_workflow_instance_id\" varchar, \"linked_workflow_run_id\" varchar, \"linked_workflow_update_id\" varchar, \"status\" varchar not null, \"outcome\" varchar, \"operation_mode\" varchar not null, \"resolved_binding_kind\" varchar, \"resolved_target_reference\" varchar, \"payload_codec\" varchar, \"input_payload_reference\" varchar, \"output_payload_reference\" varchar, \"failure_payload_reference\" varchar, \"failure_message\" text, \"idempotency_key\" varchar, \"deadline_policy\" text, \"idempotency_policy\" text, \"cancellation_policy\" text, \"retry_policy\" text, \"boundary_policy\" text, \"metadata\" text, \"outcome_category\" varchar, \"outcome_reason\" varchar, \"outcome_message\" text, \"outcome_metadata\" text, \"policy_name\" varchar, \"retry_after_seconds\" integer, \"caller_principal_subject\" varchar, \"caller_principal_method\" varchar, \"caller_principal_roles\" text, \"caller_principal_tenant\" varchar, \"caller_principal_claims\" text, \"accepted_at\" datetime, \"started_at\" datetime, \"completed_at\" datetime, \"failed_at\" datetime, \"cancelled_at\" datetime, \"created_at\" datetime, \"updated_at\" datetime, \"call_deadline_at\" datetime, primary key (\"id\"))" + ] + ], + "add": [ + [ + "index", + "wf_durable_streams_ns_reclaim_idx", + "workflow_durable_streams", + "CREATE INDEX \"wf_durable_streams_ns_reclaim_idx\" on \"workflow_durable_streams\" (\"namespace\", \"reclaim_at\", \"id\")" + ], + [ + "index", + "wf_service_calls_idem_scope_unique", + "workflow_service_calls", + "CREATE UNIQUE INDEX \"wf_service_calls_idem_scope_unique\" on \"workflow_service_calls\" (\"idempotency_scope_hash\")" + ], + [ + "index", + "wf_service_dispatch_claim_idx", + "workflow_service_call_dispatches", + "CREATE INDEX \"wf_service_dispatch_claim_idx\" on \"workflow_service_call_dispatches\" (\"state\", \"claim_expires_at\")" + ], + [ + "index", + "wf_service_dispatch_due_idx", + "workflow_service_call_dispatches", + "CREATE INDEX \"wf_service_dispatch_due_idx\" on \"workflow_service_call_dispatches\" (\"state\", \"available_at\")" + ], + [ + "index", + "wf_service_dispatch_repair_idx", + "workflow_service_call_dispatches", + "CREATE INDEX \"wf_service_dispatch_repair_idx\" on \"workflow_service_call_dispatches\" (\"state\", \"repair_available_at\")" + ], + [ + "table", + "workflow_durable_streams", + "workflow_durable_streams", + "CREATE TABLE \"workflow_durable_streams\" (\"id\" integer primary key autoincrement not null, \"namespace\" varchar not null, \"workflow_instance_id\" varchar not null, \"workflow_run_id\" varchar not null, \"stream_name\" varchar not null, \"status\" varchar not null default 'open', \"last_offset\" integer not null default '-1', \"total_items\" integer not null default '0', \"retention_seconds\" integer, \"opened_at\" datetime, \"last_appended_at\" datetime, \"closed_at\" datetime, \"error_reason\" varchar, \"pending_items\" integer not null default '0', \"metadata\" text, \"created_at\" datetime, \"updated_at\" datetime, \"reclaim_at\" datetime)" + ], + [ + "table", + "workflow_failures", + "workflow_failures", + "CREATE TABLE \"workflow_failures\" (\"id\" varchar not null, \"workflow_run_id\" varchar not null, \"source_kind\" varchar not null, \"source_id\" varchar not null, \"propagation_kind\" varchar not null, \"failure_category\" varchar, \"non_retryable\" tinyint(1) not null default '0', \"handled\" tinyint(1) not null default '0', \"exception_class\" varchar not null, \"message\" text not null, \"file\" text not null, \"line\" integer, \"trace_preview\" text, \"created_at\" datetime, \"updated_at\" datetime, \"message_full\" text, primary key (\"id\"))" + ], + [ + "table", + "workflow_service_call_dispatches", + "workflow_service_call_dispatches", + "CREATE TABLE \"workflow_service_call_dispatches\" (\"service_call_id\" varchar not null, \"state\" varchar not null, \"dispatch_options\" text not null, \"attempt\" integer not null default '1', \"max_attempts\" integer not null default '1', \"available_at\" datetime not null, \"claim_generation\" integer not null default '0', \"claim_token\" varchar, \"claimed_at\" datetime, \"claim_expires_at\" datetime, \"authorized_catalog_revision\" varchar, \"authorized_policy_revision\" varchar, \"dispatch_attempted_at\" datetime, \"repair_available_at\" datetime, \"completed_at\" datetime, \"last_error\" text, \"created_at\" datetime, \"updated_at\" datetime, primary key (\"service_call_id\"))" + ], + [ + "table", + "workflow_service_calls", + "workflow_service_calls", + "CREATE TABLE \"workflow_service_calls\" (\"id\" varchar not null, \"workflow_service_endpoint_id\" varchar, \"workflow_service_id\" varchar, \"workflow_service_operation_id\" varchar, \"namespace\" varchar, \"endpoint_name\" varchar not null, \"service_name\" varchar not null, \"operation_name\" varchar not null, \"caller_namespace\" varchar, \"caller_workflow_instance_id\" varchar, \"caller_workflow_run_id\" varchar, \"target_namespace\" varchar, \"linked_workflow_instance_id\" varchar, \"linked_workflow_run_id\" varchar, \"linked_workflow_update_id\" varchar, \"status\" varchar not null, \"outcome\" varchar, \"operation_mode\" varchar not null, \"resolved_binding_kind\" varchar, \"resolved_target_reference\" varchar, \"payload_codec\" varchar, \"input_payload_reference\" varchar, \"output_payload_reference\" varchar, \"failure_payload_reference\" varchar, \"failure_message\" text, \"idempotency_key\" varchar, \"deadline_policy\" text, \"idempotency_policy\" text, \"cancellation_policy\" text, \"retry_policy\" text, \"boundary_policy\" text, \"metadata\" text, \"outcome_category\" varchar, \"outcome_reason\" varchar, \"outcome_message\" text, \"outcome_metadata\" text, \"policy_name\" varchar, \"retry_after_seconds\" integer, \"caller_principal_subject\" varchar, \"caller_principal_method\" varchar, \"caller_principal_roles\" text, \"caller_principal_tenant\" varchar, \"caller_principal_claims\" text, \"accepted_at\" datetime, \"started_at\" datetime, \"completed_at\" datetime, \"failed_at\" datetime, \"cancelled_at\" datetime, \"created_at\" datetime, \"updated_at\" datetime, \"call_deadline_at\" datetime, \"idempotency_scope_hash\" varchar, primary key (\"id\"))" + ], + [ + "table", + "workflow_worker_sessions", + "workflow_worker_sessions", + "CREATE TABLE \"workflow_worker_sessions\" (\"id\" integer primary key autoincrement not null, \"namespace\" varchar not null, \"session_id\" varchar not null, \"connection\" varchar, \"queue\" varchar, \"requirements\" text, \"status\" varchar not null default 'active', \"lease_owner\" varchar, \"lease_expires_at\" datetime, \"ttl_expires_at\" datetime, \"closed_at\" datetime, \"failure_reason\" varchar, \"lease_seconds\" integer not null default '120', \"ttl_seconds\" integer not null default '1800', \"max_concurrent_activities\" integer not null default '1', \"create_if_missing\" tinyint(1) not null default '1', \"allow_reacquire_after_failure\" tinyint(1) not null default '1', \"last_heartbeat_at\" datetime, \"created_at\" datetime, \"updated_at\" datetime, \"lease_owner_incarnation\" varchar)" + ] + ] +} diff --git a/rust/upgrade/php-2.11.3/README.md b/rust/upgrade/php-2.11.3/README.md new file mode 100644 index 000000000..a73d6004e --- /dev/null +++ b/rust/upgrade/php-2.11.3/README.md @@ -0,0 +1,48 @@ +# Exact PHP source-catalog variant: Server 2.11.3 + +These deltas apply to the fresh [PHP 2.11.1 layout](../php-2.11.1/README.md). +They come from normal bootstrap of published Server 2.11.3, tag `2.11.3`, +source `8009904aac5d0a2bf082f06ec0f9f35b816d885a`, image index +`sha256:923a8924ecad13485562d7af30dc87d7fd6d9d7d4758ec8113bac0766fa8603e`, +which pins Core (Workflow package) 2.14.2. + +The physical changes from 2.11.1: + +- Core `2026_10_23_000100_add_full_failure_message_storage`: nullable + `workflow_failures.message_full`. +- Core `2026_10_25_000100_add_service_call_idempotency_scope`: nullable + `workflow_service_calls.idempotency_scope_hash` and its unique index + `wf_service_calls_idem_scope_unique`. +- Core `2026_10_26_000100_create_workflow_service_call_dispatches_table`: the + `workflow_service_call_dispatches` table and its three indexes. +- Server: nullable `workflow_worker_sessions.lease_owner_incarnation`, and + nullable `workflow_durable_streams.reclaim_at` with its index + `wf_durable_streams_ns_reclaim_idx`. + +The same delta was captured from four databases: a fresh 2.11.3 database, +one 2.11.1 created and 2.11.3 migrated, and one 2.8.0 created and 2.11.3 +migrated. The new migrations follow every earlier one, so all three end with +the same layout. A database 2.9.x created keeps its own task column order; +that is the [`php-2.11.3-from-2.9.7`](../php-2.11.3-from-2.9.7/README.md) +variant. + +Definitions were read from actual published-image databases: SQLite, MySQL +8.0, MariaDB 10.11 through the `mysql` connection, and PostgreSQL 17. No +application row values or credentials are recorded. + +Takeover: + +- copies each active session's recorded holder incarnation into + `dw_worker_session_holders`. PHP and native both use the SHA-256 of the + holder registration's token, so a session PHP already orphaned stays + orphaned; +- counts and drops `message_full` (`full_failure_messages_removed`). Native + reads the labelled preview in `message`, as PHP's own older readers do; + retained history and the verified backup keep the full message; +- drops the idempotency scope digest and its index. The digest is derived + from the call's own target namespace, names and key; +- refuses while any dispatch is still scheduled or claimed + (`upgrade_source_service_call_dispatches_pending`), and otherwise counts + and drops the completed outbox (`service_call_dispatches_removed`); +- drops `reclaim_at` and its index. Only a durable stream has one, and + takeover still refuses any message stream. diff --git a/rust/upgrade/php-2.11.3/mariadb.json b/rust/upgrade/php-2.11.3/mariadb.json new file mode 100644 index 000000000..71db587f2 --- /dev/null +++ b/rust/upgrade/php-2.11.3/mariadb.json @@ -0,0 +1,42 @@ +{ + "remove": [ + [ + "workflow_durable_streams", + "CREATE TABLE `workflow_durable_streams` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) NOT NULL,\n `workflow_instance_id` varchar(191) NOT NULL,\n `workflow_run_id` varchar(26) NOT NULL,\n `stream_name` varchar(191) NOT NULL,\n `status` varchar(16) NOT NULL DEFAULT 'open',\n `last_offset` bigint(20) NOT NULL DEFAULT -1,\n `total_items` bigint(20) unsigned NOT NULL DEFAULT 0,\n `retention_seconds` int(10) unsigned DEFAULT NULL,\n `opened_at` timestamp(6) NULL DEFAULT NULL,\n `last_appended_at` timestamp(6) NULL DEFAULT NULL,\n `closed_at` timestamp(6) NULL DEFAULT NULL,\n `error_reason` varchar(191) DEFAULT NULL,\n `pending_items` bigint(20) unsigned NOT NULL DEFAULT 0,\n `metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_durable_streams_run_name_unique` (`workflow_run_id`,`stream_name`),\n KEY `wf_durable_streams_ns_instance_name_idx` (`namespace`,`workflow_instance_id`,`stream_name`),\n KEY `wf_durable_streams_ns_status_idx` (`namespace`,`status`),\n KEY `workflow_durable_streams_namespace_index` (`namespace`),\n KEY `workflow_durable_streams_workflow_instance_id_index` (`workflow_instance_id`),\n KEY `workflow_durable_streams_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_durable_streams_status_index` (`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_failures", + "CREATE TABLE `workflow_failures` (\n `id` varchar(26) NOT NULL,\n `workflow_run_id` varchar(26) NOT NULL,\n `source_kind` varchar(255) NOT NULL,\n `source_id` varchar(255) NOT NULL,\n `propagation_kind` varchar(255) NOT NULL,\n `failure_category` varchar(255) DEFAULT NULL,\n `non_retryable` tinyint(1) NOT NULL DEFAULT 0,\n `handled` tinyint(1) NOT NULL DEFAULT 0,\n `exception_class` varchar(255) NOT NULL,\n `message` text NOT NULL,\n `file` text NOT NULL,\n `line` int(10) unsigned DEFAULT NULL,\n `trace_preview` longtext DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `workflow_failures_source_kind_source_id_index` (`source_kind`,`source_id`),\n KEY `workflow_failures_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_failures_failure_category_index` (`failure_category`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_worker_sessions", + "CREATE TABLE `workflow_worker_sessions` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) NOT NULL,\n `session_id` varchar(255) NOT NULL,\n `connection` varchar(255) DEFAULT NULL,\n `queue` varchar(255) DEFAULT NULL,\n `requirements` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`requirements`)),\n `status` varchar(32) NOT NULL DEFAULT 'active',\n `lease_owner` varchar(255) DEFAULT NULL,\n `lease_expires_at` timestamp NULL DEFAULT NULL,\n `ttl_expires_at` timestamp NULL DEFAULT NULL,\n `closed_at` timestamp NULL DEFAULT NULL,\n `failure_reason` varchar(128) DEFAULT NULL,\n `lease_seconds` int(10) unsigned NOT NULL DEFAULT 120,\n `ttl_seconds` int(10) unsigned NOT NULL DEFAULT 1800,\n `max_concurrent_activities` int(10) unsigned NOT NULL DEFAULT 1,\n `create_if_missing` tinyint(1) NOT NULL DEFAULT 1,\n `allow_reacquire_after_failure` tinyint(1) NOT NULL DEFAULT 1,\n `last_heartbeat_at` timestamp NULL DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `workflow_worker_sessions_namespace_session_id_unique` (`namespace`,`session_id`),\n KEY `workflow_worker_sessions_namespace_status_index` (`namespace`,`status`),\n KEY `workflow_worker_sessions_namespace_lease_owner_status_index` (`namespace`,`lease_owner`,`status`),\n KEY `workflow_worker_sessions_namespace_queue_status_index` (`namespace`,`queue`,`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_calls", + "CREATE TABLE `workflow_service_calls` (\n `id` varchar(26) NOT NULL,\n `workflow_service_endpoint_id` varchar(26) DEFAULT NULL,\n `workflow_service_id` varchar(26) DEFAULT NULL,\n `workflow_service_operation_id` varchar(26) DEFAULT NULL,\n `namespace` varchar(255) DEFAULT NULL,\n `endpoint_name` varchar(191) NOT NULL,\n `service_name` varchar(191) NOT NULL,\n `operation_name` varchar(191) NOT NULL,\n `caller_namespace` varchar(255) DEFAULT NULL,\n `caller_workflow_instance_id` varchar(191) DEFAULT NULL,\n `caller_workflow_run_id` varchar(26) DEFAULT NULL,\n `target_namespace` varchar(255) DEFAULT NULL,\n `linked_workflow_instance_id` varchar(191) DEFAULT NULL,\n `linked_workflow_run_id` varchar(26) DEFAULT NULL,\n `linked_workflow_update_id` varchar(26) DEFAULT NULL,\n `status` varchar(32) NOT NULL,\n `outcome` varchar(64) DEFAULT NULL,\n `operation_mode` varchar(32) NOT NULL,\n `resolved_binding_kind` varchar(64) DEFAULT NULL,\n `resolved_target_reference` varchar(191) DEFAULT NULL,\n `payload_codec` varchar(255) DEFAULT NULL,\n `input_payload_reference` varchar(191) DEFAULT NULL,\n `output_payload_reference` varchar(191) DEFAULT NULL,\n `failure_payload_reference` varchar(191) DEFAULT NULL,\n `failure_message` text DEFAULT NULL,\n `idempotency_key` varchar(191) DEFAULT NULL,\n `deadline_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`deadline_policy`)),\n `idempotency_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`idempotency_policy`)),\n `cancellation_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`cancellation_policy`)),\n `retry_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`retry_policy`)),\n `boundary_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`boundary_policy`)),\n `metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),\n `outcome_category` varchar(32) DEFAULT NULL,\n `outcome_reason` varchar(191) DEFAULT NULL,\n `outcome_message` text DEFAULT NULL,\n `outcome_metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`outcome_metadata`)),\n `policy_name` varchar(191) DEFAULT NULL,\n `retry_after_seconds` int(10) unsigned DEFAULT NULL,\n `caller_principal_subject` varchar(191) DEFAULT NULL,\n `caller_principal_method` varchar(64) DEFAULT NULL,\n `caller_principal_roles` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`caller_principal_roles`)),\n `caller_principal_tenant` varchar(191) DEFAULT NULL,\n `caller_principal_claims` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`caller_principal_claims`)),\n `accepted_at` timestamp(6) NULL DEFAULT NULL,\n `started_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `failed_at` timestamp(6) NULL DEFAULT NULL,\n `cancelled_at` timestamp(6) NULL DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `call_deadline_at` datetime(6) DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `wf_service_calls_namespace_status_idx` (`namespace`,`status`),\n KEY `wf_service_calls_caller_status_idx` (`caller_namespace`,`status`),\n KEY `wf_service_calls_target_status_idx` (`target_namespace`,`status`),\n KEY `wf_service_calls_namespace_outcome_idx` (`namespace`,`outcome`),\n KEY `wf_service_calls_caller_outcome_idx` (`caller_namespace`,`outcome`),\n KEY `wf_service_calls_target_outcome_idx` (`target_namespace`,`outcome`),\n KEY `wf_service_calls_endpoint_idx` (`workflow_service_endpoint_id`),\n KEY `wf_service_calls_service_idx` (`workflow_service_id`),\n KEY `wf_service_calls_operation_idx` (`workflow_service_operation_id`),\n KEY `wf_service_calls_namespace_idx` (`namespace`),\n KEY `wf_service_calls_caller_namespace_idx` (`caller_namespace`),\n KEY `wf_service_calls_caller_instance_idx` (`caller_workflow_instance_id`),\n KEY `wf_service_calls_caller_run_idx` (`caller_workflow_run_id`),\n KEY `wf_service_calls_target_namespace_idx` (`target_namespace`),\n KEY `wf_service_calls_linked_instance_idx` (`linked_workflow_instance_id`),\n KEY `wf_service_calls_linked_run_idx` (`linked_workflow_run_id`),\n KEY `wf_service_calls_linked_update_idx` (`linked_workflow_update_id`),\n KEY `wf_service_calls_status_idx` (`status`),\n KEY `wf_service_calls_outcome_idx` (`outcome`),\n KEY `wf_service_calls_mode_idx` (`operation_mode`),\n KEY `wf_service_calls_binding_kind_idx` (`resolved_binding_kind`),\n KEY `wf_service_calls_idempotency_idx` (`idempotency_key`),\n KEY `wf_service_calls_outcome_category_idx` (`outcome_category`),\n KEY `wf_service_calls_policy_idx` (`policy_name`),\n KEY `wf_service_calls_principal_subject_idx` (`caller_principal_subject`),\n KEY `wf_service_calls_accepted_at_idx` (`accepted_at`),\n KEY `wf_service_calls_deadline_idx` (`status`,`call_deadline_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ] + ], + "add": [ + [ + "workflow_durable_streams", + "CREATE TABLE `workflow_durable_streams` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) NOT NULL,\n `workflow_instance_id` varchar(191) NOT NULL,\n `workflow_run_id` varchar(26) NOT NULL,\n `stream_name` varchar(191) NOT NULL,\n `status` varchar(16) NOT NULL DEFAULT 'open',\n `last_offset` bigint(20) NOT NULL DEFAULT -1,\n `total_items` bigint(20) unsigned NOT NULL DEFAULT 0,\n `retention_seconds` int(10) unsigned DEFAULT NULL,\n `opened_at` timestamp(6) NULL DEFAULT NULL,\n `last_appended_at` timestamp(6) NULL DEFAULT NULL,\n `closed_at` timestamp(6) NULL DEFAULT NULL,\n `error_reason` varchar(191) DEFAULT NULL,\n `pending_items` bigint(20) unsigned NOT NULL DEFAULT 0,\n `metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `reclaim_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_durable_streams_run_name_unique` (`workflow_run_id`,`stream_name`),\n KEY `wf_durable_streams_ns_instance_name_idx` (`namespace`,`workflow_instance_id`,`stream_name`),\n KEY `wf_durable_streams_ns_status_idx` (`namespace`,`status`),\n KEY `workflow_durable_streams_namespace_index` (`namespace`),\n KEY `workflow_durable_streams_workflow_instance_id_index` (`workflow_instance_id`),\n KEY `workflow_durable_streams_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_durable_streams_status_index` (`status`),\n KEY `wf_durable_streams_ns_reclaim_idx` (`namespace`,`reclaim_at`,`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_failures", + "CREATE TABLE `workflow_failures` (\n `id` varchar(26) NOT NULL,\n `workflow_run_id` varchar(26) NOT NULL,\n `source_kind` varchar(255) NOT NULL,\n `source_id` varchar(255) NOT NULL,\n `propagation_kind` varchar(255) NOT NULL,\n `failure_category` varchar(255) DEFAULT NULL,\n `non_retryable` tinyint(1) NOT NULL DEFAULT 0,\n `handled` tinyint(1) NOT NULL DEFAULT 0,\n `exception_class` varchar(255) NOT NULL,\n `message` text NOT NULL,\n `file` text NOT NULL,\n `line` int(10) unsigned DEFAULT NULL,\n `trace_preview` longtext DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `message_full` longtext DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `workflow_failures_source_kind_source_id_index` (`source_kind`,`source_id`),\n KEY `workflow_failures_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_failures_failure_category_index` (`failure_category`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_call_dispatches", + "CREATE TABLE `workflow_service_call_dispatches` (\n `service_call_id` varchar(26) NOT NULL,\n `state` varchar(32) NOT NULL,\n `dispatch_options` longtext COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`dispatch_options`)),\n `attempt` int(10) unsigned NOT NULL DEFAULT 1,\n `max_attempts` int(10) unsigned NOT NULL DEFAULT 1,\n `available_at` timestamp(6) NOT NULL,\n `claim_generation` bigint(20) unsigned NOT NULL DEFAULT 0,\n `claim_token` char(64) DEFAULT NULL,\n `claimed_at` timestamp(6) NULL DEFAULT NULL,\n `claim_expires_at` timestamp(6) NULL DEFAULT NULL,\n `authorized_catalog_revision` char(64) DEFAULT NULL,\n `authorized_policy_revision` char(64) DEFAULT NULL,\n `dispatch_attempted_at` timestamp(6) NULL DEFAULT NULL,\n `repair_available_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `last_error` text DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`service_call_id`),\n KEY `wf_service_dispatch_due_idx` (`state`,`available_at`),\n KEY `wf_service_dispatch_claim_idx` (`state`,`claim_expires_at`),\n KEY `wf_service_dispatch_repair_idx` (`state`,`repair_available_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_calls", + "CREATE TABLE `workflow_service_calls` (\n `id` varchar(26) NOT NULL,\n `workflow_service_endpoint_id` varchar(26) DEFAULT NULL,\n `workflow_service_id` varchar(26) DEFAULT NULL,\n `workflow_service_operation_id` varchar(26) DEFAULT NULL,\n `namespace` varchar(255) DEFAULT NULL,\n `endpoint_name` varchar(191) NOT NULL,\n `service_name` varchar(191) NOT NULL,\n `operation_name` varchar(191) NOT NULL,\n `caller_namespace` varchar(255) DEFAULT NULL,\n `caller_workflow_instance_id` varchar(191) DEFAULT NULL,\n `caller_workflow_run_id` varchar(26) DEFAULT NULL,\n `target_namespace` varchar(255) DEFAULT NULL,\n `linked_workflow_instance_id` varchar(191) DEFAULT NULL,\n `linked_workflow_run_id` varchar(26) DEFAULT NULL,\n `linked_workflow_update_id` varchar(26) DEFAULT NULL,\n `status` varchar(32) NOT NULL,\n `outcome` varchar(64) DEFAULT NULL,\n `operation_mode` varchar(32) NOT NULL,\n `resolved_binding_kind` varchar(64) DEFAULT NULL,\n `resolved_target_reference` varchar(191) DEFAULT NULL,\n `payload_codec` varchar(255) DEFAULT NULL,\n `input_payload_reference` varchar(191) DEFAULT NULL,\n `output_payload_reference` varchar(191) DEFAULT NULL,\n `failure_payload_reference` varchar(191) DEFAULT NULL,\n `failure_message` text DEFAULT NULL,\n `idempotency_key` varchar(191) DEFAULT NULL,\n `deadline_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`deadline_policy`)),\n `idempotency_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`idempotency_policy`)),\n `cancellation_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`cancellation_policy`)),\n `retry_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`retry_policy`)),\n `boundary_policy` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`boundary_policy`)),\n `metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`metadata`)),\n `outcome_category` varchar(32) DEFAULT NULL,\n `outcome_reason` varchar(191) DEFAULT NULL,\n `outcome_message` text DEFAULT NULL,\n `outcome_metadata` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`outcome_metadata`)),\n `policy_name` varchar(191) DEFAULT NULL,\n `retry_after_seconds` int(10) unsigned DEFAULT NULL,\n `caller_principal_subject` varchar(191) DEFAULT NULL,\n `caller_principal_method` varchar(64) DEFAULT NULL,\n `caller_principal_roles` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`caller_principal_roles`)),\n `caller_principal_tenant` varchar(191) DEFAULT NULL,\n `caller_principal_claims` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`caller_principal_claims`)),\n `accepted_at` timestamp(6) NULL DEFAULT NULL,\n `started_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `failed_at` timestamp(6) NULL DEFAULT NULL,\n `cancelled_at` timestamp(6) NULL DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `call_deadline_at` datetime(6) DEFAULT NULL,\n `idempotency_scope_hash` char(64) DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_service_calls_idem_scope_unique` (`idempotency_scope_hash`),\n KEY `wf_service_calls_namespace_status_idx` (`namespace`,`status`),\n KEY `wf_service_calls_caller_status_idx` (`caller_namespace`,`status`),\n KEY `wf_service_calls_target_status_idx` (`target_namespace`,`status`),\n KEY `wf_service_calls_namespace_outcome_idx` (`namespace`,`outcome`),\n KEY `wf_service_calls_caller_outcome_idx` (`caller_namespace`,`outcome`),\n KEY `wf_service_calls_target_outcome_idx` (`target_namespace`,`outcome`),\n KEY `wf_service_calls_endpoint_idx` (`workflow_service_endpoint_id`),\n KEY `wf_service_calls_service_idx` (`workflow_service_id`),\n KEY `wf_service_calls_operation_idx` (`workflow_service_operation_id`),\n KEY `wf_service_calls_namespace_idx` (`namespace`),\n KEY `wf_service_calls_caller_namespace_idx` (`caller_namespace`),\n KEY `wf_service_calls_caller_instance_idx` (`caller_workflow_instance_id`),\n KEY `wf_service_calls_caller_run_idx` (`caller_workflow_run_id`),\n KEY `wf_service_calls_target_namespace_idx` (`target_namespace`),\n KEY `wf_service_calls_linked_instance_idx` (`linked_workflow_instance_id`),\n KEY `wf_service_calls_linked_run_idx` (`linked_workflow_run_id`),\n KEY `wf_service_calls_linked_update_idx` (`linked_workflow_update_id`),\n KEY `wf_service_calls_status_idx` (`status`),\n KEY `wf_service_calls_outcome_idx` (`outcome`),\n KEY `wf_service_calls_mode_idx` (`operation_mode`),\n KEY `wf_service_calls_binding_kind_idx` (`resolved_binding_kind`),\n KEY `wf_service_calls_idempotency_idx` (`idempotency_key`),\n KEY `wf_service_calls_outcome_category_idx` (`outcome_category`),\n KEY `wf_service_calls_policy_idx` (`policy_name`),\n KEY `wf_service_calls_principal_subject_idx` (`caller_principal_subject`),\n KEY `wf_service_calls_accepted_at_idx` (`accepted_at`),\n KEY `wf_service_calls_deadline_idx` (`status`,`call_deadline_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_worker_sessions", + "CREATE TABLE `workflow_worker_sessions` (\n `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) NOT NULL,\n `session_id` varchar(255) NOT NULL,\n `connection` varchar(255) DEFAULT NULL,\n `queue` varchar(255) DEFAULT NULL,\n `requirements` longtext COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`requirements`)),\n `status` varchar(32) NOT NULL DEFAULT 'active',\n `lease_owner` varchar(255) DEFAULT NULL,\n `lease_owner_incarnation` char(64) DEFAULT NULL,\n `lease_expires_at` timestamp NULL DEFAULT NULL,\n `ttl_expires_at` timestamp NULL DEFAULT NULL,\n `closed_at` timestamp NULL DEFAULT NULL,\n `failure_reason` varchar(128) DEFAULT NULL,\n `lease_seconds` int(10) unsigned NOT NULL DEFAULT 120,\n `ttl_seconds` int(10) unsigned NOT NULL DEFAULT 1800,\n `max_concurrent_activities` int(10) unsigned NOT NULL DEFAULT 1,\n `create_if_missing` tinyint(1) NOT NULL DEFAULT 1,\n `allow_reacquire_after_failure` tinyint(1) NOT NULL DEFAULT 1,\n `last_heartbeat_at` timestamp NULL DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `workflow_worker_sessions_namespace_session_id_unique` (`namespace`,`session_id`),\n KEY `workflow_worker_sessions_namespace_status_index` (`namespace`,`status`),\n KEY `workflow_worker_sessions_namespace_lease_owner_status_index` (`namespace`,`lease_owner`,`status`),\n KEY `workflow_worker_sessions_namespace_queue_status_index` (`namespace`,`queue`,`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ] + ] +} diff --git a/rust/upgrade/php-2.11.3/mysql.json b/rust/upgrade/php-2.11.3/mysql.json new file mode 100644 index 000000000..1a69d1df3 --- /dev/null +++ b/rust/upgrade/php-2.11.3/mysql.json @@ -0,0 +1,42 @@ +{ + "remove": [ + [ + "workflow_durable_streams", + "CREATE TABLE `workflow_durable_streams` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `stream_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open',\n `last_offset` bigint NOT NULL DEFAULT '-1',\n `total_items` bigint unsigned NOT NULL DEFAULT '0',\n `retention_seconds` int unsigned DEFAULT NULL,\n `opened_at` timestamp(6) NULL DEFAULT NULL,\n `last_appended_at` timestamp(6) NULL DEFAULT NULL,\n `closed_at` timestamp(6) NULL DEFAULT NULL,\n `error_reason` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `pending_items` bigint unsigned NOT NULL DEFAULT '0',\n `metadata` json DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_durable_streams_run_name_unique` (`workflow_run_id`,`stream_name`),\n KEY `wf_durable_streams_ns_instance_name_idx` (`namespace`,`workflow_instance_id`,`stream_name`),\n KEY `wf_durable_streams_ns_status_idx` (`namespace`,`status`),\n KEY `workflow_durable_streams_namespace_index` (`namespace`),\n KEY `workflow_durable_streams_workflow_instance_id_index` (`workflow_instance_id`),\n KEY `workflow_durable_streams_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_durable_streams_status_index` (`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_failures", + "CREATE TABLE `workflow_failures` (\n `id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `source_kind` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `source_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `propagation_kind` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `failure_category` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `non_retryable` tinyint(1) NOT NULL DEFAULT '0',\n `handled` tinyint(1) NOT NULL DEFAULT '0',\n `exception_class` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `message` text COLLATE utf8mb4_unicode_ci NOT NULL,\n `file` text COLLATE utf8mb4_unicode_ci NOT NULL,\n `line` int unsigned DEFAULT NULL,\n `trace_preview` longtext COLLATE utf8mb4_unicode_ci,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `workflow_failures_source_kind_source_id_index` (`source_kind`,`source_id`),\n KEY `workflow_failures_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_failures_failure_category_index` (`failure_category`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_worker_sessions", + "CREATE TABLE `workflow_worker_sessions` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,\n `session_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `connection` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `queue` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `requirements` json DEFAULT NULL,\n `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',\n `lease_owner` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_expires_at` timestamp NULL DEFAULT NULL,\n `ttl_expires_at` timestamp NULL DEFAULT NULL,\n `closed_at` timestamp NULL DEFAULT NULL,\n `failure_reason` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_seconds` int unsigned NOT NULL DEFAULT '120',\n `ttl_seconds` int unsigned NOT NULL DEFAULT '1800',\n `max_concurrent_activities` int unsigned NOT NULL DEFAULT '1',\n `create_if_missing` tinyint(1) NOT NULL DEFAULT '1',\n `allow_reacquire_after_failure` tinyint(1) NOT NULL DEFAULT '1',\n `last_heartbeat_at` timestamp NULL DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `workflow_worker_sessions_namespace_session_id_unique` (`namespace`,`session_id`),\n KEY `workflow_worker_sessions_namespace_status_index` (`namespace`,`status`),\n KEY `workflow_worker_sessions_namespace_lease_owner_status_index` (`namespace`,`lease_owner`,`status`),\n KEY `workflow_worker_sessions_namespace_queue_status_index` (`namespace`,`queue`,`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_calls", + "CREATE TABLE `workflow_service_calls` (\n `id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_service_endpoint_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `workflow_service_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `workflow_service_operation_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `endpoint_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `service_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `operation_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `caller_namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `target_namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_update_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `outcome` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `operation_mode` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `resolved_binding_kind` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `resolved_target_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `payload_codec` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `input_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `output_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `failure_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `failure_message` text COLLATE utf8mb4_unicode_ci,\n `idempotency_key` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `deadline_policy` json DEFAULT NULL,\n `idempotency_policy` json DEFAULT NULL,\n `cancellation_policy` json DEFAULT NULL,\n `retry_policy` json DEFAULT NULL,\n `boundary_policy` json DEFAULT NULL,\n `metadata` json DEFAULT NULL,\n `outcome_category` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `outcome_reason` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `outcome_message` text COLLATE utf8mb4_unicode_ci,\n `outcome_metadata` json DEFAULT NULL,\n `policy_name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `retry_after_seconds` int unsigned DEFAULT NULL,\n `caller_principal_subject` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_method` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_roles` json DEFAULT NULL,\n `caller_principal_tenant` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_claims` json DEFAULT NULL,\n `accepted_at` timestamp(6) NULL DEFAULT NULL,\n `started_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `failed_at` timestamp(6) NULL DEFAULT NULL,\n `cancelled_at` timestamp(6) NULL DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `call_deadline_at` datetime(6) DEFAULT NULL,\n PRIMARY KEY (`id`),\n KEY `wf_service_calls_namespace_status_idx` (`namespace`,`status`),\n KEY `wf_service_calls_caller_status_idx` (`caller_namespace`,`status`),\n KEY `wf_service_calls_target_status_idx` (`target_namespace`,`status`),\n KEY `wf_service_calls_namespace_outcome_idx` (`namespace`,`outcome`),\n KEY `wf_service_calls_caller_outcome_idx` (`caller_namespace`,`outcome`),\n KEY `wf_service_calls_target_outcome_idx` (`target_namespace`,`outcome`),\n KEY `wf_service_calls_endpoint_idx` (`workflow_service_endpoint_id`),\n KEY `wf_service_calls_service_idx` (`workflow_service_id`),\n KEY `wf_service_calls_operation_idx` (`workflow_service_operation_id`),\n KEY `wf_service_calls_namespace_idx` (`namespace`),\n KEY `wf_service_calls_caller_namespace_idx` (`caller_namespace`),\n KEY `wf_service_calls_caller_instance_idx` (`caller_workflow_instance_id`),\n KEY `wf_service_calls_caller_run_idx` (`caller_workflow_run_id`),\n KEY `wf_service_calls_target_namespace_idx` (`target_namespace`),\n KEY `wf_service_calls_linked_instance_idx` (`linked_workflow_instance_id`),\n KEY `wf_service_calls_linked_run_idx` (`linked_workflow_run_id`),\n KEY `wf_service_calls_linked_update_idx` (`linked_workflow_update_id`),\n KEY `wf_service_calls_status_idx` (`status`),\n KEY `wf_service_calls_outcome_idx` (`outcome`),\n KEY `wf_service_calls_mode_idx` (`operation_mode`),\n KEY `wf_service_calls_binding_kind_idx` (`resolved_binding_kind`),\n KEY `wf_service_calls_idempotency_idx` (`idempotency_key`),\n KEY `wf_service_calls_outcome_category_idx` (`outcome_category`),\n KEY `wf_service_calls_policy_idx` (`policy_name`),\n KEY `wf_service_calls_principal_subject_idx` (`caller_principal_subject`),\n KEY `wf_service_calls_accepted_at_idx` (`accepted_at`),\n KEY `wf_service_calls_deadline_idx` (`status`,`call_deadline_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ] + ], + "add": [ + [ + "workflow_durable_streams", + "CREATE TABLE `workflow_durable_streams` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `stream_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'open',\n `last_offset` bigint NOT NULL DEFAULT '-1',\n `total_items` bigint unsigned NOT NULL DEFAULT '0',\n `retention_seconds` int unsigned DEFAULT NULL,\n `opened_at` timestamp(6) NULL DEFAULT NULL,\n `last_appended_at` timestamp(6) NULL DEFAULT NULL,\n `closed_at` timestamp(6) NULL DEFAULT NULL,\n `error_reason` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `pending_items` bigint unsigned NOT NULL DEFAULT '0',\n `metadata` json DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `reclaim_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_durable_streams_run_name_unique` (`workflow_run_id`,`stream_name`),\n KEY `wf_durable_streams_ns_instance_name_idx` (`namespace`,`workflow_instance_id`,`stream_name`),\n KEY `wf_durable_streams_ns_status_idx` (`namespace`,`status`),\n KEY `workflow_durable_streams_namespace_index` (`namespace`),\n KEY `workflow_durable_streams_workflow_instance_id_index` (`workflow_instance_id`),\n KEY `workflow_durable_streams_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_durable_streams_status_index` (`status`),\n KEY `wf_durable_streams_ns_reclaim_idx` (`namespace`,`reclaim_at`,`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_failures", + "CREATE TABLE `workflow_failures` (\n `id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `source_kind` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `source_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `propagation_kind` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `failure_category` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `non_retryable` tinyint(1) NOT NULL DEFAULT '0',\n `handled` tinyint(1) NOT NULL DEFAULT '0',\n `exception_class` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `message` text COLLATE utf8mb4_unicode_ci NOT NULL,\n `file` text COLLATE utf8mb4_unicode_ci NOT NULL,\n `line` int unsigned DEFAULT NULL,\n `trace_preview` longtext COLLATE utf8mb4_unicode_ci,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `message_full` longtext COLLATE utf8mb4_unicode_ci,\n PRIMARY KEY (`id`),\n KEY `workflow_failures_source_kind_source_id_index` (`source_kind`,`source_id`),\n KEY `workflow_failures_workflow_run_id_index` (`workflow_run_id`),\n KEY `workflow_failures_failure_category_index` (`failure_category`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_call_dispatches", + "CREATE TABLE `workflow_service_call_dispatches` (\n `service_call_id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `state` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `dispatch_options` json NOT NULL,\n `attempt` int unsigned NOT NULL DEFAULT '1',\n `max_attempts` int unsigned NOT NULL DEFAULT '1',\n `available_at` timestamp(6) NOT NULL,\n `claim_generation` bigint unsigned NOT NULL DEFAULT '0',\n `claim_token` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `claimed_at` timestamp(6) NULL DEFAULT NULL,\n `claim_expires_at` timestamp(6) NULL DEFAULT NULL,\n `authorized_catalog_revision` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `authorized_policy_revision` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `dispatch_attempted_at` timestamp(6) NULL DEFAULT NULL,\n `repair_available_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `last_error` text COLLATE utf8mb4_unicode_ci,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n PRIMARY KEY (`service_call_id`),\n KEY `wf_service_dispatch_due_idx` (`state`,`available_at`),\n KEY `wf_service_dispatch_claim_idx` (`state`,`claim_expires_at`),\n KEY `wf_service_dispatch_repair_idx` (`state`,`repair_available_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_service_calls", + "CREATE TABLE `workflow_service_calls` (\n `id` varchar(26) COLLATE utf8mb4_unicode_ci NOT NULL,\n `workflow_service_endpoint_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `workflow_service_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `workflow_service_operation_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `endpoint_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `service_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `operation_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,\n `caller_namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `target_namespace` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_instance_id` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_run_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `linked_workflow_update_id` varchar(26) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `outcome` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `operation_mode` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL,\n `resolved_binding_kind` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `resolved_target_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `payload_codec` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `input_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `output_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `failure_payload_reference` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `failure_message` text COLLATE utf8mb4_unicode_ci,\n `idempotency_key` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `deadline_policy` json DEFAULT NULL,\n `idempotency_policy` json DEFAULT NULL,\n `cancellation_policy` json DEFAULT NULL,\n `retry_policy` json DEFAULT NULL,\n `boundary_policy` json DEFAULT NULL,\n `metadata` json DEFAULT NULL,\n `outcome_category` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `outcome_reason` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `outcome_message` text COLLATE utf8mb4_unicode_ci,\n `outcome_metadata` json DEFAULT NULL,\n `policy_name` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `retry_after_seconds` int unsigned DEFAULT NULL,\n `caller_principal_subject` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_method` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_roles` json DEFAULT NULL,\n `caller_principal_tenant` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `caller_principal_claims` json DEFAULT NULL,\n `accepted_at` timestamp(6) NULL DEFAULT NULL,\n `started_at` timestamp(6) NULL DEFAULT NULL,\n `completed_at` timestamp(6) NULL DEFAULT NULL,\n `failed_at` timestamp(6) NULL DEFAULT NULL,\n `cancelled_at` timestamp(6) NULL DEFAULT NULL,\n `created_at` timestamp(6) NULL DEFAULT NULL,\n `updated_at` timestamp(6) NULL DEFAULT NULL,\n `call_deadline_at` datetime(6) DEFAULT NULL,\n `idempotency_scope_hash` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `wf_service_calls_idem_scope_unique` (`idempotency_scope_hash`),\n KEY `wf_service_calls_namespace_status_idx` (`namespace`,`status`),\n KEY `wf_service_calls_caller_status_idx` (`caller_namespace`,`status`),\n KEY `wf_service_calls_target_status_idx` (`target_namespace`,`status`),\n KEY `wf_service_calls_namespace_outcome_idx` (`namespace`,`outcome`),\n KEY `wf_service_calls_caller_outcome_idx` (`caller_namespace`,`outcome`),\n KEY `wf_service_calls_target_outcome_idx` (`target_namespace`,`outcome`),\n KEY `wf_service_calls_endpoint_idx` (`workflow_service_endpoint_id`),\n KEY `wf_service_calls_service_idx` (`workflow_service_id`),\n KEY `wf_service_calls_operation_idx` (`workflow_service_operation_id`),\n KEY `wf_service_calls_namespace_idx` (`namespace`),\n KEY `wf_service_calls_caller_namespace_idx` (`caller_namespace`),\n KEY `wf_service_calls_caller_instance_idx` (`caller_workflow_instance_id`),\n KEY `wf_service_calls_caller_run_idx` (`caller_workflow_run_id`),\n KEY `wf_service_calls_target_namespace_idx` (`target_namespace`),\n KEY `wf_service_calls_linked_instance_idx` (`linked_workflow_instance_id`),\n KEY `wf_service_calls_linked_run_idx` (`linked_workflow_run_id`),\n KEY `wf_service_calls_linked_update_idx` (`linked_workflow_update_id`),\n KEY `wf_service_calls_status_idx` (`status`),\n KEY `wf_service_calls_outcome_idx` (`outcome`),\n KEY `wf_service_calls_mode_idx` (`operation_mode`),\n KEY `wf_service_calls_binding_kind_idx` (`resolved_binding_kind`),\n KEY `wf_service_calls_idempotency_idx` (`idempotency_key`),\n KEY `wf_service_calls_outcome_category_idx` (`outcome_category`),\n KEY `wf_service_calls_policy_idx` (`policy_name`),\n KEY `wf_service_calls_principal_subject_idx` (`caller_principal_subject`),\n KEY `wf_service_calls_accepted_at_idx` (`accepted_at`),\n KEY `wf_service_calls_deadline_idx` (`status`,`call_deadline_at`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ], + [ + "workflow_worker_sessions", + "CREATE TABLE `workflow_worker_sessions` (\n `id` bigint unsigned NOT NULL AUTO_INCREMENT,\n `namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,\n `session_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,\n `connection` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `queue` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `requirements` json DEFAULT NULL,\n `status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',\n `lease_owner` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_owner_incarnation` char(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_expires_at` timestamp NULL DEFAULT NULL,\n `ttl_expires_at` timestamp NULL DEFAULT NULL,\n `closed_at` timestamp NULL DEFAULT NULL,\n `failure_reason` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL,\n `lease_seconds` int unsigned NOT NULL DEFAULT '120',\n `ttl_seconds` int unsigned NOT NULL DEFAULT '1800',\n `max_concurrent_activities` int unsigned NOT NULL DEFAULT '1',\n `create_if_missing` tinyint(1) NOT NULL DEFAULT '1',\n `allow_reacquire_after_failure` tinyint(1) NOT NULL DEFAULT '1',\n `last_heartbeat_at` timestamp NULL DEFAULT NULL,\n `created_at` timestamp NULL DEFAULT NULL,\n `updated_at` timestamp NULL DEFAULT NULL,\n PRIMARY KEY (`id`),\n UNIQUE KEY `workflow_worker_sessions_namespace_session_id_unique` (`namespace`,`session_id`),\n KEY `workflow_worker_sessions_namespace_status_index` (`namespace`,`status`),\n KEY `workflow_worker_sessions_namespace_lease_owner_status_index` (`namespace`,`lease_owner`,`status`),\n KEY `workflow_worker_sessions_namespace_queue_status_index` (`namespace`,`queue`,`status`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ] + ] +} diff --git a/rust/upgrade/php-2.11.3/postgres.json b/rust/upgrade/php-2.11.3/postgres.json new file mode 100644 index 000000000..dae058de7 --- /dev/null +++ b/rust/upgrade/php-2.11.3/postgres.json @@ -0,0 +1,382 @@ +{ + "remove": [], + "add": [ + [ + "column", + "workflow_durable_streams.18", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "reclaim_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_failures.16", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "message_full", + "not_null": false, + "type": "text" + } + ], + [ + "column", + "workflow_service_call_dispatches.1", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "service_call_id", + "not_null": true, + "type": "character varying(26)" + } + ], + [ + "column", + "workflow_service_call_dispatches.10", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "claim_expires_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.11", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "authorized_catalog_revision", + "not_null": false, + "type": "character(64)" + } + ], + [ + "column", + "workflow_service_call_dispatches.12", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "authorized_policy_revision", + "not_null": false, + "type": "character(64)" + } + ], + [ + "column", + "workflow_service_call_dispatches.13", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "dispatch_attempted_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.14", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "repair_available_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.15", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "completed_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.16", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "last_error", + "not_null": false, + "type": "text" + } + ], + [ + "column", + "workflow_service_call_dispatches.17", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "created_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.18", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "updated_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.2", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "state", + "not_null": true, + "type": "character varying(32)" + } + ], + [ + "column", + "workflow_service_call_dispatches.3", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "dispatch_options", + "not_null": true, + "type": "json" + } + ], + [ + "column", + "workflow_service_call_dispatches.4", + { + "collation": null, + "default": "1", + "generated": "", + "identity": "", + "name": "attempt", + "not_null": true, + "type": "integer" + } + ], + [ + "column", + "workflow_service_call_dispatches.5", + { + "collation": null, + "default": "1", + "generated": "", + "identity": "", + "name": "max_attempts", + "not_null": true, + "type": "integer" + } + ], + [ + "column", + "workflow_service_call_dispatches.6", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "available_at", + "not_null": true, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_call_dispatches.7", + { + "collation": null, + "default": "'0'::bigint", + "generated": "", + "identity": "", + "name": "claim_generation", + "not_null": true, + "type": "bigint" + } + ], + [ + "column", + "workflow_service_call_dispatches.8", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "claim_token", + "not_null": false, + "type": "character(64)" + } + ], + [ + "column", + "workflow_service_call_dispatches.9", + { + "collation": null, + "default": null, + "generated": "", + "identity": "", + "name": "claimed_at", + "not_null": false, + "type": "timestamp(6) without time zone" + } + ], + [ + "column", + "workflow_service_calls.52", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "idempotency_scope_hash", + "not_null": false, + "type": "character(64)" + } + ], + [ + "column", + "workflow_worker_sessions.21", + { + "collation": "default", + "default": null, + "generated": "", + "identity": "", + "name": "lease_owner_incarnation", + "not_null": false, + "type": "character(64)" + } + ], + [ + "constraint", + "workflow_service_call_dispatches.workflow_service_call_dispatches_pkey", + { + "deferrable": false, + "deferred": false, + "definition": "PRIMARY KEY (service_call_id)", + "validated": true + } + ], + [ + "index", + "wf_durable_streams_ns_reclaim_idx", + { + "definition": "CREATE INDEX wf_durable_streams_ns_reclaim_idx ON public.workflow_durable_streams USING btree (namespace, reclaim_at, id)", + "primary": false, + "ready": true, + "unique": false, + "valid": true + } + ], + [ + "index", + "wf_service_calls_idem_scope_unique", + { + "definition": "CREATE UNIQUE INDEX wf_service_calls_idem_scope_unique ON public.workflow_service_calls USING btree (idempotency_scope_hash)", + "primary": false, + "ready": true, + "unique": true, + "valid": true + } + ], + [ + "index", + "wf_service_dispatch_claim_idx", + { + "definition": "CREATE INDEX wf_service_dispatch_claim_idx ON public.workflow_service_call_dispatches USING btree (state, claim_expires_at)", + "primary": false, + "ready": true, + "unique": false, + "valid": true + } + ], + [ + "index", + "wf_service_dispatch_due_idx", + { + "definition": "CREATE INDEX wf_service_dispatch_due_idx ON public.workflow_service_call_dispatches USING btree (state, available_at)", + "primary": false, + "ready": true, + "unique": false, + "valid": true + } + ], + [ + "index", + "wf_service_dispatch_repair_idx", + { + "definition": "CREATE INDEX wf_service_dispatch_repair_idx ON public.workflow_service_call_dispatches USING btree (state, repair_available_at)", + "primary": false, + "ready": true, + "unique": false, + "valid": true + } + ], + [ + "index", + "workflow_service_call_dispatches_pkey", + { + "definition": "CREATE UNIQUE INDEX workflow_service_call_dispatches_pkey ON public.workflow_service_call_dispatches USING btree (service_call_id)", + "primary": true, + "ready": true, + "unique": true, + "valid": true + } + ], + [ + "relation", + "workflow_service_call_dispatches", + { + "access_method": "heap", + "force_row_security": false, + "kind": "r", + "options": null, + "partition": false, + "partition_key": null, + "persistence": "p", + "row_security": false, + "view": null + } + ] + ] +} diff --git a/rust/upgrade/php-2.11.3/sqlite.json b/rust/upgrade/php-2.11.3/sqlite.json new file mode 100644 index 000000000..aeb8845b3 --- /dev/null +++ b/rust/upgrade/php-2.11.3/sqlite.json @@ -0,0 +1,90 @@ +{ + "remove": [ + [ + "table", + "workflow_durable_streams", + "workflow_durable_streams", + "CREATE TABLE \"workflow_durable_streams\" (\"id\" integer primary key autoincrement not null, \"namespace\" varchar not null, \"workflow_instance_id\" varchar not null, \"workflow_run_id\" varchar not null, \"stream_name\" varchar not null, \"status\" varchar not null default 'open', \"last_offset\" integer not null default '-1', \"total_items\" integer not null default '0', \"retention_seconds\" integer, \"opened_at\" datetime, \"last_appended_at\" datetime, \"closed_at\" datetime, \"error_reason\" varchar, \"pending_items\" integer not null default '0', \"metadata\" text, \"created_at\" datetime, \"updated_at\" datetime)" + ], + [ + "table", + "workflow_failures", + "workflow_failures", + "CREATE TABLE \"workflow_failures\" (\"id\" varchar not null, \"workflow_run_id\" varchar not null, \"source_kind\" varchar not null, \"source_id\" varchar not null, \"propagation_kind\" varchar not null, \"failure_category\" varchar, \"non_retryable\" tinyint(1) not null default '0', \"handled\" tinyint(1) not null default '0', \"exception_class\" varchar not null, \"message\" text not null, \"file\" text not null, \"line\" integer, \"trace_preview\" text, \"created_at\" datetime, \"updated_at\" datetime, primary key (\"id\"))" + ], + [ + "table", + "workflow_worker_sessions", + "workflow_worker_sessions", + "CREATE TABLE \"workflow_worker_sessions\" (\"id\" integer primary key autoincrement not null, \"namespace\" varchar not null, \"session_id\" varchar not null, \"connection\" varchar, \"queue\" varchar, \"requirements\" text, \"status\" varchar not null default 'active', \"lease_owner\" varchar, \"lease_expires_at\" datetime, \"ttl_expires_at\" datetime, \"closed_at\" datetime, \"failure_reason\" varchar, \"lease_seconds\" integer not null default '120', \"ttl_seconds\" integer not null default '1800', \"max_concurrent_activities\" integer not null default '1', \"create_if_missing\" tinyint(1) not null default '1', \"allow_reacquire_after_failure\" tinyint(1) not null default '1', \"last_heartbeat_at\" datetime, \"created_at\" datetime, \"updated_at\" datetime)" + ], + [ + "table", + "workflow_service_calls", + "workflow_service_calls", + "CREATE TABLE \"workflow_service_calls\" (\"id\" varchar not null, \"workflow_service_endpoint_id\" varchar, \"workflow_service_id\" varchar, \"workflow_service_operation_id\" varchar, \"namespace\" varchar, \"endpoint_name\" varchar not null, \"service_name\" varchar not null, \"operation_name\" varchar not null, \"caller_namespace\" varchar, \"caller_workflow_instance_id\" varchar, \"caller_workflow_run_id\" varchar, \"target_namespace\" varchar, \"linked_workflow_instance_id\" varchar, \"linked_workflow_run_id\" varchar, \"linked_workflow_update_id\" varchar, \"status\" varchar not null, \"outcome\" varchar, \"operation_mode\" varchar not null, \"resolved_binding_kind\" varchar, \"resolved_target_reference\" varchar, \"payload_codec\" varchar, \"input_payload_reference\" varchar, \"output_payload_reference\" varchar, \"failure_payload_reference\" varchar, \"failure_message\" text, \"idempotency_key\" varchar, \"deadline_policy\" text, \"idempotency_policy\" text, \"cancellation_policy\" text, \"retry_policy\" text, \"boundary_policy\" text, \"metadata\" text, \"outcome_category\" varchar, \"outcome_reason\" varchar, \"outcome_message\" text, \"outcome_metadata\" text, \"policy_name\" varchar, \"retry_after_seconds\" integer, \"caller_principal_subject\" varchar, \"caller_principal_method\" varchar, \"caller_principal_roles\" text, \"caller_principal_tenant\" varchar, \"caller_principal_claims\" text, \"accepted_at\" datetime, \"started_at\" datetime, \"completed_at\" datetime, \"failed_at\" datetime, \"cancelled_at\" datetime, \"created_at\" datetime, \"updated_at\" datetime, \"call_deadline_at\" datetime, primary key (\"id\"))" + ] + ], + "add": [ + [ + "index", + "wf_durable_streams_ns_reclaim_idx", + "workflow_durable_streams", + "CREATE INDEX \"wf_durable_streams_ns_reclaim_idx\" on \"workflow_durable_streams\" (\"namespace\", \"reclaim_at\", \"id\")" + ], + [ + "index", + "wf_service_calls_idem_scope_unique", + "workflow_service_calls", + "CREATE UNIQUE INDEX \"wf_service_calls_idem_scope_unique\" on \"workflow_service_calls\" (\"idempotency_scope_hash\")" + ], + [ + "index", + "wf_service_dispatch_claim_idx", + "workflow_service_call_dispatches", + "CREATE INDEX \"wf_service_dispatch_claim_idx\" on \"workflow_service_call_dispatches\" (\"state\", \"claim_expires_at\")" + ], + [ + "index", + "wf_service_dispatch_due_idx", + "workflow_service_call_dispatches", + "CREATE INDEX \"wf_service_dispatch_due_idx\" on \"workflow_service_call_dispatches\" (\"state\", \"available_at\")" + ], + [ + "index", + "wf_service_dispatch_repair_idx", + "workflow_service_call_dispatches", + "CREATE INDEX \"wf_service_dispatch_repair_idx\" on \"workflow_service_call_dispatches\" (\"state\", \"repair_available_at\")" + ], + [ + "table", + "workflow_durable_streams", + "workflow_durable_streams", + "CREATE TABLE \"workflow_durable_streams\" (\"id\" integer primary key autoincrement not null, \"namespace\" varchar not null, \"workflow_instance_id\" varchar not null, \"workflow_run_id\" varchar not null, \"stream_name\" varchar not null, \"status\" varchar not null default 'open', \"last_offset\" integer not null default '-1', \"total_items\" integer not null default '0', \"retention_seconds\" integer, \"opened_at\" datetime, \"last_appended_at\" datetime, \"closed_at\" datetime, \"error_reason\" varchar, \"pending_items\" integer not null default '0', \"metadata\" text, \"created_at\" datetime, \"updated_at\" datetime, \"reclaim_at\" datetime)" + ], + [ + "table", + "workflow_failures", + "workflow_failures", + "CREATE TABLE \"workflow_failures\" (\"id\" varchar not null, \"workflow_run_id\" varchar not null, \"source_kind\" varchar not null, \"source_id\" varchar not null, \"propagation_kind\" varchar not null, \"failure_category\" varchar, \"non_retryable\" tinyint(1) not null default '0', \"handled\" tinyint(1) not null default '0', \"exception_class\" varchar not null, \"message\" text not null, \"file\" text not null, \"line\" integer, \"trace_preview\" text, \"created_at\" datetime, \"updated_at\" datetime, \"message_full\" text, primary key (\"id\"))" + ], + [ + "table", + "workflow_service_call_dispatches", + "workflow_service_call_dispatches", + "CREATE TABLE \"workflow_service_call_dispatches\" (\"service_call_id\" varchar not null, \"state\" varchar not null, \"dispatch_options\" text not null, \"attempt\" integer not null default '1', \"max_attempts\" integer not null default '1', \"available_at\" datetime not null, \"claim_generation\" integer not null default '0', \"claim_token\" varchar, \"claimed_at\" datetime, \"claim_expires_at\" datetime, \"authorized_catalog_revision\" varchar, \"authorized_policy_revision\" varchar, \"dispatch_attempted_at\" datetime, \"repair_available_at\" datetime, \"completed_at\" datetime, \"last_error\" text, \"created_at\" datetime, \"updated_at\" datetime, primary key (\"service_call_id\"))" + ], + [ + "table", + "workflow_service_calls", + "workflow_service_calls", + "CREATE TABLE \"workflow_service_calls\" (\"id\" varchar not null, \"workflow_service_endpoint_id\" varchar, \"workflow_service_id\" varchar, \"workflow_service_operation_id\" varchar, \"namespace\" varchar, \"endpoint_name\" varchar not null, \"service_name\" varchar not null, \"operation_name\" varchar not null, \"caller_namespace\" varchar, \"caller_workflow_instance_id\" varchar, \"caller_workflow_run_id\" varchar, \"target_namespace\" varchar, \"linked_workflow_instance_id\" varchar, \"linked_workflow_run_id\" varchar, \"linked_workflow_update_id\" varchar, \"status\" varchar not null, \"outcome\" varchar, \"operation_mode\" varchar not null, \"resolved_binding_kind\" varchar, \"resolved_target_reference\" varchar, \"payload_codec\" varchar, \"input_payload_reference\" varchar, \"output_payload_reference\" varchar, \"failure_payload_reference\" varchar, \"failure_message\" text, \"idempotency_key\" varchar, \"deadline_policy\" text, \"idempotency_policy\" text, \"cancellation_policy\" text, \"retry_policy\" text, \"boundary_policy\" text, \"metadata\" text, \"outcome_category\" varchar, \"outcome_reason\" varchar, \"outcome_message\" text, \"outcome_metadata\" text, \"policy_name\" varchar, \"retry_after_seconds\" integer, \"caller_principal_subject\" varchar, \"caller_principal_method\" varchar, \"caller_principal_roles\" text, \"caller_principal_tenant\" varchar, \"caller_principal_claims\" text, \"accepted_at\" datetime, \"started_at\" datetime, \"completed_at\" datetime, \"failed_at\" datetime, \"cancelled_at\" datetime, \"created_at\" datetime, \"updated_at\" datetime, \"call_deadline_at\" datetime, \"idempotency_scope_hash\" varchar, primary key (\"id\"))" + ], + [ + "table", + "workflow_worker_sessions", + "workflow_worker_sessions", + "CREATE TABLE \"workflow_worker_sessions\" (\"id\" integer primary key autoincrement not null, \"namespace\" varchar not null, \"session_id\" varchar not null, \"connection\" varchar, \"queue\" varchar, \"requirements\" text, \"status\" varchar not null default 'active', \"lease_owner\" varchar, \"lease_expires_at\" datetime, \"ttl_expires_at\" datetime, \"closed_at\" datetime, \"failure_reason\" varchar, \"lease_seconds\" integer not null default '120', \"ttl_seconds\" integer not null default '1800', \"max_concurrent_activities\" integer not null default '1', \"create_if_missing\" tinyint(1) not null default '1', \"allow_reacquire_after_failure\" tinyint(1) not null default '1', \"last_heartbeat_at\" datetime, \"created_at\" datetime, \"updated_at\" datetime, \"lease_owner_incarnation\" varchar)" + ] + ] +} From 53fd36beb13b74d25badcb06e2b4ef3be89b5de4 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:45:46 +0000 Subject: [PATCH 4/7] Cache the fixture adapters' Cargo registry and build tree from main Every Shared Server fixtures matrix job waits for published-sdk-adapters, which spent about four of its 6.4 minutes building the same pinned, --locked adapter dependencies each run (PR run 40414928426). The job now restores the public Cargo registry data (registry, git) and the build tree, never the rest of CARGO_HOME. The exact key names the Rust image, the runner OS and architecture, and a digest of every tracked adapter input: the five adapter crates, their build scripts, pip wrapper and SDK profiles. The key step fails unless all five crates and scripts are among the inputs, so the key is never empty. There are no restore keys. Only successful pushes to main save, so no pull request or merge group writes what a later run reads. Every run still builds, stamps and hashes its own adapters, and the final adapter directories and their revision checks are unchanged. A unit test pins this boundary. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/server-parity.yml | 51 ++++++++++++++++- tests/Unit/ServerParityAdapterCacheTest.mjs | 62 +++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/ServerParityAdapterCacheTest.mjs diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 281b88f36..bf9c7a052 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -67,6 +67,7 @@ on: - tests/Unit/ServerParityUpdateValidationRetryTest.mjs - tests/Unit/ServerParityKilledAuthorPollTest.mjs - tests/Unit/ServerParityImagePullTest.mjs + - tests/Unit/ServerParityAdapterCacheTest.mjs - tests/Unit/ServerParityMessageStreamsContractTest.mjs - tests/Unit/ServerParityWorkflowStreamWritesContractTest.mjs - tests/Unit/ServerParityWaitingHistoryContractTest.mjs @@ -142,6 +143,45 @@ jobs: mirror.gcr.io/library/rust@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 \ mirror.gcr.io/library/python@sha256:bf44cdfcb76cd3b41e879bc058fc37ec5872002ccfde7fcb765e218cde0cd79c \ "$(baseline php-2.11.1)" + # The adapters' pinned, --locked dependencies build the same way every + # run. Restore their public Cargo registry data and build tree, keyed + # by the toolchain image, the runner and every tracked adapter input. + # Only successful pushes to main save them, so no pull request or merge + # group writes what a later run reads. Every run still builds, stamps + # and hashes its own adapters, and the final adapters are never cached. + - name: Key the adapters' Cargo cache by every tracked adapter input + id: adapter_key + run: | + set -euo pipefail + mapfile -t inputs < <(git ls-files -- \ + scripts/conformance/server-parity/published-sdk-patch \ + scripts/conformance/server-parity/sdk-directions \ + scripts/conformance/server-parity/legacy-marker-aliases \ + scripts/conformance/server-parity/corrected-markers \ + scripts/conformance/server-parity/query-authority \ + ':(glob)scripts/conformance/server-parity/*-adapters.sh' \ + scripts/ci/patched-pip.sh \ + tests/Fixtures/ServerParityProfiles/published-sdk-patch-replay \ + tests/Fixtures/ServerParityProfiles/published-sdk-directions \ + tests/Fixtures/ServerParityProfiles/legacy-marker-aliases \ + tests/Fixtures/ServerParityProfiles/corrected-markers \ + tests/Fixtures/ServerParityProfiles/query-authority \ + tests/Fixtures/ServerParityProfiles/version-range-repair | sort) + # Never an empty key: every one of the five adapter crates and build + # scripts must be among the inputs. A sixth adapter must join here. + test "$(printf '%s\n' "${inputs[@]}" | grep -c '/rust/Cargo.lock$')" -eq 5 + test "$(printf '%s\n' "${inputs[@]}" | grep -c -- '-adapters.sh$')" -eq 5 + digest=$(sha256sum -- "${inputs[@]}" | sha256sum | cut -c1-64) + echo "key=sdk-adapter-cargo-v1-${RUNNER_OS}-${RUNNER_ARCH}-rust-300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081-${digest}" >> "$GITHUB_OUTPUT" + - name: Restore the adapters' Cargo registry and build tree + id: adapter_cargo + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + sdk-cargo/registry + sdk-cargo/git + sdk-target + key: ${{ steps.adapter_key.outputs.key }} - name: Build the locked published SDK adapter once run: bash scripts/conformance/server-parity/published-sdk-patch-adapters.sh - name: Build genuine legacy-marker adapters with the existing cache @@ -156,6 +196,15 @@ jobs: run: bash scripts/conformance/server-parity/corrected-marker-adapters.sh - name: Build the separate current query consumers once run: bash scripts/conformance/server-parity/query-authority-adapters.sh + - name: Save the adapters' Cargo registry and build tree from main + if: ${{ success() && github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.adapter_cargo.outputs.cache-hit != 'true' }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + sdk-cargo/registry + sdk-cargo/git + sdk-target + key: ${{ steps.adapter_key.outputs.key }} - name: Share current query consumers without compiler caches uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: @@ -1873,7 +1922,7 @@ jobs: bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress docker run --rm --user=1000:1000 --entrypoint node -v "$PWD:/source:ro" -w /source \ - "$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParitySdkDirectionLanesTest.mjs tests/Unit/ServerParityCodecDeadlineCellsTest.mjs tests/Unit/ServerParityCooperativeContractTest.mjs tests/Unit/ServerParityChildCancellationContractTest.mjs tests/Unit/ServerParityScheduleContractTest.mjs tests/Unit/ServerParityVisibilityContractTest.mjs tests/Unit/ServerParityAdmissionContractTest.mjs tests/Unit/ServerParityRoleTokenContractTest.mjs tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs tests/Unit/ServerParitySearchAttributeContractTest.mjs tests/Unit/ServerParityWorkerListingContractTest.mjs tests/Unit/ServerParityWorkerReplacementContractTest.mjs tests/Unit/ServerParityPatchDeploymentContractTest.mjs tests/Unit/ServerParityWaitingHistoryContractTest.mjs tests/Unit/ServerParityTaskRetryContractTest.mjs tests/Unit/ServerParityPublishedPatchContractTest.mjs tests/Unit/ServerParityPublishedPatchTransportTest.mjs tests/Unit/ServerParityRecordFileTest.mjs tests/Unit/ServerParityFailureLocationTest.mjs tests/Unit/ServerParityFailureExcerptTest.mjs tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs tests/Unit/ServerParityScheduleCatalogContractTest.mjs tests/Unit/ServerParityOperatorMetricsContractTest.mjs tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs tests/Unit/ServerParityRecordLocalActivityContractTest.mjs tests/Unit/ServerParityActivityCancellationAckContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParityServiceEndpointsContractTest.mjs tests/Unit/ServerParityServiceCallsContractTest.mjs tests/Unit/ServerParityWorkflowImportContractTest.mjs tests/Unit/ServerParityWaterlineImportContractTest.mjs tests/Unit/ServerParitySystemMetricsContractTest.mjs tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs tests/Unit/ServerParityPrometheusMetricsContractTest.mjs tests/Unit/ServerParityPayloadCleanupContractTest.mjs tests/Unit/ServerParityRepairPassContractTest.mjs tests/Unit/ServerParityClusterInfoContractTest.mjs tests/Unit/ServerParityNamespaceDeleteContractTest.mjs tests/Unit/ServerParityMessageStreamsContractTest.mjs tests/Unit/ServerParityCancellationScopesContractTest.mjs tests/Unit/ServerParityWorkflowPollRequestContractTest.mjs tests/Unit/ServerParityUpdateValidationContractTest.mjs tests/Unit/ServerParityFixtureSelectionTest.mjs tests/Unit/ServerParityUpdateValidationRetryTest.mjs tests/Unit/ServerParityKilledAuthorPollTest.mjs tests/Unit/ServerParityImagePullTest.mjs tests/Unit/ServerParityWorkerSessionsContractTest.mjs tests/Unit/ServerParityWorkerSessionActivitiesContractTest.mjs tests/Unit/ServerParityNexusOperationsContractTest.mjs tests/Unit/ServerParityBridgeAdaptersContractTest.mjs tests/Unit/ServerParityWorkflowStreamWritesContractTest.mjs > parity-evidence/comparator-tests.txt + "$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParitySdkDirectionLanesTest.mjs tests/Unit/ServerParityCodecDeadlineCellsTest.mjs tests/Unit/ServerParityCooperativeContractTest.mjs tests/Unit/ServerParityChildCancellationContractTest.mjs tests/Unit/ServerParityScheduleContractTest.mjs tests/Unit/ServerParityVisibilityContractTest.mjs tests/Unit/ServerParityAdmissionContractTest.mjs tests/Unit/ServerParityRoleTokenContractTest.mjs tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs tests/Unit/ServerParitySearchAttributeContractTest.mjs tests/Unit/ServerParityWorkerListingContractTest.mjs tests/Unit/ServerParityWorkerReplacementContractTest.mjs tests/Unit/ServerParityPatchDeploymentContractTest.mjs tests/Unit/ServerParityWaitingHistoryContractTest.mjs tests/Unit/ServerParityTaskRetryContractTest.mjs tests/Unit/ServerParityPublishedPatchContractTest.mjs tests/Unit/ServerParityPublishedPatchTransportTest.mjs tests/Unit/ServerParityRecordFileTest.mjs tests/Unit/ServerParityFailureLocationTest.mjs tests/Unit/ServerParityFailureExcerptTest.mjs tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs tests/Unit/ServerParityScheduleCatalogContractTest.mjs tests/Unit/ServerParityOperatorMetricsContractTest.mjs tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs tests/Unit/ServerParityRecordLocalActivityContractTest.mjs tests/Unit/ServerParityActivityCancellationAckContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParityServiceEndpointsContractTest.mjs tests/Unit/ServerParityServiceCallsContractTest.mjs tests/Unit/ServerParityWorkflowImportContractTest.mjs tests/Unit/ServerParityWaterlineImportContractTest.mjs tests/Unit/ServerParitySystemMetricsContractTest.mjs tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs tests/Unit/ServerParityPrometheusMetricsContractTest.mjs tests/Unit/ServerParityPayloadCleanupContractTest.mjs tests/Unit/ServerParityRepairPassContractTest.mjs tests/Unit/ServerParityClusterInfoContractTest.mjs tests/Unit/ServerParityNamespaceDeleteContractTest.mjs tests/Unit/ServerParityMessageStreamsContractTest.mjs tests/Unit/ServerParityCancellationScopesContractTest.mjs tests/Unit/ServerParityWorkflowPollRequestContractTest.mjs tests/Unit/ServerParityUpdateValidationContractTest.mjs tests/Unit/ServerParityFixtureSelectionTest.mjs tests/Unit/ServerParityUpdateValidationRetryTest.mjs tests/Unit/ServerParityKilledAuthorPollTest.mjs tests/Unit/ServerParityImagePullTest.mjs tests/Unit/ServerParityAdapterCacheTest.mjs tests/Unit/ServerParityWorkerSessionsContractTest.mjs tests/Unit/ServerParityWorkerSessionActivitiesContractTest.mjs tests/Unit/ServerParityNexusOperationsContractTest.mjs tests/Unit/ServerParityBridgeAdaptersContractTest.mjs tests/Unit/ServerParityWorkflowStreamWritesContractTest.mjs > parity-evidence/comparator-tests.txt docker run --rm --user=1000:1000 --cpus=1 --memory=256m --memory-swap=256m \ --network=none --entrypoint node -v "$PWD:/source:ro" -w /source \ "$image" --test tests/Unit/ServerParityCodecDeadlineLoadTest.mjs > parity-evidence/codec-load-tests.txt diff --git a/tests/Unit/ServerParityAdapterCacheTest.mjs b/tests/Unit/ServerParityAdapterCacheTest.mjs new file mode 100644 index 000000000..5f21884bc --- /dev/null +++ b/tests/Unit/ServerParityAdapterCacheTest.mjs @@ -0,0 +1,62 @@ +// The published SDK adapters' public Cargo registry data and build tree are +// cached between runs, but only successful pushes to main save them: a pull +// request or merge group can restore main's cache and never write one a +// later run reads. The key covers the toolchain, the runner and every +// tracked adapter input, and an empty input list fails the job. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import {resolve} from 'node:path'; + +const workflow = readFileSync(resolve('.github/workflows/server-parity.yml'), 'utf8'); + +function job(name) { + const start = workflow.indexOf(`\n ${name}:\n`); + assert.ok(start >= 0, name); + const next = workflow.slice(start + 1).search(/\n {2}[a-z0-9-]+:\n/); + return next < 0 ? workflow.slice(start) : workflow.slice(start, start + 1 + next); +} + +const steps = block => block.split(/\n {6}(?=- (?:name|uses):)/).slice(1); + +test('only the adapter job caches, and only public registry data and the build tree', () => { + const uses = workflow.match(/uses: actions\/cache[^\s]*/g) ?? []; + assert.deepEqual(uses, [ + 'uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9', + 'uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9', + ]); + const adapters = steps(job('published-sdk-adapters')); + const restore = adapters.findIndex(step => step.includes('actions/cache/restore@')); + const save = adapters.findIndex(step => step.includes('actions/cache/save@')); + const builds = adapters.map((step, index) => [step, index]).filter(([step]) => /-adapters\.sh\n?$/m.test(step)).map(([, index]) => index); + assert.equal(builds.length, 5); + assert.ok(restore >= 0 && restore < Math.min(...builds), 'restored before the first build'); + assert.ok(save > Math.max(...builds), 'saved after the last build'); + for (const index of [restore, save]) { + // Never the whole CARGO_HOME, where credentials would live. + assert.match(adapters[index], /path: \|\n {12}sdk-cargo\/registry\n {12}sdk-cargo\/git\n {12}sdk-target\n/); + assert.match(adapters[index], /key: \$\{\{ steps\.adapter_key\.outputs\.key \}\}$/m); + } + // No restore-keys: a run gets this exact key's tree or builds from nothing. + assert.doesNotMatch(adapters[restore], /restore-keys/); + assert.match(adapters[save], /if: \$\{\{ success\(\) && github\.event_name == 'push' && github\.ref == 'refs\/heads\/main' && /); +}); + +test('the cache key follows the toolchain, the runner and every tracked adapter input', () => { + const block = job('published-sdk-adapters'); + const key = steps(block).find(step => step.includes('id: adapter_key')); + assert.ok(key, 'a key step'); + const [image] = block.match(/mirror\.gcr\.io\/library\/rust@sha256:([0-9a-f]{64})/).slice(1); + assert.ok(key.includes('sdk-adapter-cargo-v1-${RUNNER_OS}-${RUNNER_ARCH}-rust-' + image + '-${digest}'), 'key names the runner and the pulled Rust image'); + assert.match(key, /git ls-files --/); + for (const adapter of ['published-sdk-patch', 'sdk-directions', 'legacy-marker-aliases', 'corrected-markers', 'query-authority']) { + assert.ok(key.includes(`scripts/conformance/server-parity/${adapter} \\\n`), adapter); + } + assert.ok(key.includes("':(glob)scripts/conformance/server-parity/*-adapters.sh'")); + // An empty or partial input list fails instead of keying a cache. + assert.match(key, /grep -c '\/rust\/Cargo\.lock\$'\)" -eq 5/); + assert.match(key, /grep -c -- '-adapters\.sh\$'\)" -eq 5/); + // The five build scripts are the job's adapter build steps. + const builds = steps(block).filter(step => /-adapters\.sh\n?$/m.test(step)); + assert.equal(builds.length, 5); +}); From 717445543bb0648b11f7faa25e4d77f93c43aaa7 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:06:14 +0000 Subject: [PATCH 5/7] Keep PHP update validation tasks at takeover Native validates updates in PHP's own workflow_update_validation_tasks rows and keeps no state of its own, so takeover can leave them as they are instead of refusing the source. A pending task whose caller was waiting on PHP is then what a PHP restart leaves: a validator may still claim it, and a caller's retry with the same request reads its recorded outcome. Co-Authored-By: Claude Opus 5.5 --- ...2026-10-26-takeover-runtime-credentials.md | 9 ++++-- rust/src/runtime/upgrade/takeover.rs | 3 +- rust/src/runtime/upgrade_tests.rs | 30 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/rust-server-port-checkpoints/2026-10-26-takeover-runtime-credentials.md b/docs/rust-server-port-checkpoints/2026-10-26-takeover-runtime-credentials.md index d096e904d..24fb6ae0a 100644 --- a/docs/rust-server-port-checkpoints/2026-10-26-takeover-runtime-credentials.md +++ b/docs/rust-server-port-checkpoints/2026-10-26-takeover-runtime-credentials.md @@ -19,5 +19,10 @@ - SQLite: `takeover_keeps_php_runtime_credentials_that_native_then_accepts`; - PostgreSQL, MySQL and MariaDB: the 2.11.1 takeover qualification, for both the fresh and the upgraded-from-2.9.7 layouts. -- **Still refused.** Message streams, update validation tasks and external - payloads. +- **Update validation tasks** carry over too (October 27). Native validates + updates in the same `workflow_update_validation_tasks` rows and keeps no + state of its own, so takeover leaves them as they are. A pending task whose + caller was waiting on PHP is then what a PHP restart leaves: a validator may + still claim it, and a caller's retry with the same request reads its + recorded outcome. `takeover_keeps_php_update_validation_tasks` covers it. +- **Still refused.** Message streams and external payloads. diff --git a/rust/src/runtime/upgrade/takeover.rs b/rust/src/runtime/upgrade/takeover.rs index 2bbb93ce1..7c0c251ab 100644 --- a/rust/src/runtime/upgrade/takeover.rs +++ b/rust/src/runtime/upgrade/takeover.rs @@ -221,7 +221,7 @@ fn report(backend: &str, layout: &str, digest: &Value, inventory: &Value, conver /// Published PHP features native does not implement yet. A source holding any /// of their rows is refused, rather than taken over without them. Derived /// projections and PHP's own queue and poll bookkeeping are left in place. -const UNSUPPORTED_FEATURES: [(&str, &[&str]); 2] = [ ( +const UNSUPPORTED_FEATURES: [(&str, &[&str]); 1] = [ ( "upgrade_source_message_streams_unsupported", &[ "workflow_messages", @@ -231,7 +231,6 @@ const UNSUPPORTED_FEATURES: [(&str, &[&str]); 2] = [ ( "workflow_inbound_stream_items", ], ), - ("upgrade_source_update_validation_unsupported", &["workflow_update_validation_tasks"]), ]; /// Takeover needs every inspection result it relies on, and refuses what diff --git a/rust/src/runtime/upgrade_tests.rs b/rust/src/runtime/upgrade_tests.rs index b1ca83a22..b9bcdb4d7 100644 --- a/rust/src/runtime/upgrade_tests.rs +++ b/rust/src/runtime/upgrade_tests.rs @@ -1402,6 +1402,36 @@ async fn takeover_keeps_php_service_calls_and_their_deadlines() { runtime.close().await; } +/// PHP's update validation tasks carry over unchanged: native validates +/// updates in the same `workflow_update_validation_tasks` rows, with no state +/// of its own. A pending task whose caller was waiting on PHP is then what a +/// PHP restart leaves: a validator may still claim it, and a caller's retry +/// with the same request reads its recorded outcome. +#[tokio::test] +async fn takeover_keeps_php_update_validation_tasks() { + let (_directory, path) = php_release_layout(&[PHP_280_CHANGES, TRACE_CARRIERS, QUEUE_DELIVERY]).await; + let row = |id: &str, status: &str, decided: &str| format!( + "INSERT INTO workflow_update_validation_tasks(id,idempotency_key,namespace,workflow_instance_id,workflow_run_id,workflow_type,task_queue,update_name,request_id,input_hash,payload_codec,arguments,status,attempt_count,approved_at,created_at,updated_at) VALUES ('{id}','key-{id}','default','order-1','01jrun0000000000000000000a','orders','q','approve','req-{id}','{}','avro','[]','{status}',{},{decided},'2026-10-26 10:00:00','2026-10-26 10:00:00')", + "0".repeat(64), if status == "pending" { 0 } else { 1 }); + for statement in [row("01m5wvy8d1j3dtr13es05kval1", "pending", "NULL"), row("01m5wvy8d1j3dtr13es05kval2", "approved", "'2026-10-26 10:00:01'")] { + mutate(&path, Box::leak(statement.into_boxed_str())).await; + } + let read = |path: String| async move { + let mut connection = SqliteConnection::connect(&format!("sqlite://{path}")).await.unwrap(); + let rows: Vec<(String, String, i64, Option)> = sqlx::query_as("SELECT id,status,attempt_count,approved_at FROM workflow_update_validation_tasks ORDER BY id") + .fetch_all(&mut connection).await.unwrap(); + connection.close().await.unwrap(); + rows + }; + let before = read(path.clone()).await; + let fingerprint = backup_fingerprint(&path).await; + let report = takeover::take_over_sqlite(&path, &fingerprint, &queue_config()).await.unwrap(); + assert_eq!(report["status"], "taken_over", "{report}"); + assert_eq!(read(path.clone()).await, before, "the tasks carry over unchanged"); + let runtime = crate::runtime::Runtime::open(&path, "test-token".into()).await.unwrap(); + runtime.close().await; +} + /// PHP's runtime credentials carry over: native stores and checks the same /// SHA-256 token hashes in the same table, so a worker's PHP-issued token /// still authenticates after takeover, and a revoked one still does not. From 037a97d1041a82605410ac07db5b2e35fc9c9846 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Tue, 27 Oct 2026 00:32:00 +0000 Subject: [PATCH 6/7] Revert the fixture adapter Cargo cache for now The organization's central action policy requires every cache key in a pull-request workflow to include github.event_name, so a cache saved only by pushes to main could never be read by pull request or merge group runs, which are the runs it was meant to speed up. The Docker workflow isolation policy also refuses the inline key step. Take the cache out of this PR and settle the design with the policy owners first. This reverts commit 53fd36beb13b74d25badcb06e2b4ef3be89b5de4. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/server-parity.yml | 51 +---------------- tests/Unit/ServerParityAdapterCacheTest.mjs | 62 --------------------- 2 files changed, 1 insertion(+), 112 deletions(-) delete mode 100644 tests/Unit/ServerParityAdapterCacheTest.mjs diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index bf9c7a052..281b88f36 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -67,7 +67,6 @@ on: - tests/Unit/ServerParityUpdateValidationRetryTest.mjs - tests/Unit/ServerParityKilledAuthorPollTest.mjs - tests/Unit/ServerParityImagePullTest.mjs - - tests/Unit/ServerParityAdapterCacheTest.mjs - tests/Unit/ServerParityMessageStreamsContractTest.mjs - tests/Unit/ServerParityWorkflowStreamWritesContractTest.mjs - tests/Unit/ServerParityWaitingHistoryContractTest.mjs @@ -143,45 +142,6 @@ jobs: mirror.gcr.io/library/rust@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 \ mirror.gcr.io/library/python@sha256:bf44cdfcb76cd3b41e879bc058fc37ec5872002ccfde7fcb765e218cde0cd79c \ "$(baseline php-2.11.1)" - # The adapters' pinned, --locked dependencies build the same way every - # run. Restore their public Cargo registry data and build tree, keyed - # by the toolchain image, the runner and every tracked adapter input. - # Only successful pushes to main save them, so no pull request or merge - # group writes what a later run reads. Every run still builds, stamps - # and hashes its own adapters, and the final adapters are never cached. - - name: Key the adapters' Cargo cache by every tracked adapter input - id: adapter_key - run: | - set -euo pipefail - mapfile -t inputs < <(git ls-files -- \ - scripts/conformance/server-parity/published-sdk-patch \ - scripts/conformance/server-parity/sdk-directions \ - scripts/conformance/server-parity/legacy-marker-aliases \ - scripts/conformance/server-parity/corrected-markers \ - scripts/conformance/server-parity/query-authority \ - ':(glob)scripts/conformance/server-parity/*-adapters.sh' \ - scripts/ci/patched-pip.sh \ - tests/Fixtures/ServerParityProfiles/published-sdk-patch-replay \ - tests/Fixtures/ServerParityProfiles/published-sdk-directions \ - tests/Fixtures/ServerParityProfiles/legacy-marker-aliases \ - tests/Fixtures/ServerParityProfiles/corrected-markers \ - tests/Fixtures/ServerParityProfiles/query-authority \ - tests/Fixtures/ServerParityProfiles/version-range-repair | sort) - # Never an empty key: every one of the five adapter crates and build - # scripts must be among the inputs. A sixth adapter must join here. - test "$(printf '%s\n' "${inputs[@]}" | grep -c '/rust/Cargo.lock$')" -eq 5 - test "$(printf '%s\n' "${inputs[@]}" | grep -c -- '-adapters.sh$')" -eq 5 - digest=$(sha256sum -- "${inputs[@]}" | sha256sum | cut -c1-64) - echo "key=sdk-adapter-cargo-v1-${RUNNER_OS}-${RUNNER_ARCH}-rust-300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081-${digest}" >> "$GITHUB_OUTPUT" - - name: Restore the adapters' Cargo registry and build tree - id: adapter_cargo - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - sdk-cargo/registry - sdk-cargo/git - sdk-target - key: ${{ steps.adapter_key.outputs.key }} - name: Build the locked published SDK adapter once run: bash scripts/conformance/server-parity/published-sdk-patch-adapters.sh - name: Build genuine legacy-marker adapters with the existing cache @@ -196,15 +156,6 @@ jobs: run: bash scripts/conformance/server-parity/corrected-marker-adapters.sh - name: Build the separate current query consumers once run: bash scripts/conformance/server-parity/query-authority-adapters.sh - - name: Save the adapters' Cargo registry and build tree from main - if: ${{ success() && github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.adapter_cargo.outputs.cache-hit != 'true' }} - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: | - sdk-cargo/registry - sdk-cargo/git - sdk-target - key: ${{ steps.adapter_key.outputs.key }} - name: Share current query consumers without compiler caches uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: @@ -1922,7 +1873,7 @@ jobs: bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress docker run --rm --user=1000:1000 --entrypoint node -v "$PWD:/source:ro" -w /source \ - "$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParitySdkDirectionLanesTest.mjs tests/Unit/ServerParityCodecDeadlineCellsTest.mjs tests/Unit/ServerParityCooperativeContractTest.mjs tests/Unit/ServerParityChildCancellationContractTest.mjs tests/Unit/ServerParityScheduleContractTest.mjs tests/Unit/ServerParityVisibilityContractTest.mjs tests/Unit/ServerParityAdmissionContractTest.mjs tests/Unit/ServerParityRoleTokenContractTest.mjs tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs tests/Unit/ServerParitySearchAttributeContractTest.mjs tests/Unit/ServerParityWorkerListingContractTest.mjs tests/Unit/ServerParityWorkerReplacementContractTest.mjs tests/Unit/ServerParityPatchDeploymentContractTest.mjs tests/Unit/ServerParityWaitingHistoryContractTest.mjs tests/Unit/ServerParityTaskRetryContractTest.mjs tests/Unit/ServerParityPublishedPatchContractTest.mjs tests/Unit/ServerParityPublishedPatchTransportTest.mjs tests/Unit/ServerParityRecordFileTest.mjs tests/Unit/ServerParityFailureLocationTest.mjs tests/Unit/ServerParityFailureExcerptTest.mjs tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs tests/Unit/ServerParityScheduleCatalogContractTest.mjs tests/Unit/ServerParityOperatorMetricsContractTest.mjs tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs tests/Unit/ServerParityRecordLocalActivityContractTest.mjs tests/Unit/ServerParityActivityCancellationAckContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParityServiceEndpointsContractTest.mjs tests/Unit/ServerParityServiceCallsContractTest.mjs tests/Unit/ServerParityWorkflowImportContractTest.mjs tests/Unit/ServerParityWaterlineImportContractTest.mjs tests/Unit/ServerParitySystemMetricsContractTest.mjs tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs tests/Unit/ServerParityPrometheusMetricsContractTest.mjs tests/Unit/ServerParityPayloadCleanupContractTest.mjs tests/Unit/ServerParityRepairPassContractTest.mjs tests/Unit/ServerParityClusterInfoContractTest.mjs tests/Unit/ServerParityNamespaceDeleteContractTest.mjs tests/Unit/ServerParityMessageStreamsContractTest.mjs tests/Unit/ServerParityCancellationScopesContractTest.mjs tests/Unit/ServerParityWorkflowPollRequestContractTest.mjs tests/Unit/ServerParityUpdateValidationContractTest.mjs tests/Unit/ServerParityFixtureSelectionTest.mjs tests/Unit/ServerParityUpdateValidationRetryTest.mjs tests/Unit/ServerParityKilledAuthorPollTest.mjs tests/Unit/ServerParityImagePullTest.mjs tests/Unit/ServerParityAdapterCacheTest.mjs tests/Unit/ServerParityWorkerSessionsContractTest.mjs tests/Unit/ServerParityWorkerSessionActivitiesContractTest.mjs tests/Unit/ServerParityNexusOperationsContractTest.mjs tests/Unit/ServerParityBridgeAdaptersContractTest.mjs tests/Unit/ServerParityWorkflowStreamWritesContractTest.mjs > parity-evidence/comparator-tests.txt + "$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParitySdkDirectionLanesTest.mjs tests/Unit/ServerParityCodecDeadlineCellsTest.mjs tests/Unit/ServerParityCooperativeContractTest.mjs tests/Unit/ServerParityChildCancellationContractTest.mjs tests/Unit/ServerParityScheduleContractTest.mjs tests/Unit/ServerParityVisibilityContractTest.mjs tests/Unit/ServerParityAdmissionContractTest.mjs tests/Unit/ServerParityRoleTokenContractTest.mjs tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs tests/Unit/ServerParitySearchAttributeContractTest.mjs tests/Unit/ServerParityWorkerListingContractTest.mjs tests/Unit/ServerParityWorkerReplacementContractTest.mjs tests/Unit/ServerParityPatchDeploymentContractTest.mjs tests/Unit/ServerParityWaitingHistoryContractTest.mjs tests/Unit/ServerParityTaskRetryContractTest.mjs tests/Unit/ServerParityPublishedPatchContractTest.mjs tests/Unit/ServerParityPublishedPatchTransportTest.mjs tests/Unit/ServerParityRecordFileTest.mjs tests/Unit/ServerParityFailureLocationTest.mjs tests/Unit/ServerParityFailureExcerptTest.mjs tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs tests/Unit/ServerParityScheduleCatalogContractTest.mjs tests/Unit/ServerParityOperatorMetricsContractTest.mjs tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs tests/Unit/ServerParityRecordLocalActivityContractTest.mjs tests/Unit/ServerParityActivityCancellationAckContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParityServiceEndpointsContractTest.mjs tests/Unit/ServerParityServiceCallsContractTest.mjs tests/Unit/ServerParityWorkflowImportContractTest.mjs tests/Unit/ServerParityWaterlineImportContractTest.mjs tests/Unit/ServerParitySystemMetricsContractTest.mjs tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs tests/Unit/ServerParityPrometheusMetricsContractTest.mjs tests/Unit/ServerParityPayloadCleanupContractTest.mjs tests/Unit/ServerParityRepairPassContractTest.mjs tests/Unit/ServerParityClusterInfoContractTest.mjs tests/Unit/ServerParityNamespaceDeleteContractTest.mjs tests/Unit/ServerParityMessageStreamsContractTest.mjs tests/Unit/ServerParityCancellationScopesContractTest.mjs tests/Unit/ServerParityWorkflowPollRequestContractTest.mjs tests/Unit/ServerParityUpdateValidationContractTest.mjs tests/Unit/ServerParityFixtureSelectionTest.mjs tests/Unit/ServerParityUpdateValidationRetryTest.mjs tests/Unit/ServerParityKilledAuthorPollTest.mjs tests/Unit/ServerParityImagePullTest.mjs tests/Unit/ServerParityWorkerSessionsContractTest.mjs tests/Unit/ServerParityWorkerSessionActivitiesContractTest.mjs tests/Unit/ServerParityNexusOperationsContractTest.mjs tests/Unit/ServerParityBridgeAdaptersContractTest.mjs tests/Unit/ServerParityWorkflowStreamWritesContractTest.mjs > parity-evidence/comparator-tests.txt docker run --rm --user=1000:1000 --cpus=1 --memory=256m --memory-swap=256m \ --network=none --entrypoint node -v "$PWD:/source:ro" -w /source \ "$image" --test tests/Unit/ServerParityCodecDeadlineLoadTest.mjs > parity-evidence/codec-load-tests.txt diff --git a/tests/Unit/ServerParityAdapterCacheTest.mjs b/tests/Unit/ServerParityAdapterCacheTest.mjs deleted file mode 100644 index 5f21884bc..000000000 --- a/tests/Unit/ServerParityAdapterCacheTest.mjs +++ /dev/null @@ -1,62 +0,0 @@ -// The published SDK adapters' public Cargo registry data and build tree are -// cached between runs, but only successful pushes to main save them: a pull -// request or merge group can restore main's cache and never write one a -// later run reads. The key covers the toolchain, the runner and every -// tracked adapter input, and an empty input list fails the job. -import test from 'node:test'; -import assert from 'node:assert/strict'; -import {readFileSync} from 'node:fs'; -import {resolve} from 'node:path'; - -const workflow = readFileSync(resolve('.github/workflows/server-parity.yml'), 'utf8'); - -function job(name) { - const start = workflow.indexOf(`\n ${name}:\n`); - assert.ok(start >= 0, name); - const next = workflow.slice(start + 1).search(/\n {2}[a-z0-9-]+:\n/); - return next < 0 ? workflow.slice(start) : workflow.slice(start, start + 1 + next); -} - -const steps = block => block.split(/\n {6}(?=- (?:name|uses):)/).slice(1); - -test('only the adapter job caches, and only public registry data and the build tree', () => { - const uses = workflow.match(/uses: actions\/cache[^\s]*/g) ?? []; - assert.deepEqual(uses, [ - 'uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9', - 'uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9', - ]); - const adapters = steps(job('published-sdk-adapters')); - const restore = adapters.findIndex(step => step.includes('actions/cache/restore@')); - const save = adapters.findIndex(step => step.includes('actions/cache/save@')); - const builds = adapters.map((step, index) => [step, index]).filter(([step]) => /-adapters\.sh\n?$/m.test(step)).map(([, index]) => index); - assert.equal(builds.length, 5); - assert.ok(restore >= 0 && restore < Math.min(...builds), 'restored before the first build'); - assert.ok(save > Math.max(...builds), 'saved after the last build'); - for (const index of [restore, save]) { - // Never the whole CARGO_HOME, where credentials would live. - assert.match(adapters[index], /path: \|\n {12}sdk-cargo\/registry\n {12}sdk-cargo\/git\n {12}sdk-target\n/); - assert.match(adapters[index], /key: \$\{\{ steps\.adapter_key\.outputs\.key \}\}$/m); - } - // No restore-keys: a run gets this exact key's tree or builds from nothing. - assert.doesNotMatch(adapters[restore], /restore-keys/); - assert.match(adapters[save], /if: \$\{\{ success\(\) && github\.event_name == 'push' && github\.ref == 'refs\/heads\/main' && /); -}); - -test('the cache key follows the toolchain, the runner and every tracked adapter input', () => { - const block = job('published-sdk-adapters'); - const key = steps(block).find(step => step.includes('id: adapter_key')); - assert.ok(key, 'a key step'); - const [image] = block.match(/mirror\.gcr\.io\/library\/rust@sha256:([0-9a-f]{64})/).slice(1); - assert.ok(key.includes('sdk-adapter-cargo-v1-${RUNNER_OS}-${RUNNER_ARCH}-rust-' + image + '-${digest}'), 'key names the runner and the pulled Rust image'); - assert.match(key, /git ls-files --/); - for (const adapter of ['published-sdk-patch', 'sdk-directions', 'legacy-marker-aliases', 'corrected-markers', 'query-authority']) { - assert.ok(key.includes(`scripts/conformance/server-parity/${adapter} \\\n`), adapter); - } - assert.ok(key.includes("':(glob)scripts/conformance/server-parity/*-adapters.sh'")); - // An empty or partial input list fails instead of keying a cache. - assert.match(key, /grep -c '\/rust\/Cargo\.lock\$'\)" -eq 5/); - assert.match(key, /grep -c -- '-adapters\.sh\$'\)" -eq 5/); - // The five build scripts are the job's adapter build steps. - const builds = steps(block).filter(step => /-adapters\.sh\n?$/m.test(step)); - assert.equal(builds.length, 5); -}); From d0108c708846da5961d0575e3a30f19200994276 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:09:44 +0000 Subject: [PATCH 7/7] Make native's maintenance repair loop opt-in; count 2.11.3's extra table PR #797's first full run showed that a default-on repair loop changes what native does beside a PHP server container. A PHP server never repairs on its own; PHP's separate scheduler container does. With the loop on, native repaired a task the corrected-marker cohort expects untouched, repaired what the deadline-health audit expects left alone, and raced repair_status's staged candidates. The binary now takes the scheduler's role only with DW_RUST_MAINTENANCE_LOOP=1, which the native image will set. DW_V2_MATCHING_ROLE_QUEUE_WAKE=false still turns it off. The php2113 upgrade cohort expected PHP's 50 tables. PHP 2.11.3 has 51, because the service-call dispatch outbox is new, so the expected count follows the layout. Co-Authored-By: Claude Opus 5.5 --- .../2026-10-26-maintenance-repair.md | 22 +++++++++++++------ rust/src/runtime/execution.rs | 14 +++++++----- rust/tests/execution/repair_pass.rs | 9 +++++--- .../server-parity/upgrade-source-profile.sh | 9 +++++--- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/docs/rust-server-port-checkpoints/2026-10-26-maintenance-repair.md b/docs/rust-server-port-checkpoints/2026-10-26-maintenance-repair.md index 050f0fb9e..3115dd493 100644 --- a/docs/rust-server-port-checkpoints/2026-10-26-maintenance-repair.md +++ b/docs/rust-server-port-checkpoints/2026-10-26-maintenance-repair.md @@ -16,17 +16,25 @@ `POST /api/system/repair/pass`. So an idle run past its execution or run deadline was never given the task that closes it, and overdue ready tasks and due service calls waited for a request. -- **Change.** The ticker now also runs the repair pass with PHP's loop +- **Change.** The ticker can now also run the repair pass with PHP's loop throttle: at most once per 5 seconds, shared with requested passes that - respect it. `DW_V2_MATCHING_ROLE_QUEUE_WAKE=false` (or `0`) turns it off, as - it does PHP's. `Runtime::set_maintenance_repair` does the same for one - runtime. + respect it. + - **Opt-in.** It runs only with `DW_RUST_MAINTENANCE_LOOP=1`, which the + native image will set. A PHP server container never repairs on its own: + PHP's separate `scheduler` container does. So the bare binary keeps the + server role, and the Shared Server fixtures, which pair each native node + with a PHP server container, compare like with like. + - On PR #797's first run, a default-on loop repaired a task the corrected + marker cohort expects untouched, and raced `repair_status`'s staged + candidates. + - `DW_V2_MATCHING_ROLE_QUEUE_WAKE=false` (or `0`) still turns it off, as it + does PHP's. + - `Runtime::set_maintenance_repair` sets it for one runtime. - **Tests.** - `the_maintenance_loop_gives_an_overdue_idle_run_its_deadline_task` waits for the loop alone to give an idle run past its deadline its workflow task. - - The two tests that count one requested pass, or keep expired leases until - they retry, turn the loop off. - - The execution suite passes with the loop on (558). + - The execution suite passes with the loop off by default, and passed with + it on (558). - Native passes, with the loop on, the shared fixtures most exposed to background repair: repair-pass, the lease, cancellation and deadline fixtures, worker sessions and service calls. diff --git a/rust/src/runtime/execution.rs b/rust/src/runtime/execution.rs index a75b78606..56e20d1e4 100644 --- a/rust/src/runtime/execution.rs +++ b/rust/src/runtime/execution.rs @@ -52,15 +52,19 @@ struct TimerScheduler { stop: watch::Sender, task: Mutex>>, healthy: Arc, - /// Whether the ticker also runs the repair pass, as PHP's maintenance - /// loop does unless `DW_V2_MATCHING_ROLE_QUEUE_WAKE` is false or 0. + /// Whether the ticker also runs the repair pass (`maintenance_repair_enabled`). repair: Arc, } -/// PHP's maintenance loop skips `workflow:v2:repair-pass` when -/// `DW_V2_MATCHING_ROLE_QUEUE_WAKE` is `false` or `0`. +/// Whether the ticker runs the repair pass, as PHP's separate `scheduler` +/// container does. A PHP server container never repairs on its own, so the +/// native binary takes that role only when asked: `DW_RUST_MAINTENANCE_LOOP=1` +/// (the native image sets it, having no separate scheduler). +/// `DW_V2_MATCHING_ROLE_QUEUE_WAKE=false` or `0` still turns it off, as it +/// turns off PHP's. fn maintenance_repair_enabled() -> bool { - !matches!(std::env::var("DW_V2_MATCHING_ROLE_QUEUE_WAKE").as_deref(), Ok("false" | "0")) + matches!(std::env::var("DW_RUST_MAINTENANCE_LOOP").as_deref(), Ok("1" | "true")) + && !matches!(std::env::var("DW_V2_MATCHING_ROLE_QUEUE_WAKE").as_deref(), Ok("false" | "0")) } impl TimerScheduler { diff --git a/rust/tests/execution/repair_pass.rs b/rust/tests/execution/repair_pass.rs index 30fe24a28..03d0401ed 100644 --- a/rust/tests/execution/repair_pass.rs +++ b/rust/tests/execution/repair_pass.rs @@ -177,14 +177,17 @@ async fn repair_pass_creates_deadline_tasks_settles_leases_and_reports_failures( assert_eq!(invalid["errors"]["queue"], json!(["The queue field must not be greater than 128 characters."])); } -// PHP's maintenance loop runs the repair pass every few seconds; native's -// ticker does the same unless the pass is turned off. An idle run past its +// PHP's scheduler container runs the repair pass every few seconds; native's +// ticker does the same when it takes that role. An idle run past its // execution deadline gets the workflow task that closes it without anyone // asking for a pass. #[tokio::test] async fn the_maintenance_loop_gives_an_overdue_idle_run_its_deadline_task() { let database = TestDatabase::new().await; - let app = router(database.open().await.unwrap()); + let runtime = database.open().await.unwrap(); + // `DW_RUST_MAINTENANCE_LOOP=1`, as the native image sets it. + runtime.set_maintenance_repair(true); + let app = router(runtime); let deadline = started_on(&app, "maintained", "repair-m").await; let deadline = deadline["run_id"].as_str().unwrap(); execute(&database, format!("UPDATE workflow_tasks SET status='completed' WHERE workflow_run_id='{deadline}'")).await; diff --git a/scripts/conformance/server-parity/upgrade-source-profile.sh b/scripts/conformance/server-parity/upgrade-source-profile.sh index 6d869e1f3..39529a1fe 100755 --- a/scripts/conformance/server-parity/upgrade-source-profile.sh +++ b/scripts/conformance/server-parity/upgrade-source-profile.sh @@ -134,7 +134,10 @@ snapshot() { } snapshot > "$evidence/before.json" inspect_source > "$evidence/inspection.json" 2> "$evidence/inspection.stderr" -jq -e --arg layout "$layout" '.status=="inspected" and .source_layout==$layout and (.table_rows|length)==50 and .table_rows.workflow_runs==4 +# PHP 2.11.3 adds one table, the service-call dispatch outbox. +tables=50 +if [[ $layout == php-2.11.3* ]]; then tables=51; fi +jq -e --arg layout "$layout" --argjson tables "$tables" '.status=="inspected" and .source_layout==$layout and (.table_rows|length)==$tables and .table_rows.workflow_runs==4 and .table_rows.activity_executions==1 and .catalog_visibility=="configured_role" and .source_mutated==false and .takeover_allowed==false and .backup_verified==false and .php_quiescence_verified==false and .stored_representation_compatibility_verified==false' "$evidence/inspection.json" >/dev/null @@ -165,8 +168,8 @@ fingerprint() { --entrypoint /target/debug/durable-workflow-server "$rust_image" upgrade-fingerprint } fingerprint "$database" > "$evidence/fingerprint.json" 2> "$evidence/fingerprint.stderr" -jq -e --arg layout "$layout" '.status=="fingerprinted" and .source_layout==$layout and (.fingerprint|test("^sha256:[0-9a-f]{64}$")) - and (.table_rows|length)==50 and .table_rows.workflow_runs==4 and .table_rows.jobs==1 +jq -e --arg layout "$layout" --argjson tables "$tables" '.status=="fingerprinted" and .source_layout==$layout and (.fingerprint|test("^sha256:[0-9a-f]{64}$")) + and (.table_rows|length)==$tables and .table_rows.workflow_runs==4 and .table_rows.jobs==1 and .source_mutated==false and .takeover_allowed==false and .backup_verified==false and .php_quiescence_verified==false and .stored_representation_compatibility_verified==false' "$evidence/fingerprint.json" >/dev/null source_fingerprint=$(jq -r .fingerprint "$evidence/fingerprint.json")