Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
40a55cd
Server parity: bridge adapter webhooks fixture, recorded from publish…
rmcdaniel Oct 24, 2026
ab5964c
Name the bridge update defect's issue
rmcdaniel Oct 24, 2026
176a5ef
Rust #325: serve bridge adapter webhooks like published 2.11.1
rmcdaniel Oct 24, 2026
928f287
Merge remote-tracking branch 'origin/main' (#729 through #759) into r…
rmcdaniel Oct 25, 2026
b8e8c89
Size the namespace access table for both new families
rmcdaniel Oct 25, 2026
2f273b1
Merge remote-tracking branch 'origin/main' (#764 through #766) into r…
rmcdaniel Oct 26, 2026
ae97a3d
Server parity: declare the bridge fixture's update validators
rmcdaniel Oct 26, 2026
4f31a8a
Merge remote-tracking branch 'origin/main' (#771) into rust-325-bridg…
rmcdaniel Oct 26, 2026
e8be9ab
Merge remote-tracking branch 'origin/main' (#772) into rust-325-bridg…
rmcdaniel Oct 26, 2026
5877358
Rust #325: find a duplicate bridge command in the database, one row a…
rmcdaniel Oct 26, 2026
87c76c9
Merge branch 'fix/775-update-wait-maximum' (#775) into rust-325-bridg…
rmcdaniel Oct 26, 2026
b480d7f
Rust #325: bridge commands deduplicate atomically under a bridge iden…
rmcdaniel Oct 26, 2026
23dfe99
Merge branch 'fix/775-update-wait-maximum' (#775) into rust-325-bridg…
rmcdaniel Oct 26, 2026
e20bb44
Merge branch 'fix/775-update-wait-maximum' (#775) into rust-325-bridg…
rmcdaniel Oct 26, 2026
867a2d5
Merge remote-tracking branch 'origin/main' (#780) into rust-325-bridg…
rmcdaniel Oct 26, 2026
34134f5
Rust #325: bridge identities fail closed for any status; starts decid…
rmcdaniel Oct 26, 2026
bfd0ac6
Rust #325: prove the bridge start decision with the real lock queue
rmcdaniel Oct 26, 2026
e29ca32
Rust #325: assert the competitor's win and state where SQLite lets it…
rmcdaniel Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/server-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ on:
- tests/Unit/ServerParityWorkerReplacementContractTest.mjs
- tests/Unit/ServerParityOperatorMetricsContractTest.mjs
- tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs
- tests/Unit/ServerParityBridgeAdaptersContractTest.mjs
- tests/Unit/ServerParityNexusOperationsContractTest.mjs
- tests/Unit/ServerParityRecordLocalActivityContractTest.mjs
- tests/Unit/ServerParityWorkflowPollRequestContractTest.mjs
Expand Down Expand Up @@ -1805,7 +1806,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/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 > 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/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 > 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
Expand Down
7 changes: 7 additions & 0 deletions app/Http/Controllers/Api/BridgeAdapterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@

class BridgeAdapterController
{
/**
* Bridge commands' identities own the `dw-bridge:` request id namespace
* (Server #777), so a caller's signal or update request id may not use
* it. MySQL compares request ids without regard to case.
*/
public const UNRESERVED_REQUEST_ID = 'not_regex:/^\s*dw-bridge:/i';

public function __construct(
private readonly WorkflowStartService $workflowStartService,
private readonly WorkflowControlPlane $workflowControlPlane,
Expand Down
4 changes: 2 additions & 2 deletions app/Http/Controllers/Api/WorkflowController.php
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ public function signal(Request $request, string $workflowId, string $signalName)

$validated = $request->validate([
'input' => ['nullable', 'array'],
'request_id' => ['nullable', 'string', 'max:255'],
'request_id' => ['nullable', 'string', 'max:255', BridgeAdapterController::UNRESERVED_REQUEST_ID],
'dry_run' => ['nullable', 'boolean'],
]);

Expand Down Expand Up @@ -749,7 +749,7 @@ public function update(Request $request, string $workflowId, string $updateName)

$validated = $request->validate([
'input' => ['nullable', 'array'],
'request_id' => ['nullable', 'string', 'max:255'],
'request_id' => ['nullable', 'string', 'max:255', BridgeAdapterController::UNRESERVED_REQUEST_ID],
'wait_for' => ['nullable', 'string', 'in:accepted,completed'],
'wait_timeout_seconds' => ['nullable', 'integer', 'min:1', 'max:'.ControlPlaneRequestContract::UPDATE_WAIT_TIMEOUT_SECONDS_MAX],
]);
Expand Down
28 changes: 22 additions & 6 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ Updated October 24, 2026.
([#737](https://github.com/durable-workflow/server/issues/737)).

No expectation is edited to make native pass.
- **Parity surface.** Native serves 145 of the 175 API route methods in
- **Parity surface.** Native serves 146 of the 175 API route methods in
published 2.11.1's route table, with message streams, update validation,
activity cancellation acknowledgement, worker sessions, workflow-task
cancellation scopes, caller-indexed Nexus operation reads, and embedded v2
and Waterline v1 workflow import. An execution test sends each one through
the native router, and the ones
still answering `501 rust_capability_not_implemented` must match
cancellation scopes, caller-indexed Nexus operation reads, bridge adapter
webhooks, and embedded v2 and Waterline v1 workflow import. An execution
test sends each one through the native router, and the ones still
answering `501 rust_capability_not_implemented` must match
[`unserved-php-routes.v2.11.1.txt`](../rust/tests/fixtures/unserved-php-routes.v2.11.1.txt).
Unlike published PHP, native's embedded v2 import refuses a bundle whose
rows reference rows outside it, links only runs of one namespace, and reads
Expand Down Expand Up @@ -169,7 +169,23 @@ any other unknown path or method answers PHP's own 404 or 405
`unsupported_workflow_command`. When it does, an operation bound to a
workflow update must refuse a `wait_timeout_seconds` above the update
ceiling of 60 before it runs, as PHP does (Server #775).)
- Bridge adapters.
- (Bridge adapter webhooks are served: `POST /api/bridge-adapters/webhook/{adapter}`,
with the `bridge-adapters` fixture. Native admits bridge updates as the
regular update route does; published 2.11.1 skips that admission, Server #747.
Four further differences from published 2.11.1, all tracked in Server #777:
- A start for an id that another namespace holds is refused as unavailable,
naming neither the type nor the namespace. The draining gate and the
instance reservation are decided under the transition lock, before
anything is written.
- The adapter name is at most 64 bytes.
- Signals and updates are deduplicated inside their own transaction under
the transition lock. Each one is recorded with a fixed-length
`dw-bridge:v1:` identity as its request id, and the commands PHP
recorded before takeover are matched by their metadata. A command
under an identity, accepted or rejected, that records another tuple is
refused `409 bridge_identity_conflict`.
- The regular signal and update routes refuse caller request ids that
start with `dw-bridge:`.)
- (Caller-indexed Nexus operation reads are served, with the
`nexus-operations` fixture. Native writes no service calls and takeover
refuses a source that holds any, so every list is PHP's empty answer; an
Expand Down
9 changes: 9 additions & 0 deletions rust/src/runtime/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ pub(super) trait Backend: Database {
const SEARCH_ATTRIBUTE_DELETE: &str = "DELETE FROM workflow_search_attributes WHERE workflow_run_id=$1 AND \"key\"=$2";
const SEARCH_ATTRIBUTE_KEY_ROWS: &str = "SELECT a.workflow_run_id,a.\"key\" AS attribute_key,a.type,a.value_string,a.value_keyword,a.value_int,a.value_float,CASE WHEN a.value_bool IS NULL THEN NULL WHEN a.value_bool THEN 1 ELSE 0 END AS value_bool,a.value_datetime,a.value_keyword_list FROM workflow_search_attributes a JOIN workflow_runs r ON r.id=a.workflow_run_id WHERE r.namespace=$1 AND a.\"key\"=$2";
const SEARCH_ATTRIBUTE_INHERIT: &str = "INSERT INTO workflow_search_attributes(workflow_run_id,workflow_instance_id,\"key\",type,value_string,value_keyword,value_keyword_list,value_int,value_float,value_bool,value_datetime,upserted_at_sequence,inherited_from_parent,created_at,updated_at) SELECT $1,$2,\"key\",type,value_string,value_keyword,value_keyword_list,value_int,value_float,value_bool,value_datetime,1,true,$3,$4 FROM workflow_search_attributes WHERE workflow_run_id=$5";
// `duplicateBridgeCommand`: the latest accepted command PHP recorded for
// this adapter, action, key and target name ($6 names the target field),
// filtered in the database as PHP's JSON-path query is. PHP's bridge
// commands carry no identity: no request id for a signal, the raw key
// ($8) for an update.
const BRIDGE_COMMAND: &str = "SELECT workflow_run_id,resolved_workflow_run_id FROM workflow_commands WHERE workflow_instance_id=$1 AND command_type=$2 AND status='accepted' AND context->'server'->'metadata'->>'adapter'=$3 AND context->'server'->'metadata'->>'action'=$4 AND context->'server'->'metadata'->>'idempotency_key'=$5 AND context->'server'->'metadata'->>$6=$7 AND (request_id IS NULL OR request_id=$8) ORDER BY accepted_at DESC,command_sequence DESC LIMIT 1";
fn statement(sql: &'static str) -> Cow<'static, str> {
Cow::Borrowed(sql)
}
Expand Down Expand Up @@ -191,6 +197,7 @@ WHERE r.status IN ('pending','running','waiting')
ORDER BY t.available_order,t.id LIMIT 100
"#;
const SELECTION_RESOLUTION_SQL: &str = "SELECT id,payload FROM workflow_history_events WHERE workflow_run_id=$1 AND event_type='SelectionResolved' AND json_extract(payload,'$.selection_group_id')=$2 ORDER BY sequence LIMIT 1";
const BRIDGE_COMMAND: &str = "SELECT workflow_run_id,resolved_workflow_run_id FROM workflow_commands WHERE workflow_instance_id=$1 AND command_type=$2 AND status='accepted' AND json_extract(context,'$.server.metadata.adapter')=$3 AND json_extract(context,'$.server.metadata.action')=$4 AND json_extract(context,'$.server.metadata.idempotency_key')=$5 AND json_extract(context,'$.server.metadata.'||$6)=$7 AND (request_id IS NULL OR request_id=$8) ORDER BY accepted_at DESC,command_sequence DESC LIMIT 1";
const REPAIR_COUNTER_MAX: i64 = i64::MAX;
const RANGE_REPAIR_TASK_SQL: &str = "SELECT * FROM workflow_tasks WHERE workflow_run_id=$1 AND namespace=$2 AND task_type='workflow' AND status='failed' AND json_extract(payload,'$.replay_blocked')=1 ORDER BY CASE WHEN available_at IS NULL THEN 1 ELSE 0 END,available_at,created_at,id LIMIT 1";
const ACTIVITY_OPEN_TASK_SQL: &str = "SELECT * FROM workflow_tasks WHERE workflow_run_id=$1 AND task_type='activity' AND status IN ('ready','leased') AND json_extract(payload,'$.activity_execution_id')=$2 ORDER BY id LIMIT 1";
Expand Down Expand Up @@ -464,6 +471,7 @@ impl Backend for MySql {
const MEMO_INHERIT_INSERT: &str = "INSERT INTO workflow_memos(workflow_run_id,workflow_instance_id,`key`,value,upserted_at_sequence,inherited_from_parent,created_at,updated_at,portable_value,portable_value_sequence) VALUES ($1,$2,$3,$4,1,true,$5,$6,$7,1)";
const MEMO_READ: &str = "SELECT `key`,value,portable_value,portable_value_sequence,upserted_at_sequence FROM workflow_memos WHERE workflow_run_id=$1 ORDER BY `key`,id LIMIT 101";
const SELECTION_RESOLUTION_SQL: &str = "SELECT id,payload FROM workflow_history_events WHERE workflow_run_id=? AND event_type='SelectionResolved' AND JSON_UNQUOTE(JSON_EXTRACT(payload,'$.selection_group_id'))=? ORDER BY sequence LIMIT 1";
const BRIDGE_COMMAND: &str = "SELECT workflow_run_id,resolved_workflow_run_id FROM workflow_commands WHERE workflow_instance_id=? AND command_type=? AND status='accepted' AND BINARY JSON_UNQUOTE(JSON_EXTRACT(context,'$.server.metadata.adapter'))=BINARY ? AND BINARY JSON_UNQUOTE(JSON_EXTRACT(context,'$.server.metadata.action'))=BINARY ? AND BINARY JSON_UNQUOTE(JSON_EXTRACT(context,'$.server.metadata.idempotency_key'))=BINARY ? AND BINARY JSON_UNQUOTE(JSON_EXTRACT(context,CONCAT('$.server.metadata.',?)))=BINARY ? AND (request_id IS NULL OR BINARY request_id=BINARY ?) ORDER BY accepted_at DESC,command_sequence DESC LIMIT 1";
const REPAIR_COUNTER_MAX: i64 = u32::MAX as i64;
const RANGE_REPAIR_TASK_SQL: &str = "SELECT * FROM workflow_tasks WHERE workflow_run_id=? AND namespace=? AND task_type='workflow' AND status='failed' AND JSON_UNQUOTE(JSON_EXTRACT(payload,'$.replay_blocked'))='true' ORDER BY CASE WHEN available_at IS NULL THEN 1 ELSE 0 END,available_at,created_at,id LIMIT 1";
const ACTIVITY_OPEN_TASK_SQL: &str = "SELECT * FROM workflow_tasks WHERE workflow_run_id=? AND task_type='activity' AND status IN ('ready','leased') AND JSON_UNQUOTE(JSON_EXTRACT(payload,'$.activity_execution_id'))=? ORDER BY id LIMIT 1";
Expand Down Expand Up @@ -509,6 +517,7 @@ impl Backend for MySql {
|| sql == Self::SIGNAL_RECEIVED_SEQUENCE_SQL
|| sql == Self::RANGE_REPAIR_TASK_SQL
|| sql == Self::SELECTION_RESOLUTION_SQL
|| sql == Self::BRIDGE_COMMAND
|| sql == Self::ACTIVITY_OPEN_TASK_SQL
|| sql == Self::LOCAL_ACTIVITY_EXECUTIONS
|| sql == Self::LOCAL_ACTIVITY_ATTEMPTS
Expand Down
Loading
Loading