Skip to content

Latest commit

 

History

History
126 lines (83 loc) · 23.3 KB

CrossPartitionEdges.md

File metadata and controls

126 lines (83 loc) · 23.3 KB

Cross-partition graph edges

Status: accepted cross-partition edge implementation contract; source and qualification gates remain open.

Canonical slice: GraphTraversal. Requirements: REQ-GRAPH-XPART-001..005. Acceptance: AC-GRAPH-XPART-001..005. Implementation contract: ADR-102.

Boundary

The source partition is the existing CommandRequest.Partition and must equal the complete UpsertEdge.From.Partition. EdgeId remains scoped to the complete source partition and graph. Endpoints retain full EntityRef values. The source partition owns the canonical edge and outgoing adjacency. A different destination atomic partition is reached by the source partition grain calling the destination partition grain through Orleans, which routes the call to the ZoneTree owners holding that partition's copies; no record or storage handle moves between partitions. The source edge mutation, its generated intent and the normal source outbox entry are committed by the existing admitted replicated command and atomic apply path. Destination adjacency and source completion use separate normal replicated commits; the feature never claims a two-partition atomic transaction.

The connection-grain CQRS boundary remains unchanged. Delivery and completion are distinct bounded typed requests. Existing local-partition graph reads and local incoming/outgoing keys stay unchanged. Source outgoing reads load the canonical EdgeRecord; destination reverse records are derived and never become canonical edge data.

Persisted contracts

All contracts use generated Orleans serialization. aliases and field IDs are stable as listed below; the root integrator registers them in the central alias inventory and dispatch joins.

Contract Alias Field IDs
GraphEdgeOwnerVersion keyload.contract.graph-edge-owner-version 0 version=1; 1 monotonic revision; 2 deleted
GraphCrossPartitionDeliveryIntent keyload.contract.graph-cross-partition-delivery-intent 0 version=1; 1 source partition; 2 graph; 3 edge ID; 4 full destination EntityRef; 5 revision; 6 deleted; 7 full edge snapshot; 8 original persisted principal ID; 9 original policy epoch; 10 fingerprint
ApplyCrossPartitionReverseEdge mutation keyload.contract.apply-cross-partition-reverse-edge 0 source partition; 1 graph; 2 edge ID; 3 full destination EntityRef; 4 expected revision
CompleteCrossPartitionReverseEdge mutation keyload.contract.graph-reverse-edge-completion 0 source partition; 1 graph; 2 edge ID; 3 full destination EntityRef; 4 expected revision
GraphCrossPartitionReceiverState keyload.contract.graph-cross-partition-receiver-state 0 version=1; 1 source partition; 2 graph; 3 edge ID; 4 full destination; 5 revision; 6 deleted; 7 fingerprint
GraphCrossPartitionCapacity keyload.contract.graph-cross-partition-capacity 0 version=1; 1 direction; 2 record count (long); 3 encoded bytes (long)
GraphCrossPartitionCapacityDirection keyload.contract.graph-cross-partition-capacity-direction enum values: PendingIntents=0; ReceiverStates=1; other values fail validation
GraphCrossPartitionFingerprint keyload.contract.graph-cross-partition-fingerprint 0 version=1; 1 source partition; 2 graph; 3 edge ID; 4 full destination; 5 revision; 6 deleted; 7 full edge snapshot
ReadIncomingGraphEdgesRequest keyload.contract.graph-incoming-edges-request 0 version; 1 full target EntityRef; 2 graph; 3 limit
GraphIncomingEdgeRow keyload.contract.graph-incoming-edge-row 0 full canonical EdgeRecord; 1 delivered revision
GraphIncomingEdgesPage keyload.contract.graph-incoming-edges-page 0 version; 1 immutable rows; 2 cut position; 3 projection identity fixed to eventual-reverse

The owner-version key is source-partition scoped by graph and edge ID and persists after edge deletion so an edge ID cannot restart at revision 1. Intent keys additionally include the complete destination partition. There is one coalesced desired-state intent per source edge and destination: an endpoint change writes the old destination tombstone and new destination active intent at the same newly incremented source revision. Receiver and remote reverse-adjacency keys are target-partition scoped and include the complete source partition, graph and edge ID; same edge IDs from other source partitions cannot collide.

