Repository navigation
Rust #325: Nexus catalog administration, service-call execution, admission limits and call reads - #793
Conversation
Native now answers the 15 admin routes under /api/service-endpoints as published Server 2.11.1's ServiceCatalogController does: create, list, show, update and delete endpoints, services and operations by lowercased name, with PHP's validation, 404 and 409 bodies, Eloquent's dirty check and the deletion refusals. The catalog reads under /api/service-catalog now render the namespace's endpoint, service and operation rows with PHP's paging and related limits; calls stay empty until native records service calls. The shared rules gain Laravel's regex (strings and numbers) and prohibited, and size a number without a numeric rule by its PHP string, as getSize does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service-endpoints fixture drives the 15 catalog administration routes and the catalog reads over their rows in order, 72 requests in a fresh namespace, and was recorded against published Server 2.11.1 on its SQLite reference. Generated ids and whole-second times are placeholders that must recur consistently; everything else is exact. The port log counts 161 of 175 served route methods and lists the three documented differences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native now serves the endpoint, service and operation tables, so takeover no longer refuses a source that holds their rows; service calls are still refused. Tests write the rows as published PHP does (lowercase ULIDs, microsecond times, json_encode documents with escaped slashes) on SQLite, PostgreSQL, MySQL and MariaDB, take the database over, and check that native renders, compares and extends them as its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST .../operations/{operation}/execute, GET .../service-calls/{call}
and POST .../service-calls/{call}/cancel now answer as published Server
2.11.1's ServiceCatalogController does over Workflow 2.10.2's
DefaultServiceControlPlane, for the handler kinds that stay accepted
(activity_execution and invocable_http): Laravel validation, the payload
codec and Avro arguments, admission at the service boundary with an
audit row for every decision (required roles, caller allow and deny
lists, the circuit breaker, and the 503 for an invalid shared limit),
the stamped handler, handler failures retried in the request, deadlines
expired lazily on every read, idempotent replays that re-check
authorization, and cancellation.
A workflow-bound handler kind, an operation with a valid shared limit,
an external_payload argument and cancelling into a linked workflow
answer 501 before anything is written.
Schema version 7 adds the native-owned dw_service_call_deadlines, so
the frozen PHP tables stay as they are and takeover layouts stay
recognisable. The service-calls fixture records 71 requests against
published 2.11.1, with shared placeholder matching for generated ids
and times.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sonar flags the closure as redundant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service boundary's per-minute rate and concurrency limits now admit or refuse calls instead of answering 501. PHP keeps these counters in its cache under a cache lock (SharedServiceBoundaryPolicy::admitWithLock); native keeps them in the native-owned dw_service_admission_counters, read and written under the transition lock in the call's transaction, with PHP's keys and lives: a rate count per boundary and minute for 120 s, and a concurrency lease per boundary for a day. A sync call returns its lease when the request ends, even after a failed dispatch; an async call's lease stays until it expires, as PHP's does. Schema version 7's migration is renamed 0007_service_calls.sql and adds the counters table on SQLite, PostgreSQL, MySQL and MariaDB, with the catalogs, takeover relations and parity schema scripts. The SQLite parity schema check now also ignores the deadline index, which the previous step missed. The service-calls fixture gains 11 cases recorded against published 2.11.1: a rate refusal with its configured retry, another caller's own window, an async lease kept, a sync lease returned, and the throttled audit row. The probe opens the rate window at least 15 seconds before a minute ends so it cannot roll over mid-case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/service-catalog/calls and GET /api/service-catalog/calls/{id}
now read the calls native records, as published Server 2.11.1's
ServiceCatalogReadController does: ServiceCatalog's scopes (by default
the calls the namespace made or that target it), the status and outcome
filters and buckets, newest accepted first as orderByDesc reads them on
each database, and ServiceCallView's list item and detail with the
caller and link references, the result envelope and the failure type.
Payload references and the Nexus result show only to the namespace that
owns the call. Before this, both answered PHP's empty page and
not-found even when calls existed.
The service-calls fixture gains nine catalog reads recorded against
published 2.11.1. Each list holds only accepted or only rejected calls:
a rejected call has no accepted_at, and where NULL sorts differs between
databases, for PHP as for native.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
682cb8e to
355aa39
Compare
|
| // Endpoints: lowercased names, PHP's array documents, 409 with the id. | ||
| let (status, billing) = request(&app, "POST", "/api/service-endpoints", | ||
| json!({"endpoint_name": " Billing ", "description": "Payments", "metadata": {"team": "core", "empty": {}}})).await; | ||
| assert_eq!(status, StatusCode::CREATED, "{billing}"); |
| let (status, billing) = request(&app, "POST", "/api/service-endpoints", | ||
| json!({"endpoint_name": " Billing ", "description": "Payments", "metadata": {"team": "core", "empty": {}}})).await; | ||
| assert_eq!(status, StatusCode::CREATED, "{billing}"); | ||
| assert!(is_ulid(&billing["id"]), "{billing}"); |
|
CodeQL (not a required check) flags two high-severity "cleartext logging" alerts in |
#793's merge group failed one PostgreSQL corpus cell on worker-sessions reacquire_terminal: ttl_span 1799 where the recording has 1800. PHP computes a new session's ttl_expires_at from its request clock, then Eloquent stamps created_at when it inserts the row; when the insert lands in the next second, the probe's ttl_span (ttl_expires_at minus created_at) reads one second short. Native stamps both from one clock reading, so the race is the published reference's. The contract now reads a ttl_span exactly one second short of the recorded one as the recorded one, and nothing else. Its test checks that one second short passes and that two seconds short or one second long still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Requeued at 22:38Z, pinned to the same head |
|
Merged through the merge queue at 23:02Z as
Native now answers 174 of 175 published route methods. Next: #795 (takeover of published 2.11.1 databases), already open and stacked on this. |



