Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -61,6 +61,7 @@ on:
- tests/Unit/ServerParityContinueTransfersContractTest.mjs
- tests/Unit/ServerParityStartSearchAttributesContractTest.mjs
- tests/Unit/ServerParityQueryArgumentsContractTest.mjs
- tests/Unit/ServerParityQueryContractArgumentsContractTest.mjs
- tests/Unit/ServerParityParentCloseCancellationContractTest.mjs
- tests/Unit/ServerParityBridgeAdaptersContractTest.mjs
- tests/Unit/ServerParityNexusOperationsContractTest.mjs
Expand Down Expand Up @@ -1984,7 +1985,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/ServerParityConcurrentWaitsContractTest.mjs tests/Unit/ServerParityContinueTransfersContractTest.mjs tests/Unit/ServerParityStartSearchAttributesContractTest.mjs tests/Unit/ServerParityQueryArgumentsContractTest.mjs tests/Unit/ServerParityParentCloseCancellationContractTest.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/ServerParityConcurrentWaitsContractTest.mjs tests/Unit/ServerParityContinueTransfersContractTest.mjs tests/Unit/ServerParityStartSearchAttributesContractTest.mjs tests/Unit/ServerParityQueryArgumentsContractTest.mjs tests/Unit/ServerParityQueryContractArgumentsContractTest.mjs tests/Unit/ServerParityParentCloseCancellationContractTest.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
named arguments (`WorkflowQueryContract::normalizeNamedArguments`), and
the worker receives the positional result. Native still refuses a map as
"must decode to an array". The port log records the gap (found in
review).
review); the query-contract-arguments slice closes it.
- **No declared queries.** The query is routed with its arguments as
given. A query-capable worker leases a task carrying the blob unchanged,
and the client receives the worker's answer.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# October 28, named and validated query arguments

- **Before.** For a declared query with a parameter contract, native
differed from published PHP in four ways:
- it checked positional arguments only;
- it refused a named-argument map as "must decode to an array";
- it answered every violation with a bare `invalid_query_arguments`;
- it refused some contracts as `unsupported_query_contract` (defaults,
and declared types other than a few scalars).

The worker then received the arguments exactly as the client sent them.
- **Contract** (published 2.11.5, Core 2.15.1;
`WorkflowQueryTaskBroker::validateContractedQueryArguments`,
`WorkflowQueryContract::validatedArgumentsForRun`,
`normalizePositionalArguments`, `normalizeNamedArguments`,
`valueMatchesDeclaredType`):
- **Arguments.** An Avro array is a positional list. A non-empty map
with no key PHP casts to an integer is a PHP array of named arguments.
`Avro::fromDatum` returns any other map (empty, or with a canonical
decimal integer key) as an `AvroMapValue` object, which is not an array.
At the top level it is refused before normalization with "must decode to
an array" (found in review).
- **Positional.** Values fill the parameters in order. A variadic
parameter takes the rest. A missing argument takes its default or is
reported if required. Extra values are reported under `arguments`.
- **Named.** Each parameter takes its name's value. A variadic parameter
spreads a list or a map's values. Defaults and required arguments are
handled as for positional ones. Each unknown name is reported under
itself.
- **Values.** A null is refused unless the parameter allows it. A value
must match the declared type: nullable, union, intersection, `int`,
`float` (an int too), `string`, `bool`, `array`, `iterable`, `scalar`,
`true`, `false`, `null`, `mixed`. An adapter map is not an `array`. A
class or `object` type matches only an object: decoded bytes or an
adapter map. Neither object is `callable`. A named variadic spreads a
PHP array's values in wire order and keeps any other value, an adapter
map included, as one argument (found in review).
- **Refusal.** Any violation answers `422 invalid_query_arguments`, with
the message "Workflow query [<name>] argument validation failed." and
`validation_errors` by argument.
- **Worker.** Otherwise the worker receives the normalized positional
list, encoded again with the request's codec.
- **No parameter contract.** A declared query without one takes a list as
given and refuses named arguments: "Named arguments require a durable
or loadable workflow query contract." (a native server never loads a
workflow class).
- **Fixture.** `query-contract-arguments`, raw HTTP on protocol 1.20 in a
fresh namespace. The worker declares `state(string $prefix)`,
`greet(string $prefix = 'dflt')`, `plain`, `arr(array $value)`,
`obj(object $value)` and `rest(mixed ...$value)`, and three `mixed`
queries whose registered defaults are raw JSON objects. There are
twenty-two cases:
- named, positional, default and named-default queries are routed, and
the worker's blob is checked;
- unknown, missing, wrong-type, null, too-many and named-without-contract
queries are refused, with their messages checked;
- an empty map, a `{"0": "ok"}` map and a mixed-key map are refused as not
arrays;
- a nested empty map is refused by `array`, but routed whole by `object`
and by the variadic `mixed`;
- `{value: {z: 20, a: 10}}` reaches the variadic `rest` as `[20, 10]`;
- a registered default `{"0": "x", "1": "y"}`, sent as a JSON object
(the probe records the bytes it sends), reaches the worker as the list
`[["x", "y"]]`. On the previous source native routed it as a map;
- a registered default `{"0": "x", "k": "y"}` or `{"1": "x"}` fails
published PHP's Avro encoder (`invalid_map_key`) with an uncaught 500.
The fixture pins only that status.
- **Native.** `query_contract.rs` ports the normalization and type rules
over `codec::OrderedValue`. Apache's generic reader keeps maps unordered,
so the new `codec/wire_order.rs` reads the bytes the public reader has
already validated again through a private binding schema. That schema
spells each map as an array of key/value records, as PHP's
`ValueDatumWriter` does, so the binary layout is the same and Apache still
reads every primitive and block. Writing goes through the same binding,
under the public header, and the public reader checks the result. For every
fixture blob the bytes match PHP's.
A registered default takes PHP's array shape: canonical integer keys
0 to n-1 make a list, string keys a map, and any other integer key is
answered 500 `query_default_unencodable`, naming the parameter.
Violations answer first, as PHP encodes only valid arguments.
`check_query_contract` returns the normalized list for a declared query,
or the refusal. `query_attempt` enqueues the re-encoded blob.
- **Kept.**
- A registered default map keeps its keys in sorted order, not the
registered order: native's JSON objects do not preserve order
(`serde_json` without `preserve_order`). The same limit makes
`{"1": "a", "0": "b"}` a list on native, where PHP's array is not a list
and fails to encode.
- `callable` matches nothing here: PHP's `is_callable` also accepts
strings naming functions, which a native server cannot resolve.
- A class name that a published PHP server can load (a real class) would
refuse other objects. Native, loading no classes, accepts any object.
- **Tests.** `query_contract_arguments.rs` (routed normalization; refusals
with messages) fails on the previous source. `query_contract.rs` unit
tests cover the type rules and integer-keyed maps.
49 changes: 35 additions & 14 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,20 +275,6 @@ contract fixture before its correction.
each priority tier across fairness keys by their weights
(`TaskFairnessScheduler`); native stores the keys and weights but does not
rebalance yet.
- **Named and validated query arguments.** For a declared query with a
durable parameter contract, published PHP normalizes the arguments through
`WorkflowQueryContract::validatedArgumentsForRun`, then gives the worker the
re-encoded positional result:
- a string-keyed map is taken as named arguments;
- positional arguments are matched to the parameters;
- defaults are filled in, variadics collected, and unknown names
reported;
- a violation answers `422 invalid_query_arguments`, "argument validation
failed", with per-parameter messages in `validation_errors`.

