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
7 changes: 4 additions & 3 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,10 @@ contract fixture before its correction.
`cluster-capabilities` fixture qualifies the activity result read against
published 2.11.7. Native advertises no manifest it does not publish and no
response compression. `native_unqualified` names each member it still
leaves out: `supported_sdk_versions`, `client_compatibility`, `topology`,
the diagnostics view, the external payload transaction limit and the
conformance manifests.
leaves out: `topology`, the diagnostics view, the external payload
transaction limit and the conformance manifests. Since server#1005 (3.1.0)
native serves `supported_sdk_versions` and `client_compatibility` from one
source with its own qualified ranges, citing only the protocols it serves.
- **Fairness within a priority tier** (October 31, closed). Published PHP
tries each batch of a poll's candidates (10 at the default
`max_tasks_per_poll`) in `TaskFairnessScheduler` order: fairness classes
Expand Down
106 changes: 104 additions & 2 deletions rust/src/runtime/cluster_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,68 @@ pub(super) fn discovery() -> Value {
"task_repair_path": "/api/system/repair"})
}

/// Native-qualified client ranges: the single source of both
/// `supported_sdk_versions` and `client_compatibility.clients`, so the two can
/// never disagree (server#1005). Each lower bound is the oldest release that
/// passed installed acceptance against native; the cluster-capabilities
/// fixture records the source of each. The upper bound is the next major.
const CLIENT_RANGES: [(&str, &str, &str); 4] = [
("php", "sdk-php", ">=3.8.1,<4.0.0"),
("python", "sdk-python", ">=2.13.0,<3.0.0"),
("rust", "sdk-rust", ">=4.10.3,<5.0.0"),
("cli", "cli", ">=2.0.0,<3.0.0"),
];

/// `supported_sdk_versions`, keyed as published PHP keys it.
pub(super) fn supported_sdk_versions() -> Value {
Value::Object(
CLIENT_RANGES
.iter()
.map(|(key, _, range)| ((*key).to_owned(), json!(range)))
.collect(),
)
}

/// `client_compatibility` (published PHP's `ClientCompatibility::info`, schema
/// version 2), citing only protocols native serves: the auth composition
/// contract, the control plane with its request contract, and the worker
/// protocol. Their identities are read from the values cluster info serves, so
/// the block cannot cite one native does not. PHP's skew refusal matrix and
/// external execution contracts are not published by native, so they are not
/// cited: the CLI checks every protocol the block requires.
pub(super) fn client_compatibility(auth_composition: &Value, request_contract: &Value) -> Value {
let identity = |contract: &Value| json!({"schema": contract["schema"], "version": contract["version"]});
let served = ["auth_composition.version", "control_plane.version", "control_plane.request_contract"];
let mut clients = serde_json::Map::new();
// PHP's order: sdk-php, cli, sdk-python, sdk-rust.
for name in ["sdk-php", "cli", "sdk-python", "sdk-rust"] {
let (_, _, range) = CLIENT_RANGES.iter().find(|(_, client, _)| *client == name).expect("every client has a range");
let mut requires: Vec<&str> = served.to_vec();
if name != "cli" {
requires.push("worker_protocol.version");
}
clients.insert(name.to_owned(), json!({"supported_versions": range, "requires": requires}));
}
json!({
"schema": "durable-workflow.v2.client-compatibility",
"version": 2,
"authority": "protocol_manifests",
"top_level_version_role": "informational",
"fail_closed": true,
"required_protocols": {
"auth_composition": identity(auth_composition),
"control_plane": {"version": "2", "header": "X-Durable-Workflow-Control-Plane-Version",
"request_contract": identity(request_contract)},
"worker_protocol": {"version": super::WORKER_PROTOCOL_VERSION, "header": "X-Durable-Workflow-Protocol-Version"},
},
"clients": clients,
})
}

