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
12 changes: 9 additions & 3 deletions .github/workflows/server-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@ on:
- tests/Unit/ServerParityScheduleContractTest.mjs
- tests/Support/ServerParityScheduleObservation.mjs
- tests/Fixtures/ServerParityPending/**
- tests/Fixtures/ServerParityExpected/**
- tests/Fixtures/ServerParityProfiles/**
- tests/Unit/ServerParityVisibilityContractTest.mjs
- tests/Unit/ServerParityAdmissionContractTest.mjs
- tests/Unit/ServerParityRoleTokenContractTest.mjs
- tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs
- tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs
- tests/Unit/ServerParitySearchAttributeContractTest.mjs
- tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs
- tests/Unit/ServerParityStorageTestContractTest.mjs
- tests/Unit/ServerParitySchedulePoliciesContractTest.mjs
- tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs
- tests/Unit/ServerParityWorkerListingContractTest.mjs
- tests/Unit/ServerParityWorkerReplacementContractTest.mjs
- tests/Unit/ServerParityWaitingHistoryContractTest.mjs
Expand Down Expand Up @@ -61,14 +64,17 @@ on:
- tests/Unit/ServerParityScheduleContractTest.mjs
- tests/Support/ServerParityScheduleObservation.mjs
- tests/Fixtures/ServerParityPending/**
- tests/Fixtures/ServerParityExpected/**
- tests/Fixtures/ServerParityProfiles/**
- tests/Unit/ServerParityVisibilityContractTest.mjs
- tests/Unit/ServerParityAdmissionContractTest.mjs
- tests/Unit/ServerParityRoleTokenContractTest.mjs
- tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs
- tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs
- tests/Unit/ServerParitySearchAttributeContractTest.mjs
- tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs
- tests/Unit/ServerParityStorageTestContractTest.mjs
- tests/Unit/ServerParitySchedulePoliciesContractTest.mjs
- tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs
- tests/Unit/ServerParityWorkerListingContractTest.mjs
- tests/Unit/ServerParityWorkerReplacementContractTest.mjs
- tests/Unit/ServerParityWaitingHistoryContractTest.mjs
Expand Down Expand Up @@ -1460,7 +1466,7 @@ jobs:
-e COMPOSER_HOME=/tmp/composer -v "$PWD:/source" -w /source "$image" \
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/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/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs > parity-evidence/comparator-tests.txt
"$image" --test tests/Unit/ServerParityRunnerTest.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/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.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
67 changes: 67 additions & 0 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,73 @@ support is not evidence of multi-node safety.
Newest checkpoints first. Each entry is exact to the source, tree and runs it
names, including unsuccessful observations.

October 21, PHP schedule specifications and policies:

- **Cron.** `schedule_cron.rs` ports `dragonmantank/cron-expression` 3.6.0 as
published PHP 2.5.14 runs it: validation, aliases, names, ranges, steps,
lists, `L`, `W`, `#` and `?`, the separate day-of-month and day-of-week
searches, the 1,000-step limit and each field's quirks. PHP's `DateTime`
rules around daylight-saving changes are kept: a skipped wall-clock time
takes the offset from before the change, a repeated one keeps the current
daylight flag, and hour and minute steps are applied in UTC. Zone rules come
from `jiff`'s bundled IANA data (2026e). Ireland and Morocco use negative
daylight saving in the main IANA data PHP bundles, so PHP's flag is
inverted there.
- **Zones.** `schedule_zones.rs` reads names as `new DateTimeZone()` does:
numeric offsets (`+5`, `-0330`, `GMT+1`), PHP's 144 abbreviations first
(`CET` is fixed +01:00) and then IANA identifiers in any case, with matched
parentheses. Offsets of 26 hours or more, which PHP accepts up to 100, are
refused.
- **Specifications.** `schedule_spec.rs` is `computeNextFireAt`: the earliest
occurrence across cron expressions in the zone and intervals counted from
the epoch plus an offset, with PHP's `DateInterval` grammar and its
365-day year and 30-day month.
- **Corpora.** `scripts/conformance/cron-oracle` regenerates, from the frozen
image, 1,200 cron cases, every PHP zone spelling (1,068 lines) and 600 whole
specifications. Native matches all of them, and 6,000 and 2,000 more seeded
cases checked locally. Six zones whose rules changed after PHP's 2026.1
database are left out (Casablanca, El Aaiun, Edmonton, Inuvik, Vancouver,
Winnipeg); native uses the newer rules.
- **Schedules.** Creation follows PHP's Laravel rules and messages (a small
`laravel_rules.rs` engine: trimming, empty strings as null, `*` expansion,
attribute names and order). It accepts any schedule id, cron and interval
specifications in any zone, all six overlap policies, jitter, any run
quota, memo, note and creation paused. Descriptions, history snapshots and
history pages have PHP's shapes and timestamps. Pause accepts an
already paused schedule and records its note; resume records its note
after the resumed audit. Triggers follow `triggerDetailed`: the stale and
status checks, quota exhaustion, buffering with a full `buffer_one`,
closing the latest run for `cancel_other` and `terminate_other`, the
ten-entry recent actions, failure accounting and the next fire time after
each fire. The tick drains buffered actions of finished runs before due
schedules. Schedules are namespace-scoped and start runs in their own
namespace.
- **Deliberate differences.** PHP's unhandled 500 for resuming an active
schedule is a 409 `schedule_not_paused`. A tick that finds another native
node already fired the occurrence skips it silently instead of recording
`stale_occurrence`. Plain JSON array input and search attributes are
refused, as native start refuses them; Avro input that does not decode is
refused at creation.
- **Evidence.** The new `schedule-policies` fixture sends 60 requests to a fresh
namespace and compares every response, without generated identities and
times, with a reviewed copy of published PHP's
(`tests/Fixtures/ServerParityExpected`). Published PHP, embedded and native
passed it locally, and native from main failed it. Published PHP on SQLite
can fail a trigger with `database is locked` against its own maintenance
roles, and the transaction rolls back, so the probe retries exactly that.
The two existing schedule fixtures still pass. A local harness running PHP
on PostgreSQL matched native on all 72 compared responses except the two
refusals above.
- **Takeover.** The takeover schedule check from #568 now refuses only what
native still cannot run: search attributes, visibility labels, Laravel
queue routing, an unknown status, a definition native's own creation
refuses (such as input that is not a payload envelope), and a
specification it cannot evaluate. Cron schedules, schedules in any
namespace, notes, memo, buffered actions, jitter, quotas and every
overlap policy are taken over.
- **Next.** Update, listing with visibility queries and page tokens, and
backfill.

October 21, signals before any worker registers:

- **Found by.** Waterline 2.12.0 service mode was compared against
Expand Down
105 changes: 101 additions & 4 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tokio-util = { version = "=0.7.19", features = ["io", "io-util"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal", "sync", "time", "fs", "io-util"] }
ulid = "1"
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
jiff = { version = "0.2.38", default-features = false, features = ["std", "tzdb-bundle-always"] }

[target.'cfg(unix)'.dependencies]
libc = "0.2"
Expand Down
39 changes: 24 additions & 15 deletions rust/src/runtime/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,20 +352,29 @@ impl Runtime {
Storage::MySql(store) => store.start(body, memo, external).await,
}
}
pub(crate) async fn create_schedule(&self, body: Value, context: Value) -> Result<Value> {
let definition = super::schedules::definition(&body, super::store::now())?;
let arguments = super::envelope(&definition.action, "input")?;
super::queries::decode(
self.signal_codec.clone(),
arguments,
true,
"invalid_schedule_arguments",
)
.await?;
pub(crate) async fn create_schedule(
&self,
body: Value,
schedule_id: String,
created: Value,
paused: Value,
) -> Result<Value> {
let mut definition = super::schedules::definition(&body)?;
definition.schedule_id = Some(schedule_id);
if definition.action.get("input").is_some_and(|input| !input.is_null()) {
let arguments = super::envelope(&definition.action, "input")?;
super::queries::decode(
self.signal_codec.clone(),
arguments,
true,
"invalid_schedule_arguments",
)
.await?;
}
match &*self.storage {
Storage::Sqlite(store) => store.create_schedule(definition, context).await,
Storage::Postgres(store) => store.create_schedule(definition, context).await,
Storage::MySql(store) => store.create_schedule(definition, context).await,
Storage::Sqlite(store) => store.create_schedule(definition, created, paused).await,
Storage::Postgres(store) => store.create_schedule(definition, created, paused).await,
Storage::MySql(store) => store.create_schedule(definition, created, paused).await,
}
}
delegate!(describe_schedule(schedule: &str) -> Result<Value>);
Expand All @@ -379,8 +388,8 @@ impl Runtime {
) -> Result<Value>
);
delegate!(schedule_history(schedule: &str, after: i64, limit: i64) -> Result<Value>);
delegate!(edit_schedule(schedule: &str, operation: &str, context: Value) -> Result<Value>);
delegate!(trigger_schedule(schedule: &str, context: Value) -> Result<Value>);
delegate!(edit_schedule(schedule: &str, operation: &str, note: Option<String>, context: Value) -> Result<Value>);
delegate!(trigger_schedule(schedule: &str, overlap: Option<String>, context: Value) -> Result<Value>);
pub(crate) async fn signal(
&self,
workflow_id: &str,
Expand Down
4 changes: 4 additions & 0 deletions rust/src/runtime/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ fn named_namespace_operation(method: &str, path: &str) -> bool {
if path.starts_with("/api/external-payloads/v1/") {
return method == "GET";
}
// Schedules are namespace-scoped rows, as in PHP.
if path == "/api/schedules" || path.starts_with("/api/schedules/") {
return true;
}
// Keep unimplemented families from reaching default-namespace selectors.
// Further families require their own shared namespace fixtures.
if path == "/api/workflows" {
Expand Down
Loading
Loading