Fingerprint is lowercase SHA-256 over generated serialization of the exact versioned delivery tuple (source and target full references, revision, disposition, and full edge snapshot). The persisted receiver high-water state never expires in this stage. Tombstones remain as high-water state.

REQ/AC-GRAPH-XPART-001 additionally requires this digest to depend on tuple values, never on CLR object sharing. Before encoding, construct the same fixed tree of independently owned source/destination/edge endpoint reference records and independently owned string occurrences, including exact attributes content. Equal references decoded from separate canonical-edge and intent frames must produce the same digest as shared references at source commit. Preserve every tuple field, exact attributes bytes, generated aliases/field IDs and execution envelope; do not alter global serializer/reference semantics or use JSON fallback. GraphCrossPartitionFingerprintTests proves independent frame equality, object-sharing independence and full-reference/content sensitivity.

This is a correction before the first qualified cross-partition feature release. Earlier unqualified development digests are not a compatible persisted generation: retain their original evidence and fail closed on mismatches. Do not silently rewrite, accept a legacy digest or use a dual decoder. Existing feature data must be writer-stopped and explicitly rebuilt under a separately qualified migration before it can be counted as accepted upgrade evidence.

Capacity records validate version, direction, nonnegative counts/bytes and the actual configured bounds before use. Checked delta accounting is part of the same transaction as its owned intent or receiver state. Missing capacity is an empty initial state only when the corresponding family is empty; contradictory capacity/family state is Corruption. The generated fingerprint tuple is the single canonical digest input; no JSON serialization or caller-supplied digest is used as authority.

Mutation and delivery rules

Both new mutations use the existing public CommandRequest / OperationKind.Batch JSON union and SDK CommitAsync. Their frozen kind discriminators are applyCrossPartitionReverseEdge and completeCrossPartitionReverseEdge; retain all existing discriminators. Their inherited Mutation.Resource equals Graph and is never an alternate authority scope. aliases and per-type field IDs stay exactly as in the table above, including owner-version revision/deleted IDs 1/2, intent revision/deleted IDs 5/6 and receiver fingerprint ID 7. A class must not reuse an ID for two of its own fields. Independent JSON/union round trips and alias/ID oracles cover both mutation variants and every new stored contract before public cluster consumption.

Every source upsert or delete bounded-scans its existing intents for that graph and edge. It advances every still-pending noncurrent destination to a tombstone at the new source owner revision, then writes the current active destination when applicable. The previous canonical destination is tombstoned even when its former intent already completed. This prevents A→B→C updates from leaving A's unresolved tombstone at a revision older than the owner and therefore permanently stale. The per-edge scan is capped by MaxScanRecords, with a lookahead overflow rejecting the entire transaction; checked capacity accounting applies to all changed intents. A multi-hop update before repair must clean every formerly delivered destination without resurrecting an old edge. This flow is mapped to GraphCrossPartitionSourceCommitTests and GraphCrossPartitionRepairTests.

UpsertEdge requires source From.Partition == CommandRequest.Partition, validates both endpoint collection rows and persisted row grants. It checks source and destination graph-write scopes. Owner revision increases with checked arithmetic. It commits canonical edge/outgoing adjacency, desired-state delivery intent(s), and the existing outbox row in the same source transaction. Upsert to a new destination tombstones the previous destination and writes the new destination's active desired state. DeleteEdge increments the persistent owner revision and writes a destination tombstone in the same transaction as canonical edge/index deletion.

A bounded pending-intent read exposes only delivery locators (not edge payload) under current persisted graph-write authority; active candidates additionally require current endpoint collection read grants/row visibility. It returns at most min(MaxResults, MaxScanRecords) candidates and charges encoded results to MaxBatchBytes. Repair repeatedly reads a bounded prefix and invokes the ordinary receiver request; no task-per-edge fanout, unbounded background retry, or alternate dispatcher exists. Pending intent records are capped per source partition at MaxScanRecords records and MaxBatchBytes bytes, using an atomic persisted capacity counter. An update reserves only its positive byte/record delta before writing; failed reservation rejects the whole source transaction as ResourceExhausted. Destination receiver rows are capped by the same record/byte limits; exhaustion rejects the entire target transaction and stays visible.