Part of #325: Nexus plan steps 1, 2a, 2b and 2c (plan, step 2 design). Native service catalog administration and Nexus service-call execution, matching published Server 2.11.1, fixture first. The steps are batched to save CI cycles; their commits are separate.
Review tier: advisory (pre-RC native). It changes the native server, a native-owned schema relation, the shared parity harness and the port log. It touches no published PHP surface, credentials, production data or CI trust. Policy: #325 comment 6423232503.
Step 1: catalog administration and reads
Fixture first.
service-endpointswas recorded against published 2.11.1 on its SQLite reference: 72 requests in order, in a fresh namespace.endpoint_not_foundcarriesaccepted: false).timeout_seconds, a malformed RFC3339, an impossible calendar date or hour, and years outside 1970–9999.updated_aton an unchanged update, a list read back as an object, services listed by name, and an allowed rename.Native. The new module
rust/src/runtime/service_endpoints.rsservesServiceCatalogController's endpoint, service and operation routes:strtolower, in the path and the body. The database's own comparison finds them, as PHP's query does.HasUlids) and microsecond times. An update writes only when a field changes, as Eloquent's dirty check does.{}is[], and{"0":..,"1":..}is a list.created_at, idbefore the controller's name order. Endpoint lists are by name.rust/src/runtime/service_catalog.rsnow renders the namespace's rows: PHP's paging (count, then the window), filters by endpoint and service id, related lists capped at 100 withrelated_has_more, and an operation's endpoint and service when the namespace holds them. Native records no service calls yet, so a calls list is still PHP's empty page.Shared validation rules (
laravel_rules.rs):Rule::Regex, Laravel'sregex:asvalidateRegexapplies it: to a string or to a number's PHP string.Rule::Prohibited, an implicit rule.sizemeasures a number without a numeric rule by its PHP string, as Laravel'sgetSizedoes. Before,max:191measured500by its value. The existingRule::Patternusers are unchanged.Three documented differences, also in the module doc and the port log:
updated_at.metadata[a]=1array syntax.Takeover no longer refuses a PHP database that holds endpoints, services or operations; a source with service calls is still refused. Tests write the rows as published PHP does (lowercase ULIDs, microsecond times,
json_encodedocuments with escaped slashes), take the database over, and check that native renders them, keepsupdated_aton an unchanged update, lists a new native service after PHP's, and refuses deleting the endpoint.Not in this PR: service execution, service-call show and cancel, and the
start_service_operationcommand (Nexus steps 2–4).The 15 routes leave
unserved-php-routes.v2.11.1.txt.Step 2a: service-call execution, show and cancel
Fixture first.
service-callswas recorded against published 2.11.1: 82 requests in a fresh namespace (71 for step 2a, 11 more for step 2b). It passes against published PHP on its SQLite reference and as an app on MySQL 8, MariaDB 10.11 and PostgreSQL 17. It covers:argumentsand envelopes, search attributes, and the runtime transport's refusal of a provider reference before routing.activity_executionandinvocable_http, with stamped handlers (an uppercase ULID, or the binding's own ids), caller options, sync and async modes; show by trimmed id, and 404 under another operation.retry_policy.max_attemptsunless its type is non-retryable, withservice_call_attemptsrecorded.timeout_secondssetscall_deadline_at; adeadline_atalready past is admitted and timed out in the same request (200,accepted: true).service_call_terminal;mode: noneis 409cancellation_not_allowed; the reason is validated after the lookup.service_boundary_admission_unavailable.rate_limit_exceededwith the configured retry of 7, while another caller namespace has its own window; an async call's concurrency lease stays taken, so the next is refused 429concurrency_limit_exceeded; a sync call returns its lease, so the next is admitted; show reads the throttled audit row. The probe starts the rate window at least 15 seconds before a minute ends so the window can't roll over mid-case.The contract matches exact answers with placeholders for generated values: lowercase and uppercase ULIDs, whole-second and microsecond times, also inside messages. Its test rejects 11 mutations.
Native. The new module
rust/src/runtime/service_calls.rsportsexecuteOperation,serviceCallShowandserviceCallCancelover Workflow 2.10.2'sDefaultServiceControlPlane, from a source-level spec of the published code:array_replaceresponses.Step 2b, shared admission limits (
SharedServiceBoundaryPolicy::admitWithLock): the per-minute rate and the concurrency limit per caller boundary (caller|target|endpoint|service|operation), checked in PHP's order (rate, then concurrency) and counted only when admitted. PHP keeps these counters in its cache; native keeps them indw_service_admission_counters, read and written under the transition lock in the call's own transaction. The keys and lives are PHP's:rate:<sha256>:<minute>for 120 s andconcurrency:<sha256>for a day. A sync call returns its lease when the request ends, even when dispatch fails, as PHP'sfinallydoes; an async call's lease stays until it expires. Allowed calls record PHP's admission metadata (admission_scope: shared_cacheand the four limits), and refusals recordobserved_window_countorobserved_in_flight.Schema version 7 (file
0007_service_calls.sql) adds two native-owned relations on SQLite, PostgreSQL, MySQL and MariaDB:dw_service_call_deadlinesfor each call's deadline anddw_service_admission_countersfor the limits. Catalogs, takeover relations, parity schema scripts and namespace cleanup are updated. The SQLite parity schema check now also ignores the deadline index, which step 2a missed.Not in this PR, each answered 501
rust_capability_not_implementedbefore anything is written:external_payloadreference inarguments;Takeover still refuses a source that holds service calls (step 5).
Step 2c, catalog call reads. Recording calls made the
/api/service-catalogcall reads wrong: they still answered PHP's empty page and not-found.GET /api/service-catalog/callsandGET /api/service-catalog/calls/{id}now read the recorded calls asServiceCatalogReadController::callsIndexandcallShowdo:ServiceCatalog's scopes: by default the calls the namespace made or that target it; alsoowned,callerandtarget.orderByDescreads it on each database. A rejected call has noaccepted_at, so where it sorts follows the database's NULL order, for PHP and native alike.ServiceCallView's list item and detail, with the caller and link references, the result envelope and the failure type. Payload references and the Nexus result show only to the namespace that owns the call.The
service-callsfixture gains nine catalog reads recorded against published 2.11.1 (91 cases in all). Each list holds only accepted or only rejected calls, so the recorded order is the same on every database.Decided differences, also in the module doc and the port log:
dw_service_call_deadlines, not in 2.11.1'sworkflow_service_calls.call_deadline_atcolumn. Native keeps the frozen PHP tables plus its own relations, so takeover layouts stay recognisable; step 5 will copy deadlines when takeover accepts service calls. This deviates from the plan's "add the column".The execute, show and cancel routes leave
unserved-php-routes.v2.11.1.txttoo. With this PR native answers 174 of the 175 published route methods; only the cancellation-scope checkpoint is left.Also:
rust/tests/execution/command_requests.rsnow registers an update-bound operation and checks its 501.workflow_streams.rsreplaced bystd::result::Result::ok.Qualification
Head
355aa395e, seven commits on main65b11820(#790). The first six replay the qualified working branches onto that base; the only hand edits are the route count in the port log and the CI test list. The seventh is step 2c.0529d87f1(cargo test --all-targets): lib 328, execution 555, upgrade_inspect 2, all passing. At355aa395e: the sevenservice_callstests, includingcatalog_reads_list_the_recorded_calls.service_callstests (includingshared_admission_limits_count_per_boundaryandcatalog_reads_list_the_recorded_calls), the threeservice_endpointstests, and thenamespaces,route_coverageandworkflow_stream_boundstests that the schema and route changes touch.--ignored): PostgreSQL 13, MySQL 19 and MariaDB 19 tests, includingqualification_takeover_keeps_php_catalog_rows_that_native_then_serves. On SQLite,takeover_keeps_php_catalog_rows_that_native_then_servesand the upgrade tests run in the full suite.schema.php, as the hosted job runs it) against the frozen 2.5.14 layout: pass, 49 tables and 333 indexes.service-calls(91 cases, including step 2c's catalog reads) on its SQLite reference and as an app on MySQL 8, MariaDB 10.11 and PostgreSQL 17;service-endpointsandservice-catalogpassed the same way for step 1;service-calls(91),service-endpointsandservice-catalogon all four backends.app/changes. The only PHP is parity harness: the probes and the schema scripts.🤖 Generated with Claude Code