create-feltdb 0.6.13 → 0.7.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/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/src/bin/feltdb_node.rs +613 -27
- package/dist/server-source/crates/feltdb/src/causal_backlog_bound.rs +452 -0
- package/dist/server-source/crates/feltdb/src/causal_dependency_barrier.rs +208 -0
- package/dist/server-source/crates/feltdb/src/convergence.rs +16 -0
- package/dist/server-source/crates/feltdb/src/dedup_bound_investigation.rs +402 -0
- package/dist/server-source/crates/feltdb/src/distributed_transactions.rs +784 -24
- package/dist/server-source/crates/feltdb/src/durable_operation_identity.rs +418 -0
- package/dist/server-source/crates/feltdb/src/lib.rs +284 -0
- package/dist/server-source/crates/feltdb/src/replica_acknowledgements.rs +471 -0
- package/dist/server-source/crates/feltdb/src/replica_membership.rs +661 -0
- package/dist/server-source/crates/feltdb/src/tcp_transport.rs +72 -3
- package/dist/server-source/crates/feltdb/src/transaction_preconditions.rs +899 -0
- package/dist/server-source/crates/feltdb-server/src/main.rs +116 -4
- package/package.json +1 -1
|
@@ -57,7 +57,7 @@ use feltdb::{
|
|
|
57
57
|
WorkerRegistration,
|
|
58
58
|
},
|
|
59
59
|
workload::{CreateWorkload, WorkloadStore},
|
|
60
|
-
DatabaseSnapshot, FeltDb, JsonCasResult, Operation, PeerAdvertisement, PeerId, StoredRow,
|
|
60
|
+
DatabaseSnapshot, FeltDb, JsonCasResult, Operation, PeerAdvertisement, PeerId, StoredRow, FlowError, RecordPrecondition,
|
|
61
61
|
};
|
|
62
62
|
use feltdb_server::{
|
|
63
63
|
app_state::AppState,
|
|
@@ -103,6 +103,16 @@ const PROTOCOL_VERSION: &str = "1";
|
|
|
103
103
|
#[derive(Debug)]
|
|
104
104
|
struct ApiError(StatusCode, String);
|
|
105
105
|
|
|
106
|
+
impl ApiError {
|
|
107
|
+
/// An error whose body is a structured document rather than a message.
|
|
108
|
+
///
|
|
109
|
+
/// `api_error_body` already parses a message that happens to be JSON, so
|
|
110
|
+
/// this is the existing mechanism named rather than a second one.
|
|
111
|
+
fn structured(status: StatusCode, body: Value) -> Self {
|
|
112
|
+
Self(status, serde_json::to_string(&body).unwrap_or_else(|_| body.to_string()))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
impl IntoResponse for ApiError {
|
|
107
117
|
fn into_response(self) -> Response {
|
|
108
118
|
let body = api_error_body(self.0, self.1);
|
|
@@ -9039,6 +9049,38 @@ struct AtomicTxOperationRequest {
|
|
|
9039
9049
|
/// When set, the record must not already exist for the transaction to commit.
|
|
9040
9050
|
#[serde(default)]
|
|
9041
9051
|
require_absent: bool,
|
|
9052
|
+
/// Fence this write on the record's current `__version`.
|
|
9053
|
+
///
|
|
9054
|
+
/// Present because the client declares it; it was previously accepted by
|
|
9055
|
+
/// serde and silently discarded, so a caller could attach a stale version
|
|
9056
|
+
/// to a staged write and watch the transaction commit anyway.
|
|
9057
|
+
#[serde(default)]
|
|
9058
|
+
expected_version: Option<u64>,
|
|
9059
|
+
#[serde(default)]
|
|
9060
|
+
expected_epoch: Option<u64>,
|
|
9061
|
+
#[serde(default)]
|
|
9062
|
+
expected_lease_id: Option<String>,
|
|
9063
|
+
}
|
|
9064
|
+
|
|
9065
|
+
/// A precondition on a record the transaction does not necessarily write.
|
|
9066
|
+
///
|
|
9067
|
+
/// Explicit rather than overloaded onto an operation, because read/decide/write
|
|
9068
|
+
/// fencing routinely guards on a record it does not modify -- a session
|
|
9069
|
+
/// generation, an ownership epoch -- and expressing that as a no-op write would
|
|
9070
|
+
/// make the guard a mutation.
|
|
9071
|
+
#[derive(Deserialize)]
|
|
9072
|
+
#[serde(rename_all = "camelCase")]
|
|
9073
|
+
struct AtomicTxPreconditionRequest {
|
|
9074
|
+
collection: String,
|
|
9075
|
+
id: String,
|
|
9076
|
+
#[serde(default)]
|
|
9077
|
+
require_absent: bool,
|
|
9078
|
+
#[serde(default)]
|
|
9079
|
+
expected_version: Option<u64>,
|
|
9080
|
+
#[serde(default)]
|
|
9081
|
+
expected_epoch: Option<u64>,
|
|
9082
|
+
#[serde(default)]
|
|
9083
|
+
expected_lease_id: Option<String>,
|
|
9042
9084
|
}
|
|
9043
9085
|
|
|
9044
9086
|
#[derive(Deserialize)]
|
|
@@ -9047,6 +9089,11 @@ struct AtomicTxRequest {
|
|
|
9047
9089
|
/// Caller-supplied identity. Reusing one makes the commit idempotent.
|
|
9048
9090
|
transaction_id: String,
|
|
9049
9091
|
operations: Vec<AtomicTxOperationRequest>,
|
|
9092
|
+
/// Conditions the authority evaluates inside the same atomic boundary as
|
|
9093
|
+
/// the writes. Absent means an unconditional transaction, which behaves
|
|
9094
|
+
/// exactly as before.
|
|
9095
|
+
#[serde(default)]
|
|
9096
|
+
preconditions: Vec<AtomicTxPreconditionRequest>,
|
|
9050
9097
|
}
|
|
9051
9098
|
|
|
9052
9099
|
#[derive(Serialize)]
|
|
@@ -9088,7 +9135,35 @@ async fn commit_transaction(
|
|
|
9088
9135
|
// a malformed request cannot commit a prefix of the transaction.
|
|
9089
9136
|
let mut mutations = Vec::with_capacity(request.operations.len());
|
|
9090
9137
|
let mut preconditions = Vec::new();
|
|
9138
|
+
let mut record_preconditions: Vec<RecordPrecondition> = Vec::new();
|
|
9091
9139
|
let mut seen = std::collections::HashSet::new();
|
|
9140
|
+
|
|
9141
|
+
// Transaction-level preconditions first, on records the transaction need
|
|
9142
|
+
// not write at all.
|
|
9143
|
+
for condition in &request.preconditions {
|
|
9144
|
+
let key = record_key(&condition.collection, &condition.id)?;
|
|
9145
|
+
let precondition = RecordPrecondition {
|
|
9146
|
+
capability: condition.collection.clone(),
|
|
9147
|
+
key,
|
|
9148
|
+
require_absent: condition.require_absent,
|
|
9149
|
+
expected_version: condition.expected_version,
|
|
9150
|
+
expected_epoch: condition.expected_epoch,
|
|
9151
|
+
expected_lease_id: condition.expected_lease_id.clone(),
|
|
9152
|
+
};
|
|
9153
|
+
if precondition.is_empty() {
|
|
9154
|
+
// Refused rather than ignored. A precondition that constrains
|
|
9155
|
+
// nothing reads as protection and provides none, and silently
|
|
9156
|
+
// accepting one is how this whole class of defect happens.
|
|
9157
|
+
return Err(ApiError(
|
|
9158
|
+
StatusCode::BAD_REQUEST,
|
|
9159
|
+
format!(
|
|
9160
|
+
"precondition on {}/{} constrains nothing",
|
|
9161
|
+
condition.collection, condition.id
|
|
9162
|
+
),
|
|
9163
|
+
));
|
|
9164
|
+
}
|
|
9165
|
+
record_preconditions.push(precondition);
|
|
9166
|
+
}
|
|
9092
9167
|
for operation in request.operations {
|
|
9093
9168
|
let key = record_key(&operation.collection, &operation.id)?;
|
|
9094
9169
|
if !seen.insert(key.clone()) {
|
|
@@ -9108,6 +9183,21 @@ async fn commit_transaction(
|
|
|
9108
9183
|
expected_version: None,
|
|
9109
9184
|
});
|
|
9110
9185
|
}
|
|
9186
|
+
// A guard attached to a staged write. Previously declared by the
|
|
9187
|
+
// client, accepted by serde, and dropped here.
|
|
9188
|
+
if operation.expected_version.is_some()
|
|
9189
|
+
|| operation.expected_epoch.is_some()
|
|
9190
|
+
|| operation.expected_lease_id.is_some()
|
|
9191
|
+
{
|
|
9192
|
+
record_preconditions.push(RecordPrecondition {
|
|
9193
|
+
capability: operation.collection.clone(),
|
|
9194
|
+
key: key.clone(),
|
|
9195
|
+
require_absent: false,
|
|
9196
|
+
expected_version: operation.expected_version,
|
|
9197
|
+
expected_epoch: operation.expected_epoch,
|
|
9198
|
+
expected_lease_id: operation.expected_lease_id.clone(),
|
|
9199
|
+
});
|
|
9200
|
+
}
|
|
9111
9201
|
let value = operation.value.map(|mut fields| {
|
|
9112
9202
|
fields.insert("id".to_string(), Value::String(operation.id.clone()));
|
|
9113
9203
|
Value::Object(fields)
|
|
@@ -9122,11 +9212,33 @@ async fn commit_transaction(
|
|
|
9122
9212
|
let operations = mutations.len();
|
|
9123
9213
|
let commit = state
|
|
9124
9214
|
.db
|
|
9125
|
-
.
|
|
9215
|
+
.apply_atomic_transaction_guarded(
|
|
9216
|
+
&request.transaction_id,
|
|
9217
|
+
None,
|
|
9218
|
+
None,
|
|
9219
|
+
&preconditions,
|
|
9220
|
+
&record_preconditions,
|
|
9221
|
+
&mutations,
|
|
9222
|
+
None,
|
|
9223
|
+
)
|
|
9126
9224
|
.map_err(|error| {
|
|
9127
9225
|
// A refused transaction applied nothing, so this is a conflict, not
|
|
9128
|
-
// a partially completed request.
|
|
9129
|
-
|
|
9226
|
+
// a partially completed request. A precondition failure carries
|
|
9227
|
+
// which predicate failed and what the authority holds, so a caller
|
|
9228
|
+
// can tell a lost race from a broken deployment without parsing
|
|
9229
|
+
// prose.
|
|
9230
|
+
match error {
|
|
9231
|
+
FlowError::PreconditionFailed(failure) => ApiError::structured(
|
|
9232
|
+
StatusCode::CONFLICT,
|
|
9233
|
+
json!({
|
|
9234
|
+
"committed": false,
|
|
9235
|
+
"code": "PRECONDITION_FAILED",
|
|
9236
|
+
"error": failure.to_string(),
|
|
9237
|
+
"failure": *failure,
|
|
9238
|
+
}),
|
|
9239
|
+
),
|
|
9240
|
+
other => ApiError(StatusCode::CONFLICT, other.to_string()),
|
|
9241
|
+
}
|
|
9130
9242
|
})?;
|
|
9131
9243
|
|
|
9132
9244
|
Ok((
|