ApplyCrossPartitionReverseEdge uses the existing Batch mutation route with CommandRequest.Partition == Destination.Partition. The supplied fields are locators only; no supplied revision, digest, intent, role, or receipt is trusted. In the destination transaction it reloads the exact source intent, owner-version and canonical edge/tombstone from the same current view, validates current caller persisted grants on both graph scopes, then checks the receiver high-water state. The current caller is reauthorized; frozen original principal/policy fields are audit/fingerprint metadata and never confer authority. Revocation blocks target apply and source completion before either write. Active apply also requires both current endpoint rows to be visible/readable. Tombstone cleanup requires current graph-write grants but does not require a deleted endpoint row to be readable.

An exact duplicate revision/fingerprint is an idempotent no-op. A lower revision is a typed stale/no-change outcome. An equal revision with different fingerprint is Conflict; persisted receiver/source inconsistencies are Corruption. A higher active revision atomically writes derived reverse adjacency and receiver high-water state; a higher tombstone atomically removes derived adjacency and preserves receiver high-water state. A caller message for an older intent after source coalescing is a stale no-op; it cannot publish stale payload.

CompleteCrossPartitionReverseEdge is a distinct source-partition Batch mutation, also locator-only. In the source transaction it reloads the exact current pending intent and owner version, plus the committed destination receiver state obtained through an Orleans call to the destination partition. It retires/account-debits the intent only when destination revision, disposition and fingerprint equal the current intent. Any caller-supplied proof is ignored. If target commit succeeded but completion failed, repair repeats idempotently then completes. If the source intent was superseded, completion is a stale no-op and the newer current intent stays pending.

Current graph reads preserve existing vertex privacy: a missing, deleted or hidden endpoint is not returned or expanded. Vertex deletion does not cascade a remote canonical edge; it hides the edge while current endpoint visibility fails. Active delivery checks current endpoint visibility and visibly fails without writing if an endpoint was removed. Tombstone delivery can clean a removed endpoint's projection. Recreating a document at the same full EntityRef follows current graph visibility behavior; it does not create a new edge revision.

ReadIncomingGraphEdgesRequest is the real public consumer of the reverse projection. It is an additive read-only capability through the connection-grain execution path and CQRS stream. The request's target partition selects the node-local read owner; the reader performs one current Store.Read, checks the existing GraphRead capability for both target and source graph scopes and current DocumentsRead/row visibility for the target and every source endpoint, and reloads each source canonical edge plus owner-version from that same view. It returns only rows whose current canonical edge, receiver revision, full endpoint references and fingerprint match and whose endpoints are currently visible. Missing, hidden, deleted or not-yet-delivered rows are omitted under the declared eventual projection contract; the projection is not complete graph truth and never invents source authority.

The Core seam is DatabaseEngine.ReadIncomingGraphEdges(string principalId, ReadIncomingGraphEdgesRequest request, TimeProvider? timeProvider = null, CancellationToken cancellationToken = default). Append GrainReadKind.GraphIncomingEdges without renumbering existing values. The existing GrainCoreReadCapabilities whitelist and switch decode with GrainPayload.ReadPublicInput and forward the original token by its named cancellationToken argument. Public surfaces are SDK IncomingEdgesAsync, HTTP POST /graph/incoming and read-only/idempotent/nondestructive MCP keyload_graph_incoming; no second dispatcher or grain boundary is added.

The read limit must be 1..MaxResults. The range visit is bounded by MaxScanRecords; every range and point read, canonical-edge verification, serialized request/result and retained row is charged through the existing ReadExecutionBudgetReadGrant/BudgetedReadView and original deadline/cancellation. If more eligible rows exist than the limit or any work/result bound is exceeded, the whole call fails BudgetExceeded without returning a truncated page. No cursor, snapshot-completeness claim, or partial-success result exists. The page's fixed eventual-reverse projection label makes the consistency mode explicit.