/// Members published PHP's cluster info carries that native leaves out, each
/// with the reason.
pub(super) fn unqualified() -> Value {
json!({
"supported_sdk_versions": "Native has not qualified SDK version ranges of its own.",
"client_compatibility": "Native publishes no client compatibility matrix yet.",
"topology": "Native runs as one process role and publishes no topology manifest.",
"discovery.diagnostics_query": "Native serves no include=diagnostics view.",
"discovery.diagnostics_path": "Native serves no include=diagnostics view.",
Expand Down Expand Up @@ -165,4 +221,50 @@ mod tests {
fn the_version_is_never_empty() {
assert!(!version().is_empty());
}

fn served_compatibility() -> Value {
let auth: Value = serde_json::from_str(include_str!("auth-composition.v1.json")).unwrap();
client_compatibility(&auth, &super::super::visibility::request_contract())
}

#[test]
fn supported_sdk_versions_and_client_compatibility_never_disagree() {
// server#1005: one source, so every client's range is the same in both.
let compatibility = served_compatibility();
let ranges = supported_sdk_versions();
for (key, client) in [("php", "sdk-php"), ("python", "sdk-python"), ("rust", "sdk-rust"), ("cli", "cli")] {
assert_eq!(compatibility["clients"][client]["supported_versions"], ranges[key], "{client}");
}
assert_eq!(ranges.as_object().unwrap().len(), 4);
assert_eq!(compatibility["clients"].as_object().unwrap().len(), 4);
assert_eq!(ranges, json!({"php": ">=3.8.1,<4.0.0", "python": ">=2.13.0,<3.0.0",
"rust": ">=4.10.3,<5.0.0", "cli": ">=2.0.0,<3.0.0"}));
}

#[test]
fn client_compatibility_cites_only_protocols_native_serves() {
let compatibility = served_compatibility();
assert_eq!(compatibility["authority"], "protocol_manifests");
assert_eq!(compatibility["fail_closed"], true);
let required = compatibility["required_protocols"].as_object().unwrap();
let mut keys: Vec<&str> = required.keys().map(String::as_str).collect();
keys.sort();
assert_eq!(keys, ["auth_composition", "control_plane", "worker_protocol"]);
// The identities are the ones cluster info serves beside the block.
assert_eq!(required["auth_composition"], json!({"schema": "durable-workflow.v2.auth-composition.contract", "version": 1}));
assert_eq!(required["control_plane"]["request_contract"],
json!({"schema": "durable-workflow.v2.control-plane-request.contract", "version": 1}));
assert_eq!(required["worker_protocol"], json!({"version": super::super::WORKER_PROTOCOL_VERSION,
"header": "X-Durable-Workflow-Protocol-Version"}));
// No external execution contract is cited anywhere: native publishes none.
assert!(!compatibility.to_string().contains("external_"), "{compatibility}");
// Every client requirement resolves to a cited protocol member.
for (client, entry) in compatibility["clients"].as_object().unwrap() {
for requirement in entry["requires"].as_array().unwrap() {
let (protocol, member) = requirement.as_str().unwrap().split_once('.').unwrap();
assert!(required.get(protocol).and_then(|value| value.get(member)).is_some(),
"{client} requires {protocol}.{member}, which the block does not cite");
}
}
}
}
6 changes: 5 additions & 1 deletion rust/src/runtime/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1079,13 +1079,17 @@ async fn cluster(
let namespace = runtime.in_namespace(&discovery_namespace(&headers, &uri)).namespace_discovery().await?;
let limits = super::payload_discovery::limits()?;
let structural_limits = runtime.cluster_structural_limits(&metrics_config(&runtime)).await?;
let request_contract = super::visibility::request_contract();
let client_compatibility = super::cluster_info::client_compatibility(&auth_composition, &request_contract);
Ok(Json(
json!({"version": super::cluster_info::version(), "implementation": "rust", "development": super::cluster_info::build_identity().0,
"capabilities": super::cluster_info::capabilities(), "structural_limits": structural_limits,
"discovery": super::cluster_info::discovery(), "native_unqualified": super::cluster_info::unqualified(),
"server_id": super::payload_discovery::server_id(), "default_namespace": "default",
"namespace": namespace, "limits": limits,
"control_plane": {"version": "2", "request_contract": super::visibility::request_contract()}, "worker_protocol": {"version": super::WORKER_PROTOCOL_VERSION, "server_capabilities": capabilities()},
"supported_sdk_versions": super::cluster_info::supported_sdk_versions(),
"client_compatibility": client_compatibility,
"control_plane": {"version": "2", "request_contract": request_contract}, "worker_protocol": {"version": super::WORKER_PROTOCOL_VERSION, "server_capabilities": capabilities()},
"auth_composition_contract": auth_composition,
"principal_attribution_contract": principal_attribution,
"service_catalog_read_contract": service_catalog,
Expand Down
7 changes: 6 additions & 1 deletion rust/tests/execution/activity_results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ async fn activity_result_read_preserves_encoded_values_and_closed_parent_history
assert_eq!(info["structural_limits"]["pending_signal_count"], json!(5000), "{info}");
assert_eq!(info["discovery"]["task_repair_path"], json!("/api/system/repair"), "{info}");
assert!(info["native_unqualified"]["topology"].is_string(), "{info}");
assert!(info.get("supported_sdk_versions").is_none(), "{info}");
// Native's own qualified ranges, from the same source as
// client_compatibility (server#1005).
assert_eq!(info["supported_sdk_versions"]["rust"], json!(">=4.10.3,<5.0.0"), "{info}");
assert_eq!(info["client_compatibility"]["clients"]["sdk-rust"]["supported_versions"],
info["supported_sdk_versions"]["rust"], "{info}");
assert!(info["native_unqualified"].get("client_compatibility").is_none(), "{info}");
reopened.close().await;
database.remove().await;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,38 @@ export function checkClusterCapabilities(fixture, observation) {
}
const {implementation, ...rest} = state;
assert.ok(implementation === null || implementation === 'rust', 'published PHP or native');
checkClientManifests(rest, implementation);
assert.deepStrictEqual(rest, expectedFor(definition, implementation), `cluster capabilities (${implementation ?? 'php'})`);
return projection;
}

// The protocols native serves beside its client_compatibility block, and so
// the only ones the block may cite (server#1005). The CLI checks every
// protocol the block requires.
const NATIVE_SERVED_PROTOCOLS = ['auth_composition', 'control_plane', 'worker_protocol'];

/// Independent of the reviewed values: each client's range is the same in
/// supported_sdk_versions and client_compatibility, and native's block cites
/// only protocols native serves, with every client requirement resolving.
export function checkClientManifests(state, implementation) {
const ranges = state.client_ranges;
assert.ok(ranges && typeof ranges === 'object', 'client ranges recorded');
for (const [client, {supported_sdk_versions: sdk, client_compatibility: matrix}] of Object.entries(ranges)) {
assert.ok(typeof sdk === 'string' && sdk !== '', `${client}: supported_sdk_versions range`);
assert.equal(matrix, sdk, `${client}: client_compatibility and supported_sdk_versions agree`);
}
if (implementation !== 'rust') return;
const block = state.native_client_compatibility;
assert.ok(block && typeof block === 'object', 'native client_compatibility block');
assert.equal(block.authority, 'protocol_manifests', 'native compatibility authority');
const required = block.required_protocols;
assert.deepStrictEqual(Object.keys(required).sort(), NATIVE_SERVED_PROTOCOLS, 'native cites only protocols it serves');
assert.ok(!JSON.stringify(block).includes('external_'), 'native cites no external execution contract');
for (const [client, entry] of Object.entries(block.clients)) {
for (const requirement of entry.requires) {
const [protocol, member] = requirement.split('.');
assert.ok(required[protocol] && Object.hasOwn(required[protocol], member),
`${client} requires ${requirement}, which the block does not cite`);
}
}
}
13 changes: 13 additions & 0 deletions scripts/conformance/server-parity/cluster-capabilities-probe.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ function finishHttpClusterCapabilities(Client $admin, array $fixture, string $wo
static fn (string $member): bool => array_key_exists($member, $info), $definition['php_only_members'])),
'native_unqualified' => array_keys($info['native_unqualified'] ?? [])];
sort($state['native_unqualified']);
// Each client's range as both members state it (server#1005): the two
// must agree. Native's full client_compatibility block is pinned too.
$compatibility = is_array($info['client_compatibility'] ?? null) ? $info['client_compatibility'] : [];
$state['client_ranges'] = [];
foreach (['php' => 'sdk-php', 'python' => 'sdk-python', 'rust' => 'sdk-rust', 'cli' => 'cli'] as $key => $client) {
$state['client_ranges'][$key] = [
'supported_sdk_versions' => $info['supported_sdk_versions'][$key] ?? null,
'client_compatibility' => $compatibility['clients'][$client]['supported_versions'] ?? null,
];
}
if (($info['implementation'] ?? null) === 'rust') {
$state['native_client_compatibility'] = $info['client_compatibility'] ?? 'absent';
}

$empty = ['codec' => 'avro', 'blob' => $definition['empty_blob']];
$id = $workflowId.'-cc-run';
Expand Down
114 changes: 104 additions & 10 deletions tests/Fixtures/ServerParity/cluster-capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,24 @@
"message": "Activity not found in this run."
}
}
},
"client_ranges": {
"php": {
"supported_sdk_versions": ">=2.0.0,<3.0.0",
"client_compatibility": ">=2.0.0,<3.0.0"
},
"python": {
"supported_sdk_versions": ">=2.0.0,<3.0.0",
"client_compatibility": ">=2.0.0,<3.0.0"
},
"rust": {
"supported_sdk_versions": ">=2.0.0,<4.0.0",
"client_compatibility": ">=2.0.0,<4.0.0"
},
"cli": {
"supported_sdk_versions": ">=2.0.0,<3.0.0",
"client_compatibility": ">=2.0.0,<3.0.0"
}
}
},
"native_differences": {
Expand Down Expand Up @@ -293,14 +311,6 @@
"value": "{absent}",
"reason": "Native serves no include=diagnostics view."
},
"present.supported_sdk_versions": {
"value": false,
"reason": "Native leaves this member out and names it in native_unqualified."
},
"present.client_compatibility": {
"value": false,
"reason": "Native leaves this member out and names it in native_unqualified."
},
"present.topology": {
"value": false,
"reason": "Native leaves this member out and names it in native_unqualified."
Expand All @@ -327,21 +337,105 @@
},
"native_unqualified": {
"value": [
"client_compatibility",
"conformance_manifests",
"discovery.diagnostics_path",
"discovery.diagnostics_query",
"structural_limits.external_payload_transaction_bytes",
"supported_sdk_versions",
"topology"
],
"reason": "Native names each member it leaves out."
},
"client_ranges": {
"value": {
"php": {
"supported_sdk_versions": ">=3.8.1,<4.0.0",
"client_compatibility": ">=3.8.1,<4.0.0"
},
"python": {
"supported_sdk_versions": ">=2.13.0,<3.0.0",
"client_compatibility": ">=2.13.0,<3.0.0"
},
"rust": {
"supported_sdk_versions": ">=4.10.3,<5.0.0",
"client_compatibility": ">=4.10.3,<5.0.0"
},
"cli": {
"supported_sdk_versions": ">=2.0.0,<3.0.0",
"client_compatibility": ">=2.0.0,<3.0.0"
}
},
"reason": "Native states the ranges its installed qualification supports, from one source for both members (server#1005). sdk-php >=3.8.1: 3.8.0 lost activity claims with heartbeat_timeout under lease maintenance (server#985), fixed in 3.8.1; 3.8.3 passed installed acceptance against native 3.0.4 (https://github.com/durable-workflow/sdk-php/pull/243#issuecomment-6612840439). sdk-python >=2.13.0: 2.13.0 passed installed acceptance against native 3.0.1 (https://github.com/durable-workflow/sdk-python/issues/228, sdk-python#168). sdk-rust >=4.10.3: 4.10.2 failed native describe parity (server#967, fixed by server#968 in 3.0.2); 4.10.3 declares and is qualified on native from 3.0.2 (https://github.com/durable-workflow/sdk-rust/pull/160, harness sdk-rust#155). cli >=2.0.0,<3.0.0: the Rust CLI ships as a 2.x minor (cli#72); its raw worker commands need native 3.1.0 (server#1001). Each upper bound is the next major."
}
},
"native_additions": {
"capabilities.child_workflow_start_turn": {
"value": true,
"reason": "Native serves start_child_workflow await: \"started\", a parent turn when the child start commits (Server #966); published PHP 2.x has no such mode."
},
"native_client_compatibility": {
"value": {
"schema": "durable-workflow.v2.client-compatibility",
"version": 2,
"authority": "protocol_manifests",
"top_level_version_role": "informational",
"fail_closed": true,
"required_protocols": {
"auth_composition": {
"schema": "durable-workflow.v2.auth-composition.contract",
"version": 1
},
"control_plane": {
"version": "2",
"header": "X-Durable-Workflow-Control-Plane-Version",
"request_contract": {
"schema": "durable-workflow.v2.control-plane-request.contract",
"version": 1
}
},
"worker_protocol": {
"version": "1.20",
"header": "X-Durable-Workflow-Protocol-Version"
}
},
"clients": {
"sdk-php": {
"supported_versions": ">=3.8.1,<4.0.0",
"requires": [
"auth_composition.version",
"control_plane.version",
"control_plane.request_contract",
"worker_protocol.version"
]
},
"cli": {
"supported_versions": ">=2.0.0,<3.0.0",
"requires": [
"auth_composition.version",
"control_plane.version",
"control_plane.request_contract"
]
},
"sdk-python": {
"supported_versions": ">=2.13.0,<3.0.0",
"requires": [
"auth_composition.version",
"control_plane.version",
"control_plane.request_contract",
"worker_protocol.version"
]
},
"sdk-rust": {
"supported_versions": ">=4.10.3,<5.0.0",
"requires": [
"auth_composition.version",
"control_plane.version",
"control_plane.request_contract",
"worker_protocol.version"
]
}
}
},
"reason": "Native's client_compatibility (server#1005) cites only protocols it serves beside the block: the auth composition contract, the control plane with its request contract, and worker protocol 1.20. Published PHP's block also cites the skew refusal matrix and five worker_protocol external execution contracts, which native does not publish; the CLI checks every protocol the block requires, so native must not cite them."
}
}
}
Expand Down
Loading
Loading