Native checks positional arguments only. It refuses a map as "must decode
to an array", reports a violation as a bare `invalid_query_arguments`, and
gives the worker the arguments as sent.
- **When an external payload reference is claimed.** Published PHP retains
a referenced object once it fetches the bytes, before the command is
recorded, so a refused command still retains it. Native claims it in the
Expand Down Expand Up @@ -552,6 +538,29 @@ contract fixture before its correction.
build-id rollout, else the single build id among the live workers for the
type, else among all the queue's live workers (`start_in`,
`deployments.rs`, `build_pins.rs`). The open-gap entry predated #696.
- **Named and validated query arguments** (October 28, closed). For a
declared query, published PHP matches the arguments to the query's durable
parameter contract (`WorkflowQueryContract::validatedArgumentsForRun`):
- a string-keyed map is taken as named arguments;
- positional arguments are matched in order;
- defaults are filled in and variadics collected;
- unknown names, missing required arguments, nulls, wrong types and extra
arguments are each reported. Declared types may be nullable, unions,
intersections, scalars, `array`, `object` or class names.

A violation answers `422 invalid_query_arguments`, "argument validation
failed", with per-argument messages in `validation_errors`. The worker
receives the normalized positional list, encoded again. A declared query
without a parameter contract takes a list as given and refuses named
arguments. Native now does the same (`query_contract.rs`). It used to
check positional arguments only, refuse a map, report a violation as a
bare `invalid_query_arguments`, and refuse some contracts as unsupported.
`Avro::fromDatum` returns an empty or integer-keyed map as an object, not
an array, and a named variadic map spreads in wire order. A registered
JSON default takes PHP's array shape. The `query-contract-arguments`
fixture records twenty-two cases against
published 2.11.5
([checkpoint](rust-server-port-checkpoints/2026-10-28-query-contract-arguments.md)).
- **Trace context (Core 2.10, published in Server 2.11.1).** Core adds
nullable `observability_context` columns on runs, commands and tasks.
Native has no carrier: it accepts and ignores a top-level carrier on
Expand Down Expand Up @@ -647,6 +656,18 @@ contract fixture before its correction.
- **Command route names.** Published PHP records each request's route name in
its command context, and its unnamed workflow routes get a name generated
per deployment. Native records names only for the named schedule routes.
- **Registered default key order.** A worker registers query parameter
defaults as JSON. PHP keeps each object's keys in the registered order,
and a default map reaches the worker in that order. Native's JSON objects
keep keys sorted (`serde_json` without `preserve_order`), so:
- a named default map such as `{"z": 20, "a": 10}` reaches the worker in
sorted order;
- `{"1": "a", "0": "b"}` becomes a list on native, where PHP's array is
not a list and its encoder fails with a 500.

Both need registered-order JSON in native contracts and a fixture case
each; this remains open #325 parity work
([checkpoint](rust-server-port-checkpoints/2026-10-28-query-contract-arguments.md)).

## Contract and verification inventory

Expand Down
2 changes: 2 additions & 0 deletions rust/src/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::collections::BTreeMap;
use std::io::{self, Read};
mod ordered;
mod validation;
mod wire_order;
pub use wire_order::OrderedValue;

use apache_avro::{
GenericSingleObjectReader, GenericSingleObjectWriter, Schema, types::Value as Datum,
Expand Down
Loading
Loading