Incoming reads include both existing same-partition canonical incoming adjacency and delivered cross-partition reverse adjacency in the same read cut. Both scans and all point checks share one combined work/byte/retention/result grant. Identity is the full source partition, graph and edge ID; equal textual IDs in different source partitions remain distinct. Local canonical rows use DeliveredRevision=0, because they require no receiver delivery; their actual canonical revision remains in Edge.Revision. Cross-partition rows use the verified positive receiver owner revision. Local adjacency without its matching canonical edge is Corruption; undelivered cross-partition rows remain absent under the eventual projection. Before returning either row kind, apply the existing Authorization.Project behavior to the canonical source graph's FieldPolicies and AttributesJson; fingerprint verification uses the original canonical snapshot before this public projection. Hidden fields cannot escape through the new row wrapper. Local-only, mixed local/cross, same-ID, hidden-attribute and combined exact/one-over budget cases are mandatory reader tests.

flowchart LR
    caller[SDK or official MCP] --> request[Connection grain and CQRS]
    request --> source[Source commit: edge, version and intents]
    source --> target[Separate target commit: reverse row and high-water]
    target --> complete[Separate source commit: retire verified intent]
    request --> read[One read cut: local incoming and delivered reverse rows]
    read --> privacy[Current grants, endpoint visibility and field projection]
Loading

Requirements and acceptance

Requirement Acceptance and proof
REQ-GRAPH-XPART-001: canonical source ownership and committed intent AC-GRAPH-XPART-001: actual ZoneTree transaction stores source edge, monotonic owner version, coalesced intent and existing outbox record together; rollback leaves none. Full refs succeed; endpoint source mismatch and source revision overflow leave canonical state unchanged. GraphCrossPartitionSourceCommitTests.
REQ-GRAPH-XPART-002: ordered idempotent reverse projection AC-GRAPH-XPART-002: actual store duplicate application is idempotent; lower versions never replace high-water; equal version/different fingerprint returns Conflict; tombstone then older active never resurrects; endpoint replacement tombstones old destination and applies new one. GraphCrossPartitionReceiverTests.
REQ-GRAPH-XPART-003: current persisted authorization and privacy AC-GRAPH-XPART-003: current caller must have source+destination graph write and endpoint collection read/row visibility for active apply; revoke/hidden/deleted endpoint prevents write. Tombstones require graph write but reveal no endpoint data. Full PartitionRef/EntityRef scopes prevent same-ID cross-tenant collisions. GraphCrossPartitionAuthorizationTests.
REQ-GRAPH-XPART-004: bounded repair and visible failures AC-GRAPH-XPART-004: candidate pages are bounded by actual limits; exact/excess capacity, corrupt counter, malformed intent, grant denial and receiver commit failure remain visible with no false source completion. Target success/source-completion failure safely replays; current receiver state is the only completion oracle. GraphCrossPartitionRepairTests.
REQ-GRAPH-XPART-005: connection-grain execution and real clients AC-GRAPH-XPART-005: root-owned Aspire cluster .NET SDK and official MCP cases create/read/update/delete cross-partition edges on distinct partitions, compare source/target commits and outgoing/incoming reads, test duplicate/reordered repair and denied grant, and show every committed write on the KeyLoad:ReplicaCount copies. The incoming query declares eventual projection scope, enforces real storage/read/result budgets and never claims completeness. GraphCrossPartitionClusterTests; actual source SHA/originals required.

The graph edge is intentionally eventual across partitions: source commit success means canonical source data and retained intent are committed, not that destination reverse adjacency is already applied. The declared window is measured from source commit to destination commit in cluster evidence; on failure, lag and typed failure remain observable. No fixed latency guarantee is asserted before those originals exist.

Ownership and verification

Frontend: N/A; no separate graph UI. SQL: N/A for this first edge transport stage; SQL must use the same authorized graph operation in a separately joined Query compiler stage. Global graph queries, an automatic repair service and external exactly-once processing remain outside this stage.

