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
4 changes: 3 additions & 1 deletion .github/workflows/server-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ on:
- tests/Unit/ServerParityStorageTestContractTest.mjs
- tests/Unit/ServerParitySchedulePoliciesContractTest.mjs
- tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs
- tests/Unit/ServerParityScheduleCatalogContractTest.mjs
- tests/Unit/ServerParityWorkerListingContractTest.mjs
- tests/Unit/ServerParityWorkerReplacementContractTest.mjs
- tests/Unit/ServerParityWaitingHistoryContractTest.mjs
Expand Down Expand Up @@ -75,6 +76,7 @@ on:
- tests/Unit/ServerParityStorageTestContractTest.mjs
- tests/Unit/ServerParitySchedulePoliciesContractTest.mjs
- tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs
- tests/Unit/ServerParityScheduleCatalogContractTest.mjs
- tests/Unit/ServerParityWorkerListingContractTest.mjs
- tests/Unit/ServerParityWorkerReplacementContractTest.mjs
- tests/Unit/ServerParityWaitingHistoryContractTest.mjs
Expand Down Expand Up @@ -1466,7 +1468,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/ServerParitySchedulePoliciesContractTest.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 tests/Unit/ServerParityScheduleCatalogContractTest.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
63 changes: 63 additions & 0 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,69 @@ 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, schedule listing, updates and backfill:

- **Listing.** `schedule_list.rs` serves `GET /api/schedules` as PHP's
`ScheduleController::index` does:
- the `status`, `workflow_type`, `query`, `page_size` and
`next_page_token` filters, with Laravel's rules and messages and PHP's
refusal bodies, including `last_safe_cursor`;
- the visibility query from `ScheduleVisibilityQuery`, which accepts only
equality predicates joined by `AND`. It works on ScheduleId, Status,
WorkflowType, TaskQueue, Note and registered search attributes, with
PHP's literal rules. A search attribute predicate matches no schedule,
because native schedules hold no search attribute values.
- continuation tokens signed as PHP signs them, with `DW_SERVER_KEY`,
then `APP_KEY`. When neither is set, PHP generates a key for each
container; native uses its auth token instead, so tokens survive
restarts and work on every node. Each token is bound to its namespace,
filters and a cursor that must still be present.
- newest-first order, with PHP's list items.

The SQL is assembled from fixed fragments with bound values for each
database family.
- **Updates.** `PUT /api/schedules/{id}` follows `ScheduleManager::update`:
the action is merged with the stored one, the quota is recomputed, the
next fire time is recalculated, and `ScheduleUpdated` carries PHP's
changed fields.
- **Backfill.** `POST /api/schedules/{id}/backfill` enumerates every
occurrence from the start time up to, but not including, the end time,
and triggers each one as `triggerForBackfill` does:
- no stale check;
- buffering policies start every occurrence;
- a fired occurrence is skipped;
- the run quota ends the backfill.

