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
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ The coordinator needs only each operation identity to schedule recovery.
## What Changes

- Project only `operation_id` when listing nonterminal work.
- Project only public receipt fields when clients poll an operation.
- Retain deterministic ordering and the existing state filter.
- Add a database-backed regression proving large payload bytes do not enter the
reconciliation list result.
- Add database-backed regressions proving large payload bytes do not enter
reconciliation or receipt query results.

## Capabilities

Expand All @@ -25,6 +26,6 @@ None.

## Impact

This changes one internal query and its row type in `src/operations.rs`. The
This changes two internal queries and their row types in `src/operations.rs`. The
operation schema, state machine, processing order, HTTP contract, and stored
payloads remain unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ executor fields into the reconciliation list result.
#### Scenario: Terminal operations share the ledger
- **WHEN** committed or rejected operations exist beside nonterminal operations
- **THEN** the reconciliation list excludes terminal identities without loading their payloads

### Requirement: Receipt lookup excludes request payloads

The durable coordinator SHALL return an operation receipt by projecting only
the fields in the public receipt contract. It MUST NOT load the stored request
payload into the receipt query result.

#### Scenario: A client polls an operation with a large request payload
- **WHEN** the client retrieves that operation's receipt by its stable identity
- **THEN** the query result contains every public receipt field
- **AND** the query result does not contain the stored request payload
89 changes: 85 additions & 4 deletions src/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,33 @@ struct DbOperationId {

const LIST_NONTERMINAL_OPERATION_IDS_QUERY: &str = "SELECT operation_id FROM memory_operation WHERE state NOT IN ['committed', 'rejected'] ORDER BY operation_id ASC";

#[derive(Debug, Deserialize, SurrealValue)]
struct DbOperationReceipt {
operation_id: String,
schema_version: u32,
kind: String,
dependencies: Vec<String>,
payload_hash: String,
state: String,
blocked_by: Vec<String>,
result: Option<Value>,
error: Option<String>,
executor_generation: u64,
#[serde(default)]
executor_progress_seq: u64,
#[serde(default)]
executor_exit_count: u64,
#[serde(default)]
executor_last_exit: Option<String>,
#[serde(default)]
executor_error: Option<String>,
progress_seq: u64,
created_at: Datetime,
updated_at: Datetime,
}

const GET_OPERATION_RECEIPT_QUERY: &str = "SELECT operation_id, schema_version, kind, dependencies, payload_hash, state, blocked_by, result, error, executor_generation, executor_progress_seq, executor_exit_count, executor_last_exit, executor_error, progress_seq, created_at, updated_at FROM memory_operation WHERE operation_id = $id LIMIT 1";

#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
struct DbOperationEvent {
#[serde(default)]
Expand Down Expand Up @@ -250,6 +277,32 @@ impl TryFrom<DbOperation> for OperationReceipt {
}
}

impl TryFrom<DbOperationReceipt> for OperationReceipt {
type Error = anyhow::Error;

fn try_from(value: DbOperationReceipt) -> Result<Self> {
Ok(Self {
operation_id: value.operation_id,
schema_version: value.schema_version,
kind: value.kind,
payload_hash: value.payload_hash,
dependencies: value.dependencies,
state: parse_state(&value.state)?,
blocked_by: value.blocked_by,
result: value.result,
error: value.error,
executor_generation: value.executor_generation,
executor_progress_seq: value.executor_progress_seq,
executor_exit_count: value.executor_exit_count,
executor_last_exit: value.executor_last_exit,
executor_error: value.executor_error,
progress_seq: value.progress_seq,
created_at: value.created_at.to_string(),
updated_at: value.updated_at.to_string(),
})
}
}

impl From<DbOperationEvent> for OperationEvent {
fn from(value: DbOperationEvent) -> Self {
Self {
Expand Down Expand Up @@ -427,10 +480,13 @@ impl OperationService {

pub async fn get(&self, operation_id: &str) -> Result<Option<OperationReceipt>> {
let db = self.surreal()?.db()?;
let operation: Option<DbOperation> = db
.select(("memory_operation", record_key(operation_id)))
.await?;
operation.map(TryInto::try_into).transpose()
let mut rows: Vec<DbOperationReceipt> = db
.query(GET_OPERATION_RECEIPT_QUERY)
.bind(("id", operation_id.to_owned()))
.await?
.check()?
.take(0)?;
rows.pop().map(TryInto::try_into).transpose()
}

async fn get_db(&self, operation_id: &str) -> Result<Option<DbOperation>> {
Expand Down Expand Up @@ -2037,6 +2093,31 @@ mod tests {
.expect("nonterminal operation identity is projected");
assert_eq!(row.as_object().unwrap().len(), 1);
assert!(!serde_json::to_string(&rows).unwrap().contains(sentinel));

let receipt = service
.get("projected-reconciliation")
.await
.unwrap()
.expect("operation receipt");
assert_eq!(receipt.operation_id, "projected-reconciliation");
let receipt_rows: Vec<Value> = storage
.db()
.unwrap()
.query(GET_OPERATION_RECEIPT_QUERY)
.bind(("id", "projected-reconciliation".to_owned()))
.await
.unwrap()
.check()
.unwrap()
.take(0)
.unwrap();
let receipt_row = receipt_rows.first().expect("projected receipt row");
assert!(!receipt_row.as_object().unwrap().contains_key("payload"));
assert!(
!serde_json::to_string(&receipt_rows)
.unwrap()
.contains(sentinel)
);
}

#[tokio::test]
Expand Down
Loading