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 @@ -515,6 +515,31 @@ is not a client domain precondition and is not part of the request digest. A
caller may carry a previously observed head revision only as transport
metadata; changing that observation does not create a new semantic operation.

Operation identity names a caller's logical attempt, not the parameter tuple.
Mint an id once outside transport retries and reuse it for that attempt; a UUID
is valid for this purpose. A later independent call may have identical parameters
and still require a new id (for example, setting a value again after another
writer changed it). Hashing parameters forever would replay stale history;
hashing a newly observed provider revision cannot recover the original id after
a commit whose response was lost.

In the shipped claim/update contract, `changed=false` describes Todo/lease state,
not the absence of a storage write: a first accepted named no-change operation
persists its terminal receipt under CAS. Retrying that id after a later state
change must replay the original no-change result, not perform new work. An empty
archive selection has a different, explicit no-transaction contract; do not
generalize it to all verbs. Receipt-only history growth is a real storage cost,
but optimizing it must retain identity consumption, replay and conflict checks.

Current local facades reuse their generated id within managed-runtime retries.
Separate CLI invocations are not implicitly one attempt: claim exposes
`--claim-operation-id`, while create and text/note update do not currently expose
an equivalent cross-process recovery key. That is a caller-recovery limitation,
not proof of duplicate business effects or universal exactly-once execution.
Any extension must define the retry boundary and distinguish retries from new
intent before adding keys or durable attempt tracking. Test lost responses and
intervening writes; a source-level ban on UUID construction proves neither.

For every request, the authority performs this sequence:

1. load the aggregate and provider generation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,24 @@ precondition 与 command parameter;不覆盖 transport retry metadata。Goal-w
携带读到的 head revision,只能把它作为 transport observation;改变该观测不构成
一条新的语义 operation。

Operation identity 标识调用方的一次逻辑尝试,不是参数组合。在 transport retry 外
生成一次 id,并在该尝试内复用;UUID 可以满足这个用途。稍后的独立调用即使参数
相同,也可能需要新 id(例如其他 writer 改值后再次设置原值)。永久按参数哈希会
重放过期历史;按新读到的 provider revision 哈希,也无法在提交后丢响应时找回原 id。

当前 claim/update 合同中的 `changed=false` 描述 Todo/lease 状态,不表示存储零写入:
首次接受一个具名 no-change operation 时,会在 CAS 下保存终结 receipt。状态后来
变化,再重试该 id,必须重放原来的 no-change,不能变成新工作。空 archive selection
有另一套明确的零事务合同,不能推广到所有动词。Receipt-only history 增长确实有
存储成本,但优化时必须保留 identity consumption、重放与冲突校验。

当前本地 facade 在 managed-runtime retry 内复用生成的 id。两次独立 CLI 调用不会
自动视为同一尝试:claim 提供 `--claim-operation-id`,create 和 text/note update
目前没有等价的跨进程恢复 key。这是 caller recovery 的限制,不证明业务效果重复,
也不能宣称通用 exactly-once。扩展前应先定义重试边界、区分 retry 与新 intent,再
决定是否需要 key 或耐久 attempt tracking。用丢响应与中间插入其他写入来验证,
而不是用禁止 UUID 构造的源码扫描代替语义测试。

对每个 request,authority 执行以下顺序:

1. load aggregate 与 provider generation;
Expand Down
15 changes: 13 additions & 2 deletions tests/control_plane_ts/todo_update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,23 @@ test(`provider-first update fails closed without a hard-lease execution proof ($
test("provider-first update records no-change identity without state mutation", async () => {
const {store, request} = await seeded();
const before = await store.loadAuthority();
const result = await executeCoordinationTodoUpdate(store, {...request,
patch: {text: "Old text"}, clear_fields: [], operation_id: "no-change"});
const noChangeRequest = {...request,
patch: {text: "Old text"}, clear_fields: [], operation_id: "no-change"};
const result = await executeCoordinationTodoUpdate(store, noChangeRequest);
assert.equal(result.status, "no_change");
const after = await store.loadAuthority();
assert.equal(after.status, "loaded");
if (before.status !== "loaded" || after.status !== "loaded") return;
assert.deepEqual(after.head, before.head);
assert.notEqual(after.provider_revision, before.provider_revision);
// A consumed no-change identity must not overwrite a later edit on retry.
assert.equal((await executeCoordinationTodoUpdate(store, request)).status, "applied");
const later = await store.loadAuthority();
const replay = await executeCoordinationTodoUpdate(store, noChangeRequest);
assert.equal(replay.status, "replayed");
assert.equal(replay.changed, false);
assert.deepEqual(await store.loadAuthority(), later);
// Identical parameters under a new attempt are new work, not stale replay.
assert.equal((await executeCoordinationTodoUpdate(store, {...noChangeRequest,
operation_id: "independent-reset"})).status, "applied");
});
Loading