Idempotency¶
Running the same thing twice is normal: a job is retried, a response is lost to a timeout, a pipeline re-runs over a store that grew. Datavo answers "what happens then" two ways, and they are not interchangeable.
| Replay | Convergence | |
|---|---|---|
| The question | has this operation already happened? | is this work already done? |
| Keyed on | operation_id, compared exactly |
(producer_id, key), derived from content |
| Applies to | source import, add, attach | attach only |
| You get | the existing operation, no new work | a workset containing only what is missing |
| Resumes partial progress | no | yes |
Both are live on an attach at the same time. Neither replaces the other: replay stops a retried job from opening a second ingest; convergence stops it from recomputing what it already computed.
The guide teaches replay in chapter 4, convergence in chapter 6, and names them side by side in chapter 7. This page is the full rules.
Replay¶
operation_id is yours. Datavo compares it by exact string equality and never
derives, normalizes, or hashes it. Two ids differing by one character are two
operations.
client.create_source_import(SourceImportCreateRequest(
sample_store="voice_corpus_v1",
operation_id="studio-session-4821", # yours; the same one replays
))
client.add_to_store("voice_corpus_v1", operation_id="seed-from-corpus", from_store="...")
client.attach_keys("voice_corpus_v1", "f0.tar", keys=["f0.npy"],
producer_id="f0_v001", operation_id="f0-run-42")
Submitting an operation whose operation_id already exists in that store returns
the existing operation and materializes nothing.
Omit it and you never replay¶
The SDK generates a fresh UUID when you pass no operation_id, so an interactive
call opens a new operation every time — replay is something you opt into by
passing a deterministic id, never something that surprises you. The server always
receives an id; it only ever compares, never invents.
Pass a deterministic id when a retry of the same logical work should collapse into one operation — a nightly import keyed on the session it imports.
Where each kind resolves it¶
- Source import — at create. A replay returns the existing import, so an interrupted upload resumes into it.
- Add — at commit. A replay returns the existing node.
- Attach — at reserve, deliberately: by commit the shard is already uploaded and the duplicate work is done. A replay of an ingest still awaiting its shard hands the upload URL back so the upload resumes; a replay of one whose shard was already accepted returns no upload URL, and the caller polls that ingest to its terminal state instead.
A failed operation releases its id¶
An operation in a terminal failed state does not hold its operation_id
against a retry. Submitting the same id again creates a new operation.
Without this, a deterministic operation_id would be a trap: the first failure
would make that id permanently unusable, and a retried pipeline job could never run
again. The failed operation stays in the store's operation log under its own system
id, so nothing is lost.
source_ref is the deprecated spelling¶
source_ref was the source import's replay key before operation_id existed on
every kind. It still works:
operation_idgiven → used.- only
source_refgiven → it is theoperation_id. - both given with different values → 400. Neither is silently preferred; which one won would be a rule you never read.
Lookups follow the same split: get_source_import_by_operation_id and
list_source_import_samples_by_operation_id are canonical, and the by_source_ref
verbs remain as deprecated aliases (see deprecations). A label
for the batch — a session id, an export job — belongs in metadata, which every
operation carries.
Convergence¶
An attach workset subtracts what the producer has already written:
client.create_workset(filters=[
HasKeys(["audio.wav"]), # inputs you need
Not(AttachedBy(producer_id="f0_v001", output_keys=["f0.npy"])), # minus what's done
])
Two properties follow, and they are why an attach loop selects rather than enumerates:
- Doneness is an empty workset.
sample_count == 0means there is nothing to compute. A re-run after a successful pass is a no-op, not a recompute. - Partial progress resumes. An attach that covered half a store leaves a workset containing exactly the other half. This is the property replay does not give you: replay is all-or-nothing per operation, convergence is per sample.
The boundary is (producer_id, key). A second producer writing the same key
sees every sample as its own work — that is what makes two producers of one key
comparable rather than competing. The same producer sees none.
producer_coverage(store, producer_id) reports the same thing out of loop; it
agrees with the workset for the same producer, store and keys.
Not idempotency: the duplicate-content guard¶
Datavo hashes a source import's committed parts. Re-committing byte-identical parts
into the same store fails the import —
source import archive already committed as <id> — whatever operation_id you
used.
That is a rejection, not a replay: the second import ends in failed and
wait_for_source_import raises, rather than the first import being handed back.
The two are easy to confuse because both stop duplicate data, so the rule is worth
stating plainly:
- same
operation_id→ replay → you get the first operation back, no error. - same bytes, new
operation_id→ rejection → a second operation is opened and then fails.
Look an existing archive up with get_source_import_by_archive(store, "<sha256>").
One word per concept¶
| Write | Not |
|---|---|
| replay | "dedup", "idempotent create", "deduplicate" |
| convergence | "drain", "skip", "incremental", "already-done check" |
| operation id | "source_ref", "idempotency key", "request id", "replay key" |