Incoming read public continuation

Test-only extension of SAME GraphIncomingClusterTests.PersistedSourceGrantHidesCrossRowsAndTargetGraphDenialIsExplicit; original declaration/UID/Args/deadline/execution50/exclusiveheavy1 preserved. REQ/AC-AUTH-005/007, REQ/AC-GRAPH-XPART-003/005 and REQ-GRAPH-002 under ADR-014/102/125/117. Existing GraphIncomingClusterScenario explicitly verifies three distinct atomic partitions. No public/persisted/product/options/clock seam.

Ordered flow: preserve original target-only source exclusion and target GraphRead denial. Retain full-reader original grants. Healthy admitted official MCP/SDK owner enters all four direct SDK/official MCP/Q1SDK/Q1MCP incoming routes through existing RequestCqrsClusterCallers and SqlClusterProtocol. Independent three-row literals retain all complete edges, full references, delivered revisions, redacted attributes and page version/projection; each call owns a fresh positive current cut. Persist epoch2 source database grant removal (local row only); epoch3 target DocumentsRead removal (PermissionDenied, no page); epoch4 original scoped repair (complete three rows); epoch5 Revoked (Unauthenticated, no protected page); epoch6 exact original scoped repair (complete three rows). All changes through existing authorized ConfigurePrincipal; full returned principal values compared, not caller role. Original MCP owner is created while healthy before revoke, then original requests reauthorize persisted state.

Each state operation brackets complete current administrator document images for ALL four vertices, all three partition outbox states and complete actual administrator incoming edge rows (cut observed independently, logical rows exact). All four refusal envelopes preserve errors and omit credentials/private marker; no global physical-position equality across replicated system commits or synthesized cut. Existing root administrator acts only as setup/current-state observer, never as substitute for reader admission.

Execute one real root expectedRevision1 PutDocument on original local source with identical literal document, keep exact original command/full receipt. All four stable-ID retries must return full original receipt and identical complete model/outbox images. Capture actual pre-stop NodeStatus per original replica node. Dispose caller sessions first; same fixture Kill ALL, BeginRestart ALL, Restart ALL, original readiness joined under SAME whole-flow token. Fresh callers/status after actual same-volume restart: exact NodeId/incarnation, nondecreasing ReadGeneration/Applied, full reader page/vertices, all four original receipt replays no second revision/outbox effect, then genuine fresh root expectedRevision2 write and complete healthy literal results. No cache/grant/owner/protocol bypass, new limits, retries or timeout changes.

Failure ownership uses existing RequestCqrsClusterCallers.RunOwnedAsync and ServerFailureObserver joined ledgers, fixture owns topology/root cleanup; original failures retained before disposal. New helpers only after NotSupported creation checks. Private Integration graph build/discovery plus relevant Unit/API controls allowed; Docker heavy1 waits actual handoff. discovery must bind original UID, no guessed count/identity. Linux full-solution, cluster, coverage and cache/Explain gates remain OPEN until genuine results.

refusal-envelope preflight

Actual existing SqlModelViewClusterAuthorizationAssertions.AssertDeniedAsync establishes that a revoked persisted credential reaches MCP HTTP authentication before tool dispatch: official Client CallAsync throws HttpRequestException with HTTP401; no structured dispatched error is fabricated. Preserve both genuine official direct/Q1 denied calls, exact Unauthorized status, and bounded original McpUnauthorizedProbe safe Problem/no operation header under the same credential/node. Capability refusals remain dispatched PermissionDenied structured envelopes. SDK generic result no-body assertions distinguish reference Value null from JsonElement Undefined/Null, preserving the actual ExecuteSqlAsync<Result> type. No fallback or accepted-success branch.

target collection privacy fence

GraphIncomingEdgesReader.RequireVisible→TryRequireVisible catches target collection PermissionDenied as hidden visibility and returns exact NotFound. Epoch3 target DocumentsRead reduction requires NotFound/no page, independently from preserved original target GraphRead PermissionDenied. Source GraphRead absence omits remote rows. Original principals/policies and all public codes stay unchanged.