Fire times carry the offset PHP formats: the schedule zone's for a cron
occurrence, UTC for an interval. The Laravel `date` rule reads ISO-8601
dates and date-times exactly as PHP does, including hour 24, second 60,
fractions and offsets.
- **Deliberate differences.**
- Backfill times use ISO-8601 only. PHP's other `strtotime` forms, such as
`2026-04-01 +1 day` or `@1700000000`, get the `date` validation message.
- A backfill range with more than 10,000 occurrences is refused with 422
`backfill_range_too_large`. PHP has no limit.
- An update to a specification that cannot be evaluated is a 422. PHP
leaves that `LogicException` unhandled and returns 500.
- **Corrected.** Laravel sizes a non-numeric value under a numeric rule by
its length, so `page_size=x` reports only the integer rule. The shared
rules engine now does the same.
- **Evidence.**
- `cron-oracle/backfill-dates.php` records PHP's `date` rule and instant
for 700 seeded strings. Native accepts nothing PHP refuses, reads every
accepted string to the same microsecond, and reads every ISO-8601 shape
PHP accepts, except offsets of a day or more.
- The next-fire corpus now also records PHP's offset, and native matches
all 600 cases.
- The new `schedule-catalog` fixture sends 72 requests. Published PHP and
native agreed on every normalized response locally, and a reviewed copy
is in `tests/Fixtures/ServerParityExpected`.
- A PHP-on-PostgreSQL harness matched native on 81 responses, except the
two deliberate differences above.
- Schedule tests pass on SQLite, PostgreSQL, MySQL 8.4 and MariaDB 10.11.
- **Next.** Narrowing the takeover schedule refusal (#568) to what native
still cannot run.

October 21, PHP schedule specifications and policies:

- **Cron.** `schedule_cron.rs` ports `dragonmantank/cron-expression` 3.6.0 as
Expand Down
3 changes: 3 additions & 0 deletions rust/src/runtime/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ impl Runtime {
delegate!(schedule_history(schedule: &str, after: i64, limit: i64) -> 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>);
delegate!(update_schedule(schedule: &str, changes: super::schedules::Changes, context: Value) -> Result<Value>);
delegate!(list_schedules(raw: Option<&str>, secret: &str) -> Result<Value>);
delegate!(backfill_schedule(schedule: &str, range: super::schedule_backfill::Range, context: Value) -> Result<Value>);
pub(crate) async fn signal(
&self,
workflow_id: &str,
Expand Down
13 changes: 11 additions & 2 deletions rust/src/runtime/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,15 @@ pub fn router(runtime: Runtime) -> Router {
delete(delete_search_attribute),
)
.route("/api/workflows", get(list_workflows).post(start))
.route("/api/schedules", post(super::schedule_http::create))
.route(
"/api/schedules",
get(super::schedule_list::list).post(super::schedule_http::create),
)
.route(
"/api/schedules/{schedule_id}",
get(super::schedule_http::describe).delete(super::schedule_http::remove),
get(super::schedule_http::describe)
.put(super::schedule_http::update)
.delete(super::schedule_http::remove),
)
.route(
"/api/schedules/{schedule_id}/history",
Expand All @@ -76,6 +81,10 @@ pub fn router(runtime: Runtime) -> Router {
"/api/schedules/{schedule_id}/trigger",
post(super::schedule_http::trigger),
)
.route(
"/api/schedules/{schedule_id}/backfill",
post(super::schedule_http::backfill),
)
.route("/api/workflows/{workflow_id}", get(describe_current))
.route(
"/api/workflows/{workflow_id}/observation",
Expand Down
45 changes: 37 additions & 8 deletions rust/src/runtime/laravel_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
pub(crate) enum Rule {
Required,
Nullable,
/// Validate only when the attribute is present.
Sometimes,
/// `regex:/\S/`: some character that is not whitespace.
NotBlank,
/// Required when the named attribute is present and not empty.
RequiredWith(&'static str),
String,
Expand All @@ -26,6 +30,8 @@
Min(i64),
Max(i64),
In(&'static [&'static str]),
/// `date`, for the ISO-8601 shapes `schedule_backfill::php_date` reads.
Date,
}

pub(crate) type Rules = [(&'static str, &'static [Rule])];
Expand Down Expand Up @@ -182,15 +188,26 @@
"array" => format!("The {attribute} field must not have more than {limit} items."),
_ => format!("The {attribute} field must not be greater than {limit} characters."),
},
Rule::Nullable => String::new(),
Rule::NotBlank => format!("The {attribute} field format is invalid."),
Rule::Date => format!("The {attribute} field must be a valid date."),
Rule::Nullable | Rule::Sometimes => String::new(),
}
}

/// Laravel's `getSize`: a numeric value under a numeric rule, otherwise
/// an item count or the string length.
fn size(value: &Value, rules: &[Rule]) -> Option<f64> {
if rules.iter().any(|rule| matches!(rule, Rule::Integer)) {
return integer(value).map(|number| number as f64);
match value {
Value::Number(number) => return number.as_f64(),
Value::String(text) if super::schedule_cron::php_is_numeric(text) => {
return text.trim().parse().ok();
}
_ => {}
}
}
match value {
Value::Bool(flag) => Some(f64::from(u8::from(*flag))),
Value::Array(items) => Some(items.len() as f64),
Value::Object(fields) => Some(fields.len() as f64),
Value::String(text) => Some(text.chars().count() as f64),
Expand All @@ -203,8 +220,12 @@
match rule {
Rule::Required => filled(value),
Rule::RequiredWith(other) => !filled(lookup(data, other)) || filled(value),
Rule::Nullable => true,
Rule::Nullable | Rule::Sometimes => true,
Rule::NotBlank => value.and_then(Value::as_str).is_some_and(|text| text.chars().any(|character| !character.is_whitespace())),
Rule::String => value.is_some_and(Value::is_string),
Rule::Date => value
.and_then(Value::as_str)
.is_some_and(|text| super::schedule_backfill::php_date(text).is_some()),
Rule::Array => value.is_some_and(|value| value.is_array() || value.is_object()),
Rule::Integer => value.and_then(integer).is_some(),
Rule::Boolean => matches!(
Expand All @@ -229,6 +250,14 @@
/// Validates `body` (already normalized) and returns Laravel's 422 failure,
/// or nothing when every rule passes.
pub(crate) fn validate(body: &Value, rules: &Rules) -> Result<(), RuntimeError> {
match failure(errors(body, rules)) {
Some(error) => Err(error),
None => Ok(()),
}
}

/// Every failing attribute and its messages, in Laravel's order.
pub(crate) fn errors(body: &Value, rules: &Rules) -> Vec<(String, Vec<String>)> {

Check failure on line 260 in rust/src/runtime/laravel_rules.rs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 31 to the 30 allowed.

See more on https://sonarcloud.io/project/issues?id=durable-workflow_server&issues=AaFddNEexo3Xvc7dOoWH&open=AaFddNEexo3Xvc7dOoWH&pullRequest=573
let mut checks: Vec<(String, bool, &[Rule])> = Vec::new();
let mut expanded = Vec::new();
for (pattern, list) in rules {
Expand All @@ -243,11 +272,14 @@
for (attribute, raw, list) in checks {
let value = lookup(body, &attribute);
let nullable = list.iter().any(|rule| matches!(rule, Rule::Nullable));
if value.is_none() && list.iter().any(|rule| matches!(rule, Rule::Sometimes)) {
continue;
}
let name = display(&attribute, raw);
let mut messages = Vec::new();
for rule in list {
let implicit = matches!(rule, Rule::Required | Rule::RequiredWith(_));
if matches!(rule, Rule::Nullable) {
if matches!(rule, Rule::Nullable | Rule::Sometimes) {
continue;
}
// presentOrRuleIsImplicit, then isNotNullIfMarkedAsNullable.
Expand All @@ -269,10 +301,7 @@
errors.push((attribute, messages));
}
}
match failure(errors) {
Some(error) => Err(error),
None => Ok(()),
}
errors
}

/// Laravel's 422 ValidationException body: the first message, a count of the
Expand Down
2 changes: 2 additions & 0 deletions rust/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ pub(crate) mod laravel_rules;
pub(crate) mod schedule_cron;
pub(crate) mod schedule_spec;
pub(crate) mod schedule_zones;
mod schedule_backfill;
mod schedule_execution;
mod schedule_http;
mod schedule_list;
mod schedule_storage;
mod schedules;
mod schema;
Expand Down
Loading
Loading