@feltdb/core 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-identity.json +13 -4
- package/dist/cli/commands.js +96 -45
- package/dist/cli/workspace-integration.js +8 -0
- package/dist/collection.d.ts +41 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +116 -47
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +11 -1
- package/dist/create/build-identity.json +13 -4
- package/dist/create/cli.js +49 -40
- package/dist/create/create.js +32 -19
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +18 -3
- package/dist/create/server-source/Cargo.toml +4 -2
- package/dist/create/server-source/crates/feltdb/Cargo.toml +20 -0
- package/dist/create/server-source/crates/feltdb/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb/README.md +75 -0
- package/dist/create/server-source/crates/feltdb/src/agent_state.rs +320 -0
- package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +1545 -0
- package/dist/create/server-source/crates/feltdb/src/authority.rs +145 -15
- package/dist/create/server-source/crates/feltdb/src/lib.rs +227 -23
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +204 -1
- package/dist/create/server-source/crates/feltdb/src/workload.rs +685 -53
- package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +1188 -0
- package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +2884 -0
- package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +534 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +23340 -0
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +18 -12
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +17 -5
- package/dist/create/server-source/crates/feltdb-client/Cargo.toml +29 -0
- package/dist/create/server-source/crates/feltdb-client/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb-client/README.md +70 -0
- package/dist/create/server-source/crates/feltdb-client/src/application.rs +527 -0
- package/dist/create/server-source/crates/feltdb-client/src/authority.rs +679 -0
- package/dist/create/server-source/crates/feltdb-client/src/control.rs +292 -0
- package/dist/create/server-source/crates/feltdb-client/src/error.rs +227 -0
- package/dist/create/server-source/crates/feltdb-client/src/events.rs +146 -0
- package/dist/create/server-source/crates/feltdb-client/src/lib.rs +466 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/README.md +4 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/private-ca.pem +20 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server-key.pem +28 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server.pem +20 -0
- package/dist/create/server-source/crates/feltdb-client/tests/private_ca.rs +95 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +20 -1
- package/dist/create/server-source/crates/feltdb-server/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +12 -1
- package/dist/create/server-source/crates/feltdb-server/src/agent_execution.rs +511 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +28 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +6 -2
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +21 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +845 -138
- package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +683 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +3 -1
- package/dist/create/setup-summary.js +40 -0
- package/dist/create/template/default-project/README.md +12 -7
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +1 -1
- package/dist/create/template/dot-feltdb-README.md +3 -3
- package/dist/db.d.ts +27 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +60 -21
- package/dist/development-runtime-bridge.d.ts +20 -0
- package/dist/development-runtime-bridge.d.ts.map +1 -1
- package/dist/development-runtime-bridge.js +74 -4
- package/dist/embedded-transaction.d.ts +75 -8
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +124 -18
- package/dist/error-codes.d.ts +2 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +2 -0
- package/dist/feltdb.d.ts +1 -11
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +6 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +43 -11
- package/dist/http-db.d.ts +13 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +33 -4
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +65 -41
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +29 -8
- package/dist/studio/{KeyManagementPanel-DZuSeWBK.js → KeyManagementPanel-skUNnKRw.js} +36 -28
- package/dist/studio/app.d.ts +8 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
- package/dist/studio/components/KeyManagementPanel.js +1 -1
- package/dist/studio/components/RouteErrorBoundary.d.ts +5 -0
- package/dist/studio/components/RouteErrorBoundary.d.ts.map +1 -0
- package/dist/studio/components/index.js +2 -2
- package/dist/studio/{components-Bivm2lX6.js → components-BSGmHfif.js} +6 -6
- package/dist/studio/control-plane/Agents.d.ts.map +1 -1
- package/dist/studio/control-plane/Data.d.ts.map +1 -1
- package/dist/studio/control-plane/Instance.d.ts +4 -1
- package/dist/studio/control-plane/Instance.d.ts.map +1 -1
- package/dist/studio/control-plane/Operations.d.ts +4 -1
- package/dist/studio/control-plane/Operations.d.ts.map +1 -1
- package/dist/studio/control-plane/Overview.d.ts.map +1 -1
- package/dist/studio/control-plane/Schema.d.ts.map +1 -1
- package/dist/studio/control-plane/context.d.ts +4 -1
- package/dist/studio/control-plane/context.d.ts.map +1 -1
- package/dist/studio/control-plane/index.js +2 -2
- package/dist/studio/control-plane/primitives.d.ts +24 -0
- package/dist/studio/control-plane/primitives.d.ts.map +1 -1
- package/dist/studio/{control-plane-CLm69XwX.js → control-plane-CaBA7Rgv.js} +1374 -1298
- package/dist/studio/index.d.ts +1 -0
- package/dist/studio/index.d.ts.map +1 -1
- package/dist/studio/index.js +415 -237
- package/dist/studio/navigation.d.ts +57 -0
- package/dist/studio/navigation.d.ts.map +1 -0
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/control-plane-api.d.ts +2 -0
- package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
- package/dist/studio/utils/control-plane-api.js +9 -4
- package/dist/studio/utils/service-api.d.ts.map +1 -1
- package/dist/studio/utils/service-api.js +5 -4
- package/dist/studio-app/assets/{feltdb_wasm-DACcD-zs.js → feltdb_wasm-3hDngl1h.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CFQT4gCy.wasm +0 -0
- package/dist/studio-app/assets/index-Dql1hS5m.js +30 -0
- package/dist/studio-app/assets/{index-CiC8VY-a.css → index-vi21oEQa.css} +1 -1
- package/dist/studio-app/index.html +2 -2
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +4 -3
- package/dist/transaction-identity.d.ts +62 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +138 -0
- package/dist/transaction.d.ts +50 -5
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +28 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/wasm/package.json +1 -1
- package/dist/workspace/local-development-authority.d.ts +19 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +335 -15
- package/package.json +1 -1
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/studio-app/assets/feltdb_wasm_bg-BejThWID.wasm +0 -0
- package/dist/studio-app/assets/index-CoZaxnlm.js +0 -30
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
//! the socket boundary.
|
|
6
6
|
|
|
7
7
|
use crate::{
|
|
8
|
-
AtomicCommit, AtomicMutation, AtomicPrecondition, FeltDb,
|
|
8
|
+
AtomicCommit, AtomicMutation, AtomicPrecondition, FeltDb, PayloadBinding, RecordPrecondition,
|
|
9
|
+
StoredRow,
|
|
9
10
|
};
|
|
10
11
|
use async_trait::async_trait;
|
|
11
12
|
use serde::{Deserialize, Serialize};
|
|
@@ -120,8 +121,78 @@ impl EmbeddedAuthority {
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
/// Names the construction of the payload digest the authority binds ids to.
|
|
125
|
+
///
|
|
126
|
+
/// The prefix is stored with the digest in the durable transaction record, so a
|
|
127
|
+
/// digest written by a different construction is recognizable as one this
|
|
128
|
+
/// build cannot judge, instead of being read as a mismatch.
|
|
129
|
+
const PAYLOAD_DIGEST_PREFIX: &str = "authority-tx1:sha256:";
|
|
130
|
+
|
|
131
|
+
/// Canonical text of a JSON value: object members sorted by name at every
|
|
132
|
+
/// depth, array order preserved. Written out here rather than left to
|
|
133
|
+
/// `serde_json`'s map ordering, which is a build feature that another crate in
|
|
134
|
+
/// the dependency graph can change.
|
|
135
|
+
fn canonical_json(value: &Value, out: &mut String) {
|
|
136
|
+
match value {
|
|
137
|
+
Value::Object(map) => {
|
|
138
|
+
let mut keys: Vec<&String> = map.keys().collect();
|
|
139
|
+
keys.sort();
|
|
140
|
+
out.push('{');
|
|
141
|
+
for (index, key) in keys.into_iter().enumerate() {
|
|
142
|
+
if index > 0 {
|
|
143
|
+
out.push(',');
|
|
144
|
+
}
|
|
145
|
+
out.push_str(&Value::String(key.clone()).to_string());
|
|
146
|
+
out.push(':');
|
|
147
|
+
canonical_json(&map[key], out);
|
|
148
|
+
}
|
|
149
|
+
out.push('}');
|
|
150
|
+
}
|
|
151
|
+
Value::Array(items) => {
|
|
152
|
+
out.push('[');
|
|
153
|
+
for (index, item) in items.iter().enumerate() {
|
|
154
|
+
if index > 0 {
|
|
155
|
+
out.push(',');
|
|
156
|
+
}
|
|
157
|
+
canonical_json(item, out);
|
|
158
|
+
}
|
|
159
|
+
out.push(']');
|
|
160
|
+
}
|
|
161
|
+
other => out.push_str(&other.to_string()),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// The digest an id is bound to: everything the commit acts on or records, and
|
|
166
|
+
/// not the id itself. `audit` is included because it is committed evidence the
|
|
167
|
+
/// caller supplied, not transport metadata; two requests that record different
|
|
168
|
+
/// audit facts under one id are different requests.
|
|
169
|
+
fn payload_digest(transaction: &AuthorityTransaction) -> Result<String, AuthorityError> {
|
|
170
|
+
let semantic = serde_json::json!({
|
|
171
|
+
"base_revision": transaction.base_revision,
|
|
172
|
+
"preconditions": transaction.preconditions,
|
|
173
|
+
"record_preconditions": transaction.record_preconditions,
|
|
174
|
+
"mutations": transaction.mutations,
|
|
175
|
+
"audit": transaction.audit,
|
|
176
|
+
});
|
|
177
|
+
let mut canonical = String::new();
|
|
178
|
+
canonical_json(&semantic, &mut canonical);
|
|
179
|
+
Ok(format!(
|
|
180
|
+
"{PAYLOAD_DIGEST_PREFIX}{:x}",
|
|
181
|
+
Sha256::digest(canonical.as_bytes())
|
|
182
|
+
))
|
|
183
|
+
}
|
|
184
|
+
|
|
123
185
|
fn authority_error(error: crate::FlowError) -> AuthorityError {
|
|
124
186
|
match error {
|
|
187
|
+
crate::FlowError::TransactionIdPayloadMismatch { transaction_id: id } => {
|
|
188
|
+
AuthorityError {
|
|
189
|
+
code: "IDEMPOTENCY_CONFLICT".into(),
|
|
190
|
+
message: format!(
|
|
191
|
+
"transaction {id} was already committed with a different payload"
|
|
192
|
+
),
|
|
193
|
+
authoritative_revision: None,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
125
196
|
crate::FlowError::RevisionConflict {
|
|
126
197
|
expected: _,
|
|
127
198
|
actual,
|
|
@@ -152,17 +223,54 @@ impl FeltDbAuthority for EmbeddedAuthority {
|
|
|
152
223
|
&self,
|
|
153
224
|
transaction: AuthorityTransaction,
|
|
154
225
|
) -> Result<AtomicCommit, AuthorityError> {
|
|
155
|
-
|
|
226
|
+
let digest = payload_digest(&transaction)?;
|
|
227
|
+
// How the id is already known, if it is. A recorded hash never changes
|
|
228
|
+
// and a missing one never appears, so this read cannot be invalidated
|
|
229
|
+
// by a concurrent commit: the comparison that matters -- the id against
|
|
230
|
+
// the digest of this request -- is made by the core, atomically with
|
|
231
|
+
// the commit, under the lock that serializes every writer.
|
|
232
|
+
let id = transaction.transaction_id.as_str();
|
|
233
|
+
let applied = self.db.has_applied_transaction(id).map_err(authority_error)?;
|
|
234
|
+
let recorded = if applied {
|
|
235
|
+
self.db
|
|
236
|
+
.applied_transaction_payload_hash(id)
|
|
237
|
+
.map_err(authority_error)?
|
|
238
|
+
} else {
|
|
239
|
+
None
|
|
240
|
+
};
|
|
241
|
+
let (bound, legacy) = match (applied, recorded) {
|
|
242
|
+
(false, _) => (Some(digest), false),
|
|
243
|
+
// Committed before ids were bound: the id is all there is to compare.
|
|
244
|
+
(true, None) => (None, true),
|
|
245
|
+
(true, Some(hash)) if hash.starts_with(PAYLOAD_DIGEST_PREFIX) => (Some(digest), false),
|
|
246
|
+
// Recorded by a construction this build does not know, or by another
|
|
247
|
+
// writer's: neither a match nor a mismatch can be established.
|
|
248
|
+
(true, Some(_)) => {
|
|
249
|
+
return Err(AuthorityError {
|
|
250
|
+
code: "IDEMPOTENCY_UNVERIFIABLE".into(),
|
|
251
|
+
message: format!(
|
|
252
|
+
"transaction {id} exists, but its recorded payload identity cannot be verified by this build"
|
|
253
|
+
),
|
|
254
|
+
authoritative_revision: None,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
let mut commit = self
|
|
259
|
+
.db
|
|
156
260
|
.apply_atomic_transaction_guarded(
|
|
157
|
-
|
|
158
|
-
|
|
261
|
+
id,
|
|
262
|
+
bound.as_deref(),
|
|
159
263
|
transaction.base_revision,
|
|
160
264
|
&transaction.preconditions,
|
|
161
265
|
&transaction.record_preconditions,
|
|
162
266
|
&transaction.mutations,
|
|
163
|
-
transaction.audit,
|
|
267
|
+
transaction.audit.clone(),
|
|
164
268
|
)
|
|
165
|
-
.map_err(authority_error)
|
|
269
|
+
.map_err(authority_error)?;
|
|
270
|
+
if legacy && commit.duplicate {
|
|
271
|
+
commit.payload_binding = Some(PayloadBinding::LegacyUnverified);
|
|
272
|
+
}
|
|
273
|
+
Ok(commit)
|
|
166
274
|
}
|
|
167
275
|
}
|
|
168
276
|
|
|
@@ -267,6 +375,17 @@ impl AuthorityClient {
|
|
|
267
375
|
}
|
|
268
376
|
|
|
269
377
|
async fn request(&self, request: Request) -> Result<Response, AuthorityError> {
|
|
378
|
+
// Once a commit has been handed to the authority it may have been
|
|
379
|
+
// applied whether or not an answer comes back, so every failure from
|
|
380
|
+
// that point is an unknown outcome, not a failure.
|
|
381
|
+
let is_commit = matches!(request, Request::Commit { .. });
|
|
382
|
+
let unknown = |detail: String| AuthorityError {
|
|
383
|
+
code: "COMMIT_OUTCOME_UNKNOWN".into(),
|
|
384
|
+
message: format!(
|
|
385
|
+
"the commit may or may not have been applied ({detail}); retry with the same transaction id and payload"
|
|
386
|
+
),
|
|
387
|
+
authoritative_revision: None,
|
|
388
|
+
};
|
|
270
389
|
let mut stream = UnixStream::connect(&self.socket_path)
|
|
271
390
|
.await
|
|
272
391
|
.map_err(|error| AuthorityError {
|
|
@@ -283,17 +402,26 @@ impl AuthorityClient {
|
|
|
283
402
|
})?,
|
|
284
403
|
)
|
|
285
404
|
.await
|
|
286
|
-
.map_err(io_authority_error)?;
|
|
287
|
-
stream
|
|
405
|
+
.map_err(|error| if is_commit { unknown(error.to_string()) } else { io_authority_error(error) })?;
|
|
406
|
+
stream
|
|
407
|
+
.write_all(b"\n")
|
|
408
|
+
.await
|
|
409
|
+
.map_err(|error| if is_commit { unknown(error.to_string()) } else { io_authority_error(error) })?;
|
|
288
410
|
let mut response = String::new();
|
|
289
411
|
BufReader::new(stream)
|
|
290
412
|
.read_line(&mut response)
|
|
291
413
|
.await
|
|
292
|
-
.map_err(io_authority_error)?;
|
|
293
|
-
match serde_json::from_str(&response).map_err(|error|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
414
|
+
.map_err(|error| if is_commit { unknown(error.to_string()) } else { io_authority_error(error) })?;
|
|
415
|
+
match serde_json::from_str(&response).map_err(|error| {
|
|
416
|
+
if is_commit {
|
|
417
|
+
unknown(format!("unreadable response: {error}"))
|
|
418
|
+
} else {
|
|
419
|
+
AuthorityError {
|
|
420
|
+
code: "INVALID_RESPONSE".into(),
|
|
421
|
+
message: error.to_string(),
|
|
422
|
+
authoritative_revision: None,
|
|
423
|
+
}
|
|
424
|
+
}
|
|
297
425
|
})? {
|
|
298
426
|
Response::Error(error) => Err(error),
|
|
299
427
|
response => Ok(response),
|
|
@@ -340,8 +468,10 @@ impl FeltDbAuthority for AuthorityClient {
|
|
|
340
468
|
match self.request(Request::Commit { transaction }).await? {
|
|
341
469
|
Response::Commit(commit) => Ok(commit),
|
|
342
470
|
_ => Err(AuthorityError {
|
|
343
|
-
code: "
|
|
344
|
-
message: "
|
|
471
|
+
code: "COMMIT_OUTCOME_UNKNOWN".into(),
|
|
472
|
+
message: "the authority answered a commit with something other than a commit; \
|
|
473
|
+
its outcome is unknown, retry with the same transaction id and payload"
|
|
474
|
+
.into(),
|
|
345
475
|
authoritative_revision: None,
|
|
346
476
|
}),
|
|
347
477
|
}
|
|
@@ -1,10 +1,158 @@
|
|
|
1
|
+
//! # FeltDB
|
|
2
|
+
//!
|
|
3
|
+
//! A durable, embedded state store. Records live under a *capability* (the part of a key before
|
|
4
|
+
//! the first `:`, so `"orders:o-1"` is record `o-1` of capability `orders`). Every write goes
|
|
5
|
+
//! through a durable log; several records can be changed in one atomic transaction; an update can
|
|
6
|
+
//! be made conditional on the version you read; and a transaction can carry an identity so that a
|
|
7
|
+
//! retry after an uncertain outcome is recognized instead of applied twice.
|
|
8
|
+
//!
|
|
9
|
+
//! ```
|
|
10
|
+
//! use feltdb::{AtomicMutation, FeltDb, FlowError, PayloadBinding, RecordPrecondition};
|
|
11
|
+
//! use serde_json::{json, Value};
|
|
12
|
+
//!
|
|
13
|
+
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
14
|
+
//! let dir = tempfile::tempdir()?;
|
|
15
|
+
//! let path = dir.path().join("app.db");
|
|
16
|
+
//! let db = FeltDb::open(&path)?;
|
|
17
|
+
//!
|
|
18
|
+
//! // Create a record only if it is absent: a second create is refused, and refused in a way a
|
|
19
|
+
//! // caller can branch on.
|
|
20
|
+
//! db.insert_if_absent("orders:o-1", json!({ "status": "pending" }))?;
|
|
21
|
+
//! assert!(matches!(
|
|
22
|
+
//! db.insert_if_absent("orders:o-1", json!({})),
|
|
23
|
+
//! Err(FlowError::PreconditionFailed(_))
|
|
24
|
+
//! ));
|
|
25
|
+
//!
|
|
26
|
+
//! // Change several records atomically, and name the transaction so a retry is safe. The payload
|
|
27
|
+
//! // hash is yours to compute: FeltDB stores it and compares it for equality.
|
|
28
|
+
//! let ship = |hash: &str| {
|
|
29
|
+
//! db.apply_atomic_transaction_content_addressed(
|
|
30
|
+
//! "ship-o-1",
|
|
31
|
+
//! Some(hash),
|
|
32
|
+
//! None,
|
|
33
|
+
//! &[],
|
|
34
|
+
//! &[
|
|
35
|
+
//! AtomicMutation {
|
|
36
|
+
//! capability: "orders".into(),
|
|
37
|
+
//! key: "orders:o-1".into(),
|
|
38
|
+
//! value: Some(json!({ "status": "shipped" })),
|
|
39
|
+
//! },
|
|
40
|
+
//! AtomicMutation {
|
|
41
|
+
//! capability: "audit".into(),
|
|
42
|
+
//! key: "audit:a-1".into(),
|
|
43
|
+
//! value: Some(json!({ "order": "o-1", "event": "shipped" })),
|
|
44
|
+
//! },
|
|
45
|
+
//! ],
|
|
46
|
+
//! None,
|
|
47
|
+
//! )
|
|
48
|
+
//! };
|
|
49
|
+
//! let first = ship("sha256:payload-v1")?;
|
|
50
|
+
//! assert!(!first.duplicate);
|
|
51
|
+
//!
|
|
52
|
+
//! // The same request again is recognized as the original, not applied a second time.
|
|
53
|
+
//! let retry = ship("sha256:payload-v1")?;
|
|
54
|
+
//! assert!(retry.duplicate);
|
|
55
|
+
//! assert_eq!(retry.payload_binding, Some(PayloadBinding::Verified));
|
|
56
|
+
//! assert_eq!(retry.commit_revision, first.commit_revision);
|
|
57
|
+
//!
|
|
58
|
+
//! // The same id with a different payload is refused, with a typed error. Nothing is written.
|
|
59
|
+
//! assert!(matches!(
|
|
60
|
+
//! ship("sha256:payload-v2"),
|
|
61
|
+
//! Err(FlowError::TransactionIdPayloadMismatch { .. })
|
|
62
|
+
//! ));
|
|
63
|
+
//!
|
|
64
|
+
//! // Close and reopen: what was acknowledged is there.
|
|
65
|
+
//! drop(db);
|
|
66
|
+
//! let db = FeltDb::open(&path)?;
|
|
67
|
+
//! assert_eq!(db.get::<Value>("orders:o-1")?, Some(json!({ "status": "shipped" })));
|
|
68
|
+
//! # let _ = RecordPrecondition::default();
|
|
69
|
+
//! # Ok(())
|
|
70
|
+
//! # }
|
|
71
|
+
//! ```
|
|
72
|
+
//!
|
|
73
|
+
//! ## What is supported
|
|
74
|
+
//!
|
|
75
|
+
//! Only the items in this table are covered by the compatibility policy
|
|
76
|
+
//! ([`docs/reference/feltdb-rust-api.md`] in the repository). While the version is `0.x`, a breaking
|
|
77
|
+
//! change to them is a minor-version bump and is recorded in the changelog; everything else may
|
|
78
|
+
//! change in any release.
|
|
79
|
+
//!
|
|
80
|
+
//! | To | Use |
|
|
81
|
+
//! | --- | --- |
|
|
82
|
+
//! | open, recover, reopen | [`FeltDb::open`], [`FeltDb::log_recovery`], [`FeltDb::health`] |
|
|
83
|
+
//! | read | [`FeltDb::get`], [`FeltDb::query`] |
|
|
84
|
+
//! | write unconditionally | [`FeltDb::insert`], [`FeltDb::update`], [`FeltDb::delete`] (all three are plain writes: see below) |
|
|
85
|
+
//! | create only if absent | [`FeltDb::insert_if_absent`] |
|
|
86
|
+
//! | update only if unchanged | [`FeltDb::compare_and_set_json`] → [`JsonCasResult`] |
|
|
87
|
+
//! | change several records atomically | [`FeltDb::apply_atomic_transaction`], [`FeltDb::apply_atomic_transaction_guarded`] with [`AtomicMutation`] and [`RecordPrecondition`] → [`AtomicCommit`] |
|
|
88
|
+
//! | make a retry safe | [`FeltDb::apply_atomic_transaction_content_addressed`], [`FeltDb::has_applied_transaction`], [`FeltDb::applied_transaction_payload_hash`], [`PayloadBinding`] |
|
|
89
|
+
//! | tell failures apart | [`FlowError`] and its variants, [`PreconditionFailure`] |
|
|
90
|
+
//! | a durable authority for several processes | the [`authority`] module (Unix) |
|
|
91
|
+
//!
|
|
92
|
+
//! **Behavior to know before relying on it.**
|
|
93
|
+
//! `insert`, `update` and `delete` are unconditional: `insert` replaces an existing record, `update`
|
|
94
|
+
//! of a missing key *creates* it, and `delete` of a missing key succeeds. A fenced write
|
|
95
|
+
//! ([`FeltDb::compare_and_set_json`], a [`RecordPrecondition`] with `expected_version`) treats a record
|
|
96
|
+
//! that has never been through one as version 1, and a record's `__version` becomes visible in its JSON
|
|
97
|
+
//! after its first fenced write. A transaction refused for a failed precondition writes nothing.
|
|
98
|
+
//! A transaction id committed *without* a payload hash can be retried (it is a duplicate, with no
|
|
99
|
+
//! [`PayloadBinding`]), but retrying it *with* a hash is refused, because there is nothing to verify against.
|
|
100
|
+
//!
|
|
101
|
+
//! ## Durability
|
|
102
|
+
//!
|
|
103
|
+
//! An acknowledged write survives the process being killed. Whether it survives the machine losing
|
|
104
|
+
//! power depends on the operating system and drive honouring a flush, and is **not** established.
|
|
105
|
+
//! A log ending in an incomplete append is recovered by discarding that append, which was never
|
|
106
|
+
//! acknowledged: [`FeltDb::log_recovery`] says so. A corrupt log, or one in a format this build does
|
|
107
|
+
//! not understand, refuses to open and is left as found.
|
|
108
|
+
//!
|
|
109
|
+
//! ## Stability tiers
|
|
110
|
+
//!
|
|
111
|
+
//! * **Supported**: the table above.
|
|
112
|
+
//! * **Experimental**: [`agent_state`], [`agent_transition_contract`], the deterministic-contract
|
|
113
|
+
//! adapters, [`semantic_decision`] and [`semantic_query`]. Public so they can be used, but they
|
|
114
|
+
//! may change shape before they are stabilized.
|
|
115
|
+
//! * **Not yet classified**: the other public modules. They are public today and carry no promise;
|
|
116
|
+
//! each is listed with the decision still needed in the repository's Rust API audit.
|
|
117
|
+
//! * **Internals**: implementation, fault-injection and diagnostic modules are private unless the
|
|
118
|
+
//! `unstable-internals` feature is enabled, and nothing behind that feature is covered by any promise.
|
|
119
|
+
//!
|
|
120
|
+
//! `FlowError` is `#[non_exhaustive]`: match on its variants, always with a catch-all arm, and never
|
|
121
|
+
//! on message text.
|
|
122
|
+
//!
|
|
123
|
+
//! [`docs/reference/feltdb-rust-api.md`]: https://github.com/rkendel1/flow_db/blob/main/docs/reference/feltdb-rust-api.md
|
|
124
|
+
|
|
125
|
+
macro_rules! unstable_internal_modules {
|
|
126
|
+
($($name:ident),* $(,)?) => {
|
|
127
|
+
$(
|
|
128
|
+
#[cfg(feature = "unstable-internals")]
|
|
129
|
+
pub mod $name;
|
|
130
|
+
#[cfg(not(feature = "unstable-internals"))]
|
|
131
|
+
#[allow(dead_code, unused_imports)]
|
|
132
|
+
mod $name;
|
|
133
|
+
)*
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
unstable_internal_modules! {
|
|
138
|
+
adversarial_transport,
|
|
139
|
+
concurrency_fuzzing,
|
|
140
|
+
crash_injection,
|
|
141
|
+
replay_fuzzing,
|
|
142
|
+
permutation_scheduler,
|
|
143
|
+
persistence_reality,
|
|
144
|
+
in_process_transport,
|
|
145
|
+
query_execution_diagnostics,
|
|
146
|
+
workload_diagnostics,
|
|
147
|
+
cardinality_endpoint,
|
|
148
|
+
}
|
|
149
|
+
|
|
1
150
|
#[cfg(test)]
|
|
2
151
|
mod acceptance_tests;
|
|
3
152
|
mod acquisition;
|
|
4
153
|
pub mod admission;
|
|
5
154
|
#[cfg(test)]
|
|
6
155
|
mod admission_contract_tests;
|
|
7
|
-
pub mod adversarial_transport;
|
|
8
156
|
pub mod analytics;
|
|
9
157
|
pub mod application;
|
|
10
158
|
pub mod application_runtime;
|
|
@@ -17,33 +165,32 @@ mod authorization_security_tests;
|
|
|
17
165
|
pub mod capabilities;
|
|
18
166
|
mod capability;
|
|
19
167
|
mod cardinality_diagnostics;
|
|
20
|
-
|
|
21
|
-
|
|
168
|
+
#[cfg(test)]
|
|
169
|
+
mod causal_backlog_bound;
|
|
22
170
|
pub mod causal_dependency_barrier;
|
|
23
171
|
#[cfg(test)]
|
|
24
172
|
mod causal_dependency_barrier_phase_7_1;
|
|
25
|
-
pub mod concurrency_fuzzing;
|
|
26
173
|
pub mod consistency_contract;
|
|
27
174
|
mod content_distribution;
|
|
28
175
|
pub mod convergence;
|
|
29
176
|
pub mod crash_atomic_boundary;
|
|
30
|
-
pub mod crash_injection;
|
|
31
177
|
#[cfg(test)]
|
|
32
178
|
mod crash_recovery_tests;
|
|
33
179
|
mod cron;
|
|
34
|
-
|
|
180
|
+
#[cfg(test)]
|
|
181
|
+
mod dedup_bound_investigation;
|
|
35
182
|
pub mod distributed_indexing;
|
|
36
183
|
#[cfg(test)]
|
|
37
184
|
mod distributed_tests;
|
|
38
185
|
pub mod distributed_transactions;
|
|
39
186
|
pub mod durability_guarantees;
|
|
40
187
|
pub mod durable_dedup_set;
|
|
41
|
-
|
|
188
|
+
#[cfg(test)]
|
|
189
|
+
mod durable_operation_identity;
|
|
42
190
|
pub mod durable_operation_log;
|
|
43
191
|
pub mod durable_sync;
|
|
44
192
|
pub mod equality_index;
|
|
45
193
|
mod execution;
|
|
46
|
-
pub mod in_process_transport;
|
|
47
194
|
pub mod indexing;
|
|
48
195
|
#[cfg(test)]
|
|
49
196
|
mod managed_cas_tests;
|
|
@@ -55,12 +202,12 @@ pub mod observability;
|
|
|
55
202
|
mod operation;
|
|
56
203
|
pub mod operation_algebra;
|
|
57
204
|
pub mod operation_log;
|
|
58
|
-
|
|
59
|
-
|
|
205
|
+
#[cfg(test)]
|
|
206
|
+
mod p1_application_atomicity;
|
|
207
|
+
#[cfg(test)]
|
|
208
|
+
mod p1_atomicity_acceptance;
|
|
60
209
|
pub mod partition_reconciliation;
|
|
61
210
|
mod peer_registry;
|
|
62
|
-
pub mod permutation_scheduler;
|
|
63
|
-
pub mod persistence_reality;
|
|
64
211
|
#[cfg(test)]
|
|
65
212
|
mod phase1b_acceptance;
|
|
66
213
|
#[cfg(test)]
|
|
@@ -99,11 +246,10 @@ mod phase_5_soak;
|
|
|
99
246
|
pub mod policy_evaluation;
|
|
100
247
|
pub mod production_api;
|
|
101
248
|
mod provenance;
|
|
102
|
-
pub mod query_execution_diagnostics;
|
|
103
249
|
pub mod query_performance;
|
|
104
|
-
|
|
250
|
+
#[cfg(test)]
|
|
251
|
+
mod read_basis;
|
|
105
252
|
mod references;
|
|
106
|
-
pub mod replay_fuzzing;
|
|
107
253
|
pub mod replica_acknowledgements;
|
|
108
254
|
pub mod replica_membership;
|
|
109
255
|
pub mod replication_manager;
|
|
@@ -112,6 +258,8 @@ mod routing;
|
|
|
112
258
|
pub mod semantic_decision;
|
|
113
259
|
pub mod semantic_query;
|
|
114
260
|
pub mod sharding;
|
|
261
|
+
pub mod agent_state;
|
|
262
|
+
pub mod agent_transition_contract;
|
|
115
263
|
pub mod state_conflict_contract;
|
|
116
264
|
pub mod state_contract;
|
|
117
265
|
pub mod state_diff_contract;
|
|
@@ -129,7 +277,8 @@ pub mod tcp_transport;
|
|
|
129
277
|
pub mod transaction_api;
|
|
130
278
|
#[cfg(test)]
|
|
131
279
|
mod transaction_invariants;
|
|
132
|
-
|
|
280
|
+
#[cfg(test)]
|
|
281
|
+
mod transaction_preconditions;
|
|
133
282
|
pub mod transactions;
|
|
134
283
|
mod trigger;
|
|
135
284
|
pub mod trigger_contract;
|
|
@@ -141,13 +290,13 @@ mod workflow_acceptance_tests;
|
|
|
141
290
|
mod workflow_integration;
|
|
142
291
|
mod workflow_runtime;
|
|
143
292
|
pub mod workload;
|
|
144
|
-
pub mod workload_diagnostics;
|
|
145
293
|
|
|
146
294
|
pub use acquisition::{
|
|
147
295
|
AcquisitionDeduplicator, AcquisitionPolicy, AcquisitionResult, ContentRequest, ContentResponse,
|
|
148
296
|
ContentTransferError,
|
|
149
297
|
};
|
|
150
298
|
pub use admission::{ProductionRetryPolicy, RejectionReason, SubmissionOutcome};
|
|
299
|
+
#[cfg(feature = "unstable-internals")]
|
|
151
300
|
pub use adversarial_transport::{
|
|
152
301
|
AdversarialTransport, ConnectionId, ConnectivityMatrix, FaultSeed, InMemoryTransport,
|
|
153
302
|
LinkState, MessageId, NetworkEvent, NetworkSchedule, NodeId, ReplicationTransport,
|
|
@@ -210,7 +359,9 @@ pub use partition_reconciliation::{
|
|
|
210
359
|
ReconciliationResult, ReplicaPartition,
|
|
211
360
|
};
|
|
212
361
|
pub use peer_registry::PeerRegistry;
|
|
362
|
+
#[cfg(feature = "unstable-internals")]
|
|
213
363
|
pub use permutation_scheduler::{OperationSchedule, PermutationScheduler, ScheduleStrategy};
|
|
364
|
+
#[cfg(feature = "unstable-internals")]
|
|
214
365
|
pub use persistence_reality::{
|
|
215
366
|
CorruptionDetectionResult, DurablePersistenceLayer, FsyncDurabilityResult,
|
|
216
367
|
PersistenceContractVerifier, PersistenceCrashPoint, RecoveryResult, WALEntry, WriteResult,
|
|
@@ -325,7 +476,14 @@ use tokio::sync::broadcast;
|
|
|
325
476
|
|
|
326
477
|
pub type Result<T> = std::result::Result<T, FlowError>;
|
|
327
478
|
|
|
479
|
+
/// Why an operation failed.
|
|
480
|
+
///
|
|
481
|
+
/// Branch on the variant, never on message text. The enum is `#[non_exhaustive]`: a later release
|
|
482
|
+
/// may add variants, so a `match` needs a catch-all arm. `CapabilityError` carries a human-readable
|
|
483
|
+
/// message, some of which begin with an upper-case code (`INVALID_READ_SET:`, `EMPTY_PRECONDITION:`);
|
|
484
|
+
/// those prefixes are **not** a supported way to tell failures apart and may change.
|
|
328
485
|
#[derive(Debug)]
|
|
486
|
+
#[non_exhaustive]
|
|
329
487
|
pub enum FlowError {
|
|
330
488
|
Io(std::io::Error),
|
|
331
489
|
Serde(serde_json::Error),
|
|
@@ -337,6 +495,16 @@ pub enum FlowError {
|
|
|
337
495
|
/// outcome of a race and a caller is meant to branch on it, while a
|
|
338
496
|
/// capability error is not.
|
|
339
497
|
PreconditionFailed(Box<PreconditionFailure>),
|
|
498
|
+
/// A transaction id was already committed with a **different** payload. Nothing was written, and
|
|
499
|
+
/// the id still names the original transaction.
|
|
500
|
+
///
|
|
501
|
+
/// Returned when a payload hash is supplied for an id that was committed with another one, and
|
|
502
|
+
/// also when an id that was committed *without* a hash is retried *with* one, since there is
|
|
503
|
+
/// nothing recorded to verify against. Retrying with the same id cannot succeed: use a new id
|
|
504
|
+
/// for a new transaction, or resend the original payload to confirm the original.
|
|
505
|
+
TransactionIdPayloadMismatch {
|
|
506
|
+
transaction_id: String,
|
|
507
|
+
},
|
|
340
508
|
/// The transaction was based on a revision which is no longer current.
|
|
341
509
|
RevisionConflict {
|
|
342
510
|
expected: u64,
|
|
@@ -360,6 +528,10 @@ impl Display for FlowError {
|
|
|
360
528
|
FlowError::CorruptLogLine(corruption) => write!(f, "CORRUPT_DURABLE_LOG: {corruption}"),
|
|
361
529
|
FlowError::CapabilityError(msg) => write!(f, "capability error: {msg}"),
|
|
362
530
|
FlowError::PreconditionFailed(failure) => write!(f, "PRECONDITION_FAILED: {failure}"),
|
|
531
|
+
FlowError::TransactionIdPayloadMismatch { transaction_id } => write!(
|
|
532
|
+
f,
|
|
533
|
+
"IDEMPOTENCY_CONFLICT: transaction {transaction_id} was already committed with a different payload"
|
|
534
|
+
),
|
|
363
535
|
FlowError::RevisionConflict { expected, actual } => write!(
|
|
364
536
|
f,
|
|
365
537
|
"REVISION_CONFLICT: expected authority revision {expected}, current revision is {actual}"
|
|
@@ -1012,6 +1184,23 @@ pub struct AtomicCommit {
|
|
|
1012
1184
|
pub state_after: u64,
|
|
1013
1185
|
pub rows: Vec<StoredRow>,
|
|
1014
1186
|
pub duplicate: bool,
|
|
1187
|
+
/// How a duplicate was recognized. Present only on a duplicate, and only
|
|
1188
|
+
/// when the caller's request could be compared with what was committed
|
|
1189
|
+
/// (`Verified`) or was deliberately judged by id alone (`LegacyUnverified`).
|
|
1190
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
1191
|
+
pub payload_binding: Option<PayloadBinding>,
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/// What a duplicate transaction id was checked against.
|
|
1195
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
1196
|
+
#[serde(rename_all = "snake_case")]
|
|
1197
|
+
pub enum PayloadBinding {
|
|
1198
|
+
/// The id and the payload hash both match what was committed: this request
|
|
1199
|
+
/// is the original one.
|
|
1200
|
+
Verified,
|
|
1201
|
+
/// The id was committed without a payload hash, so only the id could be
|
|
1202
|
+
/// checked. The caller must not read this as proof the payload matches.
|
|
1203
|
+
LegacyUnverified,
|
|
1015
1204
|
}
|
|
1016
1205
|
|
|
1017
1206
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
@@ -3204,9 +3393,9 @@ impl FeltDb {
|
|
|
3204
3393
|
.map(String::as_str)
|
|
3205
3394
|
!= Some(expected_hash)
|
|
3206
3395
|
{
|
|
3207
|
-
return Err(FlowError::
|
|
3208
|
-
|
|
3209
|
-
)
|
|
3396
|
+
return Err(FlowError::TransactionIdPayloadMismatch {
|
|
3397
|
+
transaction_id: transaction_id.to_string(),
|
|
3398
|
+
});
|
|
3210
3399
|
}
|
|
3211
3400
|
}
|
|
3212
3401
|
let (base_revision, commit_revision) = inner
|
|
@@ -3229,6 +3418,7 @@ impl FeltDb {
|
|
|
3229
3418
|
state_after,
|
|
3230
3419
|
rows: vec![],
|
|
3231
3420
|
duplicate: true,
|
|
3421
|
+
payload_binding: payload_hash.map(|_| PayloadBinding::Verified),
|
|
3232
3422
|
});
|
|
3233
3423
|
}
|
|
3234
3424
|
// Record preconditions, evaluated before anything is staged and
|
|
@@ -3599,6 +3789,7 @@ impl FeltDb {
|
|
|
3599
3789
|
state_after: inner.sequence,
|
|
3600
3790
|
rows,
|
|
3601
3791
|
duplicate: false,
|
|
3792
|
+
payload_binding: None,
|
|
3602
3793
|
},
|
|
3603
3794
|
events,
|
|
3604
3795
|
)
|
|
@@ -3962,6 +4153,15 @@ impl FeltDb {
|
|
|
3962
4153
|
Ok(inner.sync_state.clone())
|
|
3963
4154
|
}
|
|
3964
4155
|
|
|
4156
|
+
/// The authority's wall-clock time in milliseconds.
|
|
4157
|
+
///
|
|
4158
|
+
/// The same reading the commit-time lease predicate uses, exposed so a
|
|
4159
|
+
/// decision that depends on lease expiry can be made against the clock that
|
|
4160
|
+
/// will later judge the commit. A caller's own clock is never a substitute.
|
|
4161
|
+
pub fn authority_time_ms(&self) -> u64 {
|
|
4162
|
+
now_ms() as u64
|
|
4163
|
+
}
|
|
4164
|
+
|
|
3965
4165
|
/// Read an untyped value for protocol adapters such as the HTTP server.
|
|
3966
4166
|
pub fn get_value(&self, key: &str) -> Result<Option<Value>> {
|
|
3967
4167
|
self.get::<Value>(key)
|
|
@@ -6116,9 +6316,13 @@ mod tests {
|
|
|
6116
6316
|
None,
|
|
6117
6317
|
)
|
|
6118
6318
|
.unwrap_err();
|
|
6119
|
-
assert!(
|
|
6120
|
-
|
|
6121
|
-
|
|
6319
|
+
assert!(
|
|
6320
|
+
matches!(
|
|
6321
|
+
&conflict,
|
|
6322
|
+
FlowError::TransactionIdPayloadMismatch { transaction_id } if transaction_id == "managed-idempotency-1"
|
|
6323
|
+
),
|
|
6324
|
+
"expected the typed idempotency conflict, got {conflict:?}"
|
|
6325
|
+
);
|
|
6122
6326
|
let _ = fs::remove_file(sync_metadata_path(&path));
|
|
6123
6327
|
let _ = fs::remove_file(path);
|
|
6124
6328
|
}
|
|
@@ -1603,11 +1603,7 @@ pub(crate) mod durable {
|
|
|
1603
1603
|
| PreconditionFailure::Version { .. } => Refusal::Changed,
|
|
1604
1604
|
other => Refusal::Failed(format!("{other:?}")),
|
|
1605
1605
|
},
|
|
1606
|
-
FlowError::
|
|
1607
|
-
if message.starts_with("TRANSACTION_ID_PAYLOAD_MISMATCH") =>
|
|
1608
|
-
{
|
|
1609
|
-
Refusal::OtherRequest
|
|
1610
|
-
}
|
|
1606
|
+
FlowError::TransactionIdPayloadMismatch { .. } => Refusal::OtherRequest,
|
|
1611
1607
|
other => Refusal::Failed(other.to_string()),
|
|
1612
1608
|
}
|
|
1613
1609
|
}
|