@feltdb/core 0.8.1 → 0.8.2
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/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +19 -0
- package/dist/create/server-source/crates/feltdb/src/authority.rs +349 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority-client.rs +32 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb-authority.rs +19 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +444 -189
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +244 -51
- package/dist/create/server-source/crates/feltdb/tests/authority_process.rs +297 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +41 -11
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +8 -3
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1164 -225
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +134 -0
- package/dist/http-db.d.ts +3 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +2 -1
- package/dist/state-contract.d.ts +4 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/state-contract.js +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-bIqcRzAr.js → feltdb_wasm-DB8cX151.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-ClhDHp0S.wasm +0 -0
- package/dist/studio-app/assets/{index-XGYdlElN.js → index-B0k4UAlI.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BE79okwX.wasm +0 -0
|
@@ -1,69 +1,65 @@
|
|
|
1
1
|
#[cfg(test)]
|
|
2
2
|
mod acceptance_tests;
|
|
3
|
-
#[cfg(test)]
|
|
4
|
-
mod admission_contract_tests;
|
|
5
|
-
#[cfg(test)]
|
|
6
|
-
mod managed_cas_tests;
|
|
7
3
|
mod acquisition;
|
|
8
4
|
pub mod admission;
|
|
5
|
+
#[cfg(test)]
|
|
6
|
+
mod admission_contract_tests;
|
|
7
|
+
pub mod adversarial_transport;
|
|
8
|
+
pub mod analytics;
|
|
9
9
|
pub mod application;
|
|
10
10
|
pub mod application_runtime;
|
|
11
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
12
|
+
pub mod authority;
|
|
13
|
+
pub mod authority_failover;
|
|
11
14
|
pub mod authorization;
|
|
12
|
-
pub mod policy_evaluation;
|
|
13
15
|
#[cfg(test)]
|
|
14
16
|
mod authorization_security_tests;
|
|
15
17
|
pub mod capabilities;
|
|
16
18
|
mod capability;
|
|
19
|
+
mod cardinality_diagnostics;
|
|
20
|
+
pub mod cardinality_endpoint;
|
|
21
|
+
pub mod causal_backlog_bound;
|
|
22
|
+
pub mod causal_dependency_barrier;
|
|
23
|
+
#[cfg(test)]
|
|
24
|
+
mod causal_dependency_barrier_phase_7_1;
|
|
25
|
+
pub mod concurrency_fuzzing;
|
|
26
|
+
pub mod consistency_contract;
|
|
17
27
|
mod content_distribution;
|
|
18
28
|
pub mod convergence;
|
|
29
|
+
pub mod crash_atomic_boundary;
|
|
30
|
+
pub mod crash_injection;
|
|
31
|
+
#[cfg(test)]
|
|
32
|
+
mod crash_recovery_tests;
|
|
19
33
|
mod cron;
|
|
34
|
+
pub mod dedup_bound_investigation;
|
|
35
|
+
pub mod distributed_indexing;
|
|
20
36
|
#[cfg(test)]
|
|
21
37
|
mod distributed_tests;
|
|
22
|
-
mod execution;
|
|
23
|
-
pub mod analytics;
|
|
24
|
-
pub mod distributed_indexing;
|
|
25
|
-
pub mod indexing;
|
|
26
|
-
pub mod sharding;
|
|
27
|
-
pub mod transaction_preconditions;
|
|
28
|
-
pub mod transactions;
|
|
29
|
-
pub mod state_hash;
|
|
30
|
-
pub mod state_model;
|
|
31
|
-
pub mod state_facade;
|
|
32
|
-
pub mod crash_injection;
|
|
33
|
-
pub mod concurrency_fuzzing;
|
|
34
|
-
pub mod replay_fuzzing;
|
|
35
|
-
pub mod permutation_scheduler;
|
|
36
|
-
pub mod multi_node_convergence;
|
|
37
38
|
pub mod distributed_transactions;
|
|
38
|
-
pub mod
|
|
39
|
+
pub mod durability_guarantees;
|
|
39
40
|
pub mod durable_dedup_set;
|
|
40
|
-
pub mod
|
|
41
|
+
pub mod durable_operation_identity;
|
|
42
|
+
pub mod durable_operation_log;
|
|
43
|
+
pub mod durable_sync;
|
|
44
|
+
mod execution;
|
|
41
45
|
pub mod in_process_transport;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
pub mod indexing;
|
|
47
|
+
#[cfg(test)]
|
|
48
|
+
mod managed_cas_tests;
|
|
49
|
+
mod materialization;
|
|
50
|
+
pub mod metrics;
|
|
51
|
+
pub mod multi_node_convergence;
|
|
46
52
|
pub mod multi_operation_transaction;
|
|
47
|
-
pub mod
|
|
53
|
+
pub mod observability;
|
|
54
|
+
mod operation;
|
|
55
|
+
pub mod operation_algebra;
|
|
56
|
+
pub mod operation_log;
|
|
48
57
|
pub mod p1_application_atomicity;
|
|
49
|
-
pub mod
|
|
50
|
-
pub mod dedup_bound_investigation;
|
|
51
|
-
pub mod durable_operation_identity;
|
|
52
|
-
pub mod causal_dependency_barrier;
|
|
58
|
+
pub mod p1_atomicity_acceptance;
|
|
53
59
|
pub mod partition_reconciliation;
|
|
54
|
-
|
|
60
|
+
mod peer_registry;
|
|
61
|
+
pub mod permutation_scheduler;
|
|
55
62
|
pub mod persistence_reality;
|
|
56
|
-
pub mod adversarial_transport;
|
|
57
|
-
pub mod replica_membership;
|
|
58
|
-
pub mod replica_acknowledgements;
|
|
59
|
-
pub mod replication_manager;
|
|
60
|
-
pub mod authority_failover;
|
|
61
|
-
pub mod metrics;
|
|
62
|
-
pub mod query_performance;
|
|
63
|
-
pub mod durable_sync;
|
|
64
|
-
pub mod production_api;
|
|
65
|
-
pub mod observability;
|
|
66
|
-
pub mod durability_guarantees;
|
|
67
63
|
#[cfg(test)]
|
|
68
64
|
mod phase1b_acceptance;
|
|
69
65
|
#[cfg(test)]
|
|
@@ -71,9 +67,19 @@ mod phase1c1_acceptance;
|
|
|
71
67
|
#[cfg(test)]
|
|
72
68
|
mod phase1c2_acceptance;
|
|
73
69
|
#[cfg(test)]
|
|
70
|
+
mod phase1c3_acceptance;
|
|
71
|
+
#[cfg(test)]
|
|
74
72
|
mod phase1c_atomicity_proof;
|
|
75
73
|
#[cfg(test)]
|
|
76
|
-
mod
|
|
74
|
+
mod phase5_integration;
|
|
75
|
+
#[cfg(test)]
|
|
76
|
+
mod phase5_scenarios;
|
|
77
|
+
#[cfg(test)]
|
|
78
|
+
mod phase6_adversarial_scenarios;
|
|
79
|
+
#[cfg(test)]
|
|
80
|
+
mod phase6_convergence_validator;
|
|
81
|
+
#[cfg(test)]
|
|
82
|
+
mod phase6_persistence;
|
|
77
83
|
#[cfg(test)]
|
|
78
84
|
#[cfg(test)]
|
|
79
85
|
mod phase_1c_real_tcp;
|
|
@@ -89,38 +95,34 @@ mod phase_3_durability;
|
|
|
89
95
|
mod phase_4_baseline;
|
|
90
96
|
#[cfg(test)]
|
|
91
97
|
mod phase_5_soak;
|
|
92
|
-
|
|
93
|
-
mod
|
|
94
|
-
#[cfg(test)]
|
|
95
|
-
mod phase5_scenarios;
|
|
96
|
-
#[cfg(test)]
|
|
97
|
-
mod phase6_persistence;
|
|
98
|
-
#[cfg(test)]
|
|
99
|
-
mod phase6_adversarial_scenarios;
|
|
100
|
-
#[cfg(test)]
|
|
101
|
-
mod phase6_convergence_validator;
|
|
102
|
-
#[cfg(test)]
|
|
103
|
-
mod transaction_invariants;
|
|
104
|
-
#[cfg(test)]
|
|
105
|
-
mod crash_recovery_tests;
|
|
106
|
-
#[cfg(test)]
|
|
107
|
-
mod causal_dependency_barrier_phase_7_1;
|
|
108
|
-
mod cardinality_diagnostics;
|
|
109
|
-
pub mod cardinality_endpoint;
|
|
110
|
-
mod materialization;
|
|
111
|
-
mod operation;
|
|
112
|
-
pub mod operation_log;
|
|
113
|
-
pub mod state_transition_store;
|
|
114
|
-
pub mod submission;
|
|
115
|
-
pub mod transaction_api;
|
|
116
|
-
mod peer_registry;
|
|
98
|
+
pub mod policy_evaluation;
|
|
99
|
+
pub mod production_api;
|
|
117
100
|
mod provenance;
|
|
101
|
+
pub mod query_performance;
|
|
118
102
|
mod references;
|
|
103
|
+
pub mod replay_fuzzing;
|
|
104
|
+
pub mod replica_acknowledgements;
|
|
105
|
+
pub mod replica_membership;
|
|
106
|
+
pub mod replication_manager;
|
|
107
|
+
pub mod replication_protocol;
|
|
119
108
|
mod routing;
|
|
109
|
+
pub mod sharding;
|
|
120
110
|
pub mod state_contract;
|
|
111
|
+
pub mod state_facade;
|
|
112
|
+
pub mod state_hash;
|
|
113
|
+
pub mod state_model;
|
|
114
|
+
pub mod state_transition_store;
|
|
121
115
|
mod storage;
|
|
116
|
+
pub mod submission;
|
|
122
117
|
mod sync;
|
|
123
118
|
pub mod sync_contract;
|
|
119
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
120
|
+
pub mod tcp_transport;
|
|
121
|
+
pub mod transaction_api;
|
|
122
|
+
#[cfg(test)]
|
|
123
|
+
mod transaction_invariants;
|
|
124
|
+
pub mod transaction_preconditions;
|
|
125
|
+
pub mod transactions;
|
|
124
126
|
mod trigger;
|
|
125
127
|
pub mod worker_mesh;
|
|
126
128
|
mod workflow;
|
|
@@ -134,6 +136,14 @@ pub use acquisition::{
|
|
|
134
136
|
AcquisitionDeduplicator, AcquisitionPolicy, AcquisitionResult, ContentRequest, ContentResponse,
|
|
135
137
|
ContentTransferError,
|
|
136
138
|
};
|
|
139
|
+
pub use admission::{ProductionRetryPolicy, RejectionReason, SubmissionOutcome};
|
|
140
|
+
pub use adversarial_transport::{
|
|
141
|
+
AdversarialTransport, ConnectionId, ConnectivityMatrix, FaultSeed, InMemoryTransport,
|
|
142
|
+
LinkState, MessageId, NetworkEvent, NetworkSchedule, NodeId, ReplicationTransport,
|
|
143
|
+
};
|
|
144
|
+
pub use analytics::{
|
|
145
|
+
AutoTuner, IndexAnalytics, IndexMetrics, PerformanceReport, QueryTrace, TuningRecommendation,
|
|
146
|
+
};
|
|
137
147
|
pub use capability::{
|
|
138
148
|
CapabilityAccessModel, CapabilityCheckpoint, CapabilityContext, CapabilityExecutionContext,
|
|
139
149
|
CapabilityExecutionResult, CapabilityId, CapabilityLifecycleState, CapabilityMetadata,
|
|
@@ -141,6 +151,15 @@ pub use capability::{
|
|
|
141
151
|
CapabilityRegistry, CapabilityRequirements, CapabilityResolver, CapabilityRole,
|
|
142
152
|
DistributedCapabilityAdvertisement, ExecutionRef, FlowCapability, StateRequirement,
|
|
143
153
|
};
|
|
154
|
+
pub use causal_dependency_barrier::{
|
|
155
|
+
ApplyResult, CausalDependencyBarrier, OperationState, PendingOperation, ReceiveResult,
|
|
156
|
+
StateCounters,
|
|
157
|
+
};
|
|
158
|
+
pub use consistency_contract::{
|
|
159
|
+
CommutativeVerificationResult, ConsensusEnforcementResult, ConsensusRequirementChecker,
|
|
160
|
+
ConsistencyContractEnforcer, DeterministicMergeVerificationResult,
|
|
161
|
+
NoDowngradeVerificationResult, OperationApplication,
|
|
162
|
+
};
|
|
144
163
|
pub use content_distribution::{
|
|
145
164
|
CapabilityLocationRegistry, ContentAcquisitionManager, ContentAddressableStore,
|
|
146
165
|
DistributedReferenceResolver, ProjectionDistribution,
|
|
@@ -149,74 +168,41 @@ pub use convergence::{
|
|
|
149
168
|
CausalHistory, ConflictResolution, ConflictResolutionStrategy, MergeResult, StateVersion,
|
|
150
169
|
VectorClock,
|
|
151
170
|
};
|
|
152
|
-
pub use
|
|
171
|
+
pub use crash_atomic_boundary::{
|
|
172
|
+
CrashAtomicExecutor, CrashBoundaryPoint, CrashRecoveryState, InvariantCheckResult,
|
|
173
|
+
RecoveryDecision,
|
|
174
|
+
};
|
|
153
175
|
pub use cron::{CronSchedule, CronScheduler};
|
|
154
176
|
pub use distributed_indexing::{
|
|
155
177
|
ConsistencyLevel, ConsistencyProtocol, DistributedIndexManager, DistributedSyncStatus,
|
|
156
178
|
IndexOperation, IndexReplicationState, IndexSyncMessage, IndexVectorClock, RemoteIndexMetadata,
|
|
157
179
|
RemoteIndexState, ReplicationStatus, VersionConflict,
|
|
158
180
|
};
|
|
181
|
+
pub use distributed_transactions::{
|
|
182
|
+
DistributedTransactionExecutor, EnvelopeId, ReplicaState, ReplicationMessage,
|
|
183
|
+
TransactionEnvelope,
|
|
184
|
+
};
|
|
159
185
|
pub use execution::{Execution, ExecutionQueue, ExecutionStatus, RetryPolicy};
|
|
160
186
|
pub use indexing::{IndexConfig, IndexManager, IndexStats, IndexType};
|
|
161
|
-
pub use
|
|
162
|
-
pub use admission::{SubmissionOutcome, RejectionReason, ProductionRetryPolicy};
|
|
163
|
-
pub use sharding::{
|
|
164
|
-
HotspotAlert, RebalanceOperation, ShardDistributionSummary, ShardId, ShardKey, ShardManager,
|
|
165
|
-
ShardMetrics, ShardRange, ShardingStrategy,
|
|
166
|
-
};
|
|
167
|
-
pub use transactions::{
|
|
168
|
-
CommitId, CommitValidator, ConsistencyContract, OperationCommand,
|
|
169
|
-
OperationId, StateTransition, StateDependency, TransactionExecutor,
|
|
170
|
-
TransitionResult,
|
|
171
|
-
};
|
|
172
|
-
pub use state_hash::{CanonicalState, StateHash};
|
|
173
|
-
pub use state_model::{
|
|
174
|
-
StateId, StateRevision, StateTopology, Relationship, SemanticDiff, SemanticChange,
|
|
175
|
-
ChangeKind, PathComponent, ConflictClassification, ConflictClass, PathConflict,
|
|
176
|
-
ReconciliationPlan, StateReconciliationResult, StateStore, STATE_MODEL_VERSION,
|
|
177
|
-
};
|
|
178
|
-
pub use state_facade::FeltDBStateSystem;
|
|
179
|
-
pub use permutation_scheduler::{OperationSchedule, PermutationScheduler, ScheduleStrategy};
|
|
187
|
+
pub use materialization::{MaterializationHandler, ReactiveCollectionIntegration};
|
|
180
188
|
pub use multi_node_convergence::{
|
|
181
189
|
ConvergenceAggregation, ConvergenceResult, MultiNodeConvergenceSimulator, NodeExecutionResult,
|
|
182
190
|
};
|
|
183
|
-
pub use
|
|
184
|
-
DistributedTransactionExecutor, EnvelopeId, ReplicaState, ReplicationMessage, TransactionEnvelope,
|
|
185
|
-
};
|
|
191
|
+
pub use operation::{MutationOrder, Operation, OperationType, DEFAULT_MUTATION_ORDER};
|
|
186
192
|
pub use operation_algebra::{
|
|
187
193
|
ConflictType, ConvergenceSemantic, OperationAlgebra, OperationAlgebraRegistry,
|
|
188
194
|
};
|
|
189
|
-
pub use
|
|
190
|
-
CrashAtomicExecutor, CrashBoundaryPoint, CrashRecoveryState, InvariantCheckResult,
|
|
191
|
-
RecoveryDecision,
|
|
192
|
-
};
|
|
193
|
-
pub use causal_dependency_barrier::{
|
|
194
|
-
ApplyResult, CausalDependencyBarrier, OperationState, PendingOperation, ReceiveResult,
|
|
195
|
-
StateCounters,
|
|
196
|
-
};
|
|
195
|
+
pub use operation_log::{MemoryOperationLog, OperationLogEntry};
|
|
197
196
|
pub use partition_reconciliation::{
|
|
198
197
|
DeterminismCheckResult, PartitionDeterminismChecker, PartitionTrace, PartitionedReplica,
|
|
199
198
|
ReconciliationResult, ReplicaPartition,
|
|
200
199
|
};
|
|
201
|
-
pub use
|
|
202
|
-
|
|
203
|
-
ConsistencyContractEnforcer, DeterministicMergeVerificationResult, NoDowngradeVerificationResult,
|
|
204
|
-
OperationApplication,
|
|
205
|
-
};
|
|
200
|
+
pub use peer_registry::PeerRegistry;
|
|
201
|
+
pub use permutation_scheduler::{OperationSchedule, PermutationScheduler, ScheduleStrategy};
|
|
206
202
|
pub use persistence_reality::{
|
|
207
203
|
CorruptionDetectionResult, DurablePersistenceLayer, FsyncDurabilityResult,
|
|
208
|
-
|
|
209
|
-
};
|
|
210
|
-
pub use adversarial_transport::{
|
|
211
|
-
AdversarialTransport, ConnectionId, ConnectivityMatrix, FaultSeed, InMemoryTransport,
|
|
212
|
-
LinkState, MessageId, NetworkEvent, NetworkSchedule, NodeId, ReplicationTransport,
|
|
204
|
+
PersistenceContractVerifier, PersistenceCrashPoint, RecoveryResult, WALEntry, WriteResult,
|
|
213
205
|
};
|
|
214
|
-
pub use materialization::{MaterializationHandler, ReactiveCollectionIntegration};
|
|
215
|
-
pub use operation::{MutationOrder, Operation, OperationType, DEFAULT_MUTATION_ORDER};
|
|
216
|
-
pub use operation_log::{MemoryOperationLog, OperationLogEntry};
|
|
217
|
-
pub use state_transition_store::{MemoryStateTransitionStore, StateTransitionRecord};
|
|
218
|
-
pub use transaction_api::{TransactionManager, TxError, TxResult};
|
|
219
|
-
pub use peer_registry::PeerRegistry;
|
|
220
206
|
pub use provenance::{
|
|
221
207
|
AcquisitionMetadata, DerivedState, DerivedStateProvenance, DerivedValue, OperationSource,
|
|
222
208
|
};
|
|
@@ -228,8 +214,26 @@ pub use routing::{
|
|
|
228
214
|
CapabilityRouter, ExecutionClaim, ExecutionClaimer, ExecutionResultPublisher, PeerStateVersion,
|
|
229
215
|
RouteDecision,
|
|
230
216
|
};
|
|
217
|
+
pub use sharding::{
|
|
218
|
+
HotspotAlert, RebalanceOperation, ShardDistributionSummary, ShardId, ShardKey, ShardManager,
|
|
219
|
+
ShardMetrics, ShardRange, ShardingStrategy,
|
|
220
|
+
};
|
|
221
|
+
pub use state_facade::FeltDBStateSystem;
|
|
222
|
+
pub use state_hash::{CanonicalState, StateHash};
|
|
223
|
+
pub use state_model::{
|
|
224
|
+
ChangeKind, ConflictClass, ConflictClassification, PathComponent, PathConflict,
|
|
225
|
+
ReconciliationPlan, Relationship, SemanticChange, SemanticDiff, StateId,
|
|
226
|
+
StateReconciliationResult, StateRevision, StateStore, StateTopology, STATE_MODEL_VERSION,
|
|
227
|
+
};
|
|
228
|
+
pub use state_transition_store::{MemoryStateTransitionStore, StateTransitionRecord};
|
|
231
229
|
pub use storage::{CheckpointData, FileStorage, MemoryStorage, Storage};
|
|
230
|
+
pub use submission::{SubmissionManager, SubmissionMetrics};
|
|
232
231
|
pub use sync::{ChangeLog, Conflict, ConflictDetector, PeerState, SyncMessage, SyncState};
|
|
232
|
+
pub use transaction_api::{TransactionManager, TxError, TxResult};
|
|
233
|
+
pub use transactions::{
|
|
234
|
+
CommitId, CommitValidator, ConsistencyContract, OperationCommand, OperationId, StateDependency,
|
|
235
|
+
StateTransition, TransactionExecutor, TransitionResult,
|
|
236
|
+
};
|
|
233
237
|
pub use trigger::{Trigger, TriggerFilter, TriggerRegistry};
|
|
234
238
|
pub use workflow::{
|
|
235
239
|
BlockedReason, WorkflowGraph, WorkflowInstance, WorkflowNode, WorkflowOperation, WorkflowRef,
|
|
@@ -240,6 +244,7 @@ pub use workflow_integration::{
|
|
|
240
244
|
};
|
|
241
245
|
pub use workflow_runtime::{WorkflowOperationEntry, WorkflowRuntime};
|
|
242
246
|
|
|
247
|
+
use crate::state_contract::AuthorizationContext;
|
|
243
248
|
use serde::de::DeserializeOwned;
|
|
244
249
|
use serde::{Deserialize, Serialize};
|
|
245
250
|
use serde_json::Value;
|
|
@@ -247,16 +252,15 @@ use sha2::{Digest, Sha256};
|
|
|
247
252
|
use std::any::type_name;
|
|
248
253
|
use std::collections::hash_map::DefaultHasher;
|
|
249
254
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
|
250
|
-
use std::ops::Bound::{Excluded, Unbounded};
|
|
251
255
|
use std::fmt::{Display, Formatter};
|
|
252
256
|
use std::fs::{self, OpenOptions};
|
|
253
257
|
use std::hash::{Hash, Hasher};
|
|
254
258
|
use std::io::{BufRead, BufReader, Write};
|
|
259
|
+
use std::ops::Bound::{Excluded, Unbounded};
|
|
255
260
|
use std::path::{Path, PathBuf};
|
|
256
261
|
use std::sync::{Arc, Mutex};
|
|
257
262
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
258
263
|
use tokio::sync::broadcast;
|
|
259
|
-
use crate::state_contract::AuthorizationContext;
|
|
260
264
|
|
|
261
265
|
pub type Result<T> = std::result::Result<T, FlowError>;
|
|
262
266
|
|
|
@@ -272,6 +276,11 @@ pub enum FlowError {
|
|
|
272
276
|
/// outcome of a race and a caller is meant to branch on it, while a
|
|
273
277
|
/// capability error is not.
|
|
274
278
|
PreconditionFailed(Box<PreconditionFailure>),
|
|
279
|
+
/// The transaction was based on a revision which is no longer current.
|
|
280
|
+
RevisionConflict {
|
|
281
|
+
expected: u64,
|
|
282
|
+
actual: u64,
|
|
283
|
+
},
|
|
275
284
|
}
|
|
276
285
|
|
|
277
286
|
impl Display for FlowError {
|
|
@@ -282,6 +291,10 @@ impl Display for FlowError {
|
|
|
282
291
|
FlowError::CorruptLogLine(line) => write!(f, "corrupt log line: {line}"),
|
|
283
292
|
FlowError::CapabilityError(msg) => write!(f, "capability error: {msg}"),
|
|
284
293
|
FlowError::PreconditionFailed(failure) => write!(f, "PRECONDITION_FAILED: {failure}"),
|
|
294
|
+
FlowError::RevisionConflict { expected, actual } => write!(
|
|
295
|
+
f,
|
|
296
|
+
"REVISION_CONFLICT: expected authority revision {expected}, current revision is {actual}"
|
|
297
|
+
),
|
|
285
298
|
}
|
|
286
299
|
}
|
|
287
300
|
}
|
|
@@ -322,6 +335,9 @@ struct Inner {
|
|
|
322
335
|
adaptive_indexes: HashSet<String>,
|
|
323
336
|
instance_id: String,
|
|
324
337
|
sequence: u64,
|
|
338
|
+
/// Monotonic, authority-assigned commit order. Unlike `sequence`, one
|
|
339
|
+
/// transaction advances this exactly once regardless of its mutation count.
|
|
340
|
+
authority_revision: u64,
|
|
325
341
|
sync_state: SyncState,
|
|
326
342
|
change_log: ChangeLog,
|
|
327
343
|
execution_queue: ExecutionQueue,
|
|
@@ -334,6 +350,7 @@ struct Inner {
|
|
|
334
350
|
bootstrapped: bool,
|
|
335
351
|
applied_transactions: HashSet<String>,
|
|
336
352
|
transaction_payload_hashes: HashMap<String, String>,
|
|
353
|
+
transaction_revisions: HashMap<String, (u64, u64)>,
|
|
337
354
|
collection_cardinality: HashMap<String, u64>,
|
|
338
355
|
}
|
|
339
356
|
|
|
@@ -409,9 +426,24 @@ pub enum PreconditionFailure {
|
|
|
409
426
|
Present { collection: String, key: String },
|
|
410
427
|
/// A predicate was given for a record that does not exist.
|
|
411
428
|
Missing { collection: String, key: String },
|
|
412
|
-
Version {
|
|
413
|
-
|
|
414
|
-
|
|
429
|
+
Version {
|
|
430
|
+
collection: String,
|
|
431
|
+
key: String,
|
|
432
|
+
expected: u64,
|
|
433
|
+
actual: u64,
|
|
434
|
+
},
|
|
435
|
+
Epoch {
|
|
436
|
+
collection: String,
|
|
437
|
+
key: String,
|
|
438
|
+
expected: u64,
|
|
439
|
+
actual: u64,
|
|
440
|
+
},
|
|
441
|
+
Lease {
|
|
442
|
+
collection: String,
|
|
443
|
+
key: String,
|
|
444
|
+
expected: String,
|
|
445
|
+
actual: Option<String>,
|
|
446
|
+
},
|
|
415
447
|
}
|
|
416
448
|
|
|
417
449
|
impl PreconditionFailure {
|
|
@@ -439,28 +471,58 @@ impl PreconditionFailure {
|
|
|
439
471
|
impl Display for PreconditionFailure {
|
|
440
472
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
441
473
|
match self {
|
|
442
|
-
Self::Present { collection, key } =>
|
|
443
|
-
write!(f, "{collection}/{key} was required to be absent but exists")
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
Self::Version {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
474
|
+
Self::Present { collection, key } => {
|
|
475
|
+
write!(f, "{collection}/{key} was required to be absent but exists")
|
|
476
|
+
}
|
|
477
|
+
Self::Missing { collection, key } => write!(f, "{collection}/{key} does not exist"),
|
|
478
|
+
Self::Version {
|
|
479
|
+
collection,
|
|
480
|
+
key,
|
|
481
|
+
expected,
|
|
482
|
+
actual,
|
|
483
|
+
} => write!(
|
|
484
|
+
f,
|
|
485
|
+
"{collection}/{key} is at version {actual}, expected {expected}"
|
|
486
|
+
),
|
|
487
|
+
Self::Epoch {
|
|
488
|
+
collection,
|
|
489
|
+
key,
|
|
490
|
+
expected,
|
|
491
|
+
actual,
|
|
492
|
+
} => write!(
|
|
493
|
+
f,
|
|
494
|
+
"{collection}/{key} is at epoch {actual}, expected {expected}"
|
|
495
|
+
),
|
|
496
|
+
Self::Lease {
|
|
497
|
+
collection,
|
|
498
|
+
key,
|
|
499
|
+
expected,
|
|
500
|
+
actual,
|
|
501
|
+
} => write!(
|
|
502
|
+
f,
|
|
503
|
+
"{collection}/{key} holds lease {actual:?}, expected {expected}"
|
|
504
|
+
),
|
|
452
505
|
}
|
|
453
506
|
}
|
|
454
507
|
}
|
|
455
508
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
456
509
|
pub struct AtomicCommit {
|
|
457
510
|
pub transaction_id: String,
|
|
511
|
+
pub base_revision: u64,
|
|
512
|
+
pub commit_revision: u64,
|
|
513
|
+
pub status: TransactionStatus,
|
|
458
514
|
pub state_before: u64,
|
|
459
515
|
pub state_after: u64,
|
|
460
516
|
pub rows: Vec<StoredRow>,
|
|
461
517
|
pub duplicate: bool,
|
|
462
518
|
}
|
|
463
519
|
|
|
520
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
521
|
+
#[serde(rename_all = "snake_case")]
|
|
522
|
+
pub enum TransactionStatus {
|
|
523
|
+
Committed,
|
|
524
|
+
}
|
|
525
|
+
|
|
464
526
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
465
527
|
pub enum JsonCasResult {
|
|
466
528
|
Updated {
|
|
@@ -494,6 +556,10 @@ struct TransactionLogRecord {
|
|
|
494
556
|
payload_hash: Option<String>,
|
|
495
557
|
state_before: u64,
|
|
496
558
|
state_after: u64,
|
|
559
|
+
#[serde(default)]
|
|
560
|
+
base_revision: u64,
|
|
561
|
+
#[serde(default)]
|
|
562
|
+
commit_revision: u64,
|
|
497
563
|
rows: Vec<StoredRow>,
|
|
498
564
|
#[serde(default)]
|
|
499
565
|
audit: Option<Value>,
|
|
@@ -754,6 +820,7 @@ impl FeltDb {
|
|
|
754
820
|
operation: Some(operation.clone()),
|
|
755
821
|
};
|
|
756
822
|
append_event(&inner.path, &row)?;
|
|
823
|
+
inner.authority_revision += 1;
|
|
757
824
|
inner.change_log.add_operation(operation);
|
|
758
825
|
inner
|
|
759
826
|
.rows
|
|
@@ -828,9 +895,10 @@ impl FeltDb {
|
|
|
828
895
|
if !context.writable_collections.is_empty()
|
|
829
896
|
&& !context.writable_collections.contains(&capability)
|
|
830
897
|
{
|
|
831
|
-
return Err(FlowError::CapabilityError(
|
|
832
|
-
|
|
833
|
-
|
|
898
|
+
return Err(FlowError::CapabilityError(format!(
|
|
899
|
+
"AUTHORIZATION_DENIED:not_writable_collection:{}",
|
|
900
|
+
capability
|
|
901
|
+
)));
|
|
834
902
|
}
|
|
835
903
|
|
|
836
904
|
// Authorized: apply mutation
|
|
@@ -858,9 +926,10 @@ impl FeltDb {
|
|
|
858
926
|
if !context.writable_collections.is_empty()
|
|
859
927
|
&& !context.writable_collections.contains(&capability)
|
|
860
928
|
{
|
|
861
|
-
return Err(FlowError::CapabilityError(
|
|
862
|
-
|
|
863
|
-
|
|
929
|
+
return Err(FlowError::CapabilityError(format!(
|
|
930
|
+
"AUTHORIZATION_DENIED:not_writable_collection:{}",
|
|
931
|
+
capability
|
|
932
|
+
)));
|
|
864
933
|
}
|
|
865
934
|
|
|
866
935
|
// Authorized: apply mutation
|
|
@@ -888,9 +957,10 @@ impl FeltDb {
|
|
|
888
957
|
if !context.writable_collections.is_empty()
|
|
889
958
|
&& !context.writable_collections.contains(&capability)
|
|
890
959
|
{
|
|
891
|
-
return Err(FlowError::CapabilityError(
|
|
892
|
-
|
|
893
|
-
|
|
960
|
+
return Err(FlowError::CapabilityError(format!(
|
|
961
|
+
"AUTHORIZATION_DENIED:not_writable_collection:{}",
|
|
962
|
+
capability
|
|
963
|
+
)));
|
|
894
964
|
}
|
|
895
965
|
|
|
896
966
|
// Authorized: apply mutation
|
|
@@ -970,11 +1040,34 @@ impl FeltDb {
|
|
|
970
1040
|
Ok(inner.sequence)
|
|
971
1041
|
}
|
|
972
1042
|
|
|
1043
|
+
/// Revision of the snapshot held by this embedded authority instance.
|
|
1044
|
+
/// In multi-process topology callers must use `AuthorityClient::read` to
|
|
1045
|
+
/// cross the authority boundary and obtain a current authoritative value.
|
|
1046
|
+
pub fn current_revision(&self) -> Result<u64> {
|
|
1047
|
+
Ok(self.inner.lock().expect("lock poisoned").authority_revision)
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/// Capture revision and rows under one lock. Authority reads must never
|
|
1051
|
+
/// pair metadata from one commit with state from another.
|
|
1052
|
+
pub(crate) fn authority_state(&self) -> (u64, Vec<StoredRow>) {
|
|
1053
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
1054
|
+
let rows = inner
|
|
1055
|
+
.rows
|
|
1056
|
+
.values()
|
|
1057
|
+
.flat_map(|bucket| bucket.values().cloned())
|
|
1058
|
+
.collect();
|
|
1059
|
+
(inner.authority_revision, rows)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
973
1062
|
/// Get the maintained cardinality (record count) for a collection capability.
|
|
974
1063
|
/// Returns the count of non-deleted records in the collection.
|
|
975
1064
|
pub fn collection_cardinality(&self, capability: &str) -> Result<u64> {
|
|
976
1065
|
let inner = self.inner.lock().expect("lock poisoned");
|
|
977
|
-
Ok(inner
|
|
1066
|
+
Ok(inner
|
|
1067
|
+
.collection_cardinality
|
|
1068
|
+
.get(capability)
|
|
1069
|
+
.copied()
|
|
1070
|
+
.unwrap_or(0))
|
|
978
1071
|
}
|
|
979
1072
|
|
|
980
1073
|
/// Diagnostic: List all collection capabilities and their cardinalities.
|
|
@@ -1026,7 +1119,13 @@ impl FeltDb {
|
|
|
1026
1119
|
}
|
|
1027
1120
|
|
|
1028
1121
|
pub fn applied_transaction_payload_hash(&self, transaction_id: &str) -> Result<Option<String>> {
|
|
1029
|
-
Ok(self
|
|
1122
|
+
Ok(self
|
|
1123
|
+
.inner
|
|
1124
|
+
.lock()
|
|
1125
|
+
.expect("lock poisoned")
|
|
1126
|
+
.transaction_payload_hashes
|
|
1127
|
+
.get(transaction_id)
|
|
1128
|
+
.cloned())
|
|
1030
1129
|
}
|
|
1031
1130
|
|
|
1032
1131
|
/// Atomically validates and durably applies an ordered mutation batch.
|
|
@@ -1040,7 +1139,14 @@ impl FeltDb {
|
|
|
1040
1139
|
mutations: &[AtomicMutation],
|
|
1041
1140
|
audit: Option<Value>,
|
|
1042
1141
|
) -> Result<AtomicCommit> {
|
|
1043
|
-
self.apply_atomic_transaction_content_addressed(
|
|
1142
|
+
self.apply_atomic_transaction_content_addressed(
|
|
1143
|
+
transaction_id,
|
|
1144
|
+
None,
|
|
1145
|
+
expected_parent,
|
|
1146
|
+
preconditions,
|
|
1147
|
+
mutations,
|
|
1148
|
+
audit,
|
|
1149
|
+
)
|
|
1044
1150
|
}
|
|
1045
1151
|
|
|
1046
1152
|
/// Applies a transaction whose identifier is durably bound to a canonical
|
|
@@ -1092,12 +1198,27 @@ impl FeltDb {
|
|
|
1092
1198
|
let mut inner = self.inner.lock().expect("lock poisoned");
|
|
1093
1199
|
if inner.applied_transactions.contains(transaction_id) {
|
|
1094
1200
|
if let Some(expected_hash) = payload_hash {
|
|
1095
|
-
if inner
|
|
1096
|
-
|
|
1201
|
+
if inner
|
|
1202
|
+
.transaction_payload_hashes
|
|
1203
|
+
.get(transaction_id)
|
|
1204
|
+
.map(String::as_str)
|
|
1205
|
+
!= Some(expected_hash)
|
|
1206
|
+
{
|
|
1207
|
+
return Err(FlowError::CapabilityError(format!(
|
|
1208
|
+
"TRANSACTION_ID_PAYLOAD_MISMATCH:{transaction_id}"
|
|
1209
|
+
)));
|
|
1097
1210
|
}
|
|
1098
1211
|
}
|
|
1212
|
+
let (base_revision, commit_revision) = inner
|
|
1213
|
+
.transaction_revisions
|
|
1214
|
+
.get(transaction_id)
|
|
1215
|
+
.copied()
|
|
1216
|
+
.unwrap_or((inner.authority_revision, inner.authority_revision));
|
|
1099
1217
|
return Ok(AtomicCommit {
|
|
1100
1218
|
transaction_id: transaction_id.into(),
|
|
1219
|
+
base_revision,
|
|
1220
|
+
commit_revision,
|
|
1221
|
+
status: TransactionStatus::Committed,
|
|
1101
1222
|
state_before: inner.sequence,
|
|
1102
1223
|
state_after: inner.sequence,
|
|
1103
1224
|
rows: vec![],
|
|
@@ -1146,7 +1267,12 @@ impl FeltDb {
|
|
|
1146
1267
|
.unwrap_or(1);
|
|
1147
1268
|
if actual != expected {
|
|
1148
1269
|
return Err(FlowError::PreconditionFailed(Box::new(
|
|
1149
|
-
PreconditionFailure::Version {
|
|
1270
|
+
PreconditionFailure::Version {
|
|
1271
|
+
collection,
|
|
1272
|
+
key,
|
|
1273
|
+
expected,
|
|
1274
|
+
actual,
|
|
1275
|
+
},
|
|
1150
1276
|
)));
|
|
1151
1277
|
}
|
|
1152
1278
|
}
|
|
@@ -1158,14 +1284,22 @@ impl FeltDb {
|
|
|
1158
1284
|
.unwrap_or(0);
|
|
1159
1285
|
if actual != expected {
|
|
1160
1286
|
return Err(FlowError::PreconditionFailed(Box::new(
|
|
1161
|
-
PreconditionFailure::Epoch {
|
|
1287
|
+
PreconditionFailure::Epoch {
|
|
1288
|
+
collection,
|
|
1289
|
+
key,
|
|
1290
|
+
expected,
|
|
1291
|
+
actual,
|
|
1292
|
+
},
|
|
1162
1293
|
)));
|
|
1163
1294
|
}
|
|
1164
1295
|
}
|
|
1165
1296
|
if let Some(expected) = &condition.expected_lease_id {
|
|
1166
1297
|
let lease = current.value.get("lease").filter(|lease| !lease.is_null());
|
|
1167
1298
|
let held = lease.and_then(|lease| {
|
|
1168
|
-
lease
|
|
1299
|
+
lease
|
|
1300
|
+
.get("leaseId")
|
|
1301
|
+
.and_then(Value::as_str)
|
|
1302
|
+
.map(str::to_string)
|
|
1169
1303
|
});
|
|
1170
1304
|
// An expired lease is not held, so naming it is a conflict:
|
|
1171
1305
|
// the caller believes it owns something it no longer does.
|
|
@@ -1209,9 +1343,12 @@ impl FeltDb {
|
|
|
1209
1343
|
let fenced: HashMap<(&str, &str), u64> = record_preconditions
|
|
1210
1344
|
.iter()
|
|
1211
1345
|
.filter_map(|condition| {
|
|
1212
|
-
condition
|
|
1213
|
-
|
|
1214
|
-
|
|
1346
|
+
condition.expected_version.map(|version| {
|
|
1347
|
+
(
|
|
1348
|
+
(condition.capability.as_str(), condition.key.as_str()),
|
|
1349
|
+
version,
|
|
1350
|
+
)
|
|
1351
|
+
})
|
|
1215
1352
|
})
|
|
1216
1353
|
.collect();
|
|
1217
1354
|
let mutations: &[AtomicMutation] = if fenced.is_empty() && creates.is_empty() {
|
|
@@ -1248,12 +1385,13 @@ impl FeltDb {
|
|
|
1248
1385
|
&advanced
|
|
1249
1386
|
};
|
|
1250
1387
|
|
|
1388
|
+
let base_revision = inner.authority_revision;
|
|
1251
1389
|
if let Some(expected) = expected_parent {
|
|
1252
|
-
if expected !=
|
|
1253
|
-
return Err(FlowError::
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
)
|
|
1390
|
+
if expected != base_revision {
|
|
1391
|
+
return Err(FlowError::RevisionConflict {
|
|
1392
|
+
expected,
|
|
1393
|
+
actual: base_revision,
|
|
1394
|
+
});
|
|
1257
1395
|
}
|
|
1258
1396
|
}
|
|
1259
1397
|
for condition in preconditions {
|
|
@@ -1337,10 +1475,13 @@ impl FeltDb {
|
|
|
1337
1475
|
payload_hash: payload_hash.map(str::to_owned),
|
|
1338
1476
|
state_before,
|
|
1339
1477
|
state_after: next_sequence,
|
|
1478
|
+
base_revision,
|
|
1479
|
+
commit_revision: base_revision + 1,
|
|
1340
1480
|
rows: rows.clone(),
|
|
1341
1481
|
audit,
|
|
1342
1482
|
};
|
|
1343
1483
|
append_transaction(&inner.path, &record)?;
|
|
1484
|
+
inner.authority_revision = base_revision + 1;
|
|
1344
1485
|
inner.sequence = next_sequence;
|
|
1345
1486
|
for row in &rows {
|
|
1346
1487
|
if let Some(operation) = row.operation.clone() {
|
|
@@ -1351,7 +1492,10 @@ impl FeltDb {
|
|
|
1351
1492
|
bucket.remove(&row.key);
|
|
1352
1493
|
}
|
|
1353
1494
|
// Decrement cardinality on delete
|
|
1354
|
-
let count = inner
|
|
1495
|
+
let count = inner
|
|
1496
|
+
.collection_cardinality
|
|
1497
|
+
.entry(row.capability.clone())
|
|
1498
|
+
.or_default();
|
|
1355
1499
|
*count = count.saturating_sub(1);
|
|
1356
1500
|
} else {
|
|
1357
1501
|
// Track whether this is an insert (new row) or update (existing row)
|
|
@@ -1368,14 +1512,25 @@ impl FeltDb {
|
|
|
1368
1512
|
|
|
1369
1513
|
// Increment cardinality only on insert, not update
|
|
1370
1514
|
if is_insert {
|
|
1371
|
-
let count = inner
|
|
1515
|
+
let count = inner
|
|
1516
|
+
.collection_cardinality
|
|
1517
|
+
.entry(row.capability.clone())
|
|
1518
|
+
.or_default();
|
|
1372
1519
|
*count = count.saturating_add(1);
|
|
1373
1520
|
}
|
|
1374
1521
|
}
|
|
1375
1522
|
}
|
|
1376
1523
|
inner.sync_state.merge_vector_clock(&vector_clock);
|
|
1377
1524
|
inner.applied_transactions.insert(transaction_id.into());
|
|
1378
|
-
|
|
1525
|
+
let commit_revision = inner.authority_revision;
|
|
1526
|
+
inner
|
|
1527
|
+
.transaction_revisions
|
|
1528
|
+
.insert(transaction_id.into(), (base_revision, commit_revision));
|
|
1529
|
+
if let Some(hash) = payload_hash {
|
|
1530
|
+
inner
|
|
1531
|
+
.transaction_payload_hashes
|
|
1532
|
+
.insert(transaction_id.into(), hash.into());
|
|
1533
|
+
}
|
|
1379
1534
|
let events = rows
|
|
1380
1535
|
.iter()
|
|
1381
1536
|
.map(|row| ChangeEvent {
|
|
@@ -1388,6 +1543,9 @@ impl FeltDb {
|
|
|
1388
1543
|
(
|
|
1389
1544
|
AtomicCommit {
|
|
1390
1545
|
transaction_id: transaction_id.into(),
|
|
1546
|
+
base_revision,
|
|
1547
|
+
commit_revision: inner.authority_revision,
|
|
1548
|
+
status: TransactionStatus::Committed,
|
|
1391
1549
|
state_before,
|
|
1392
1550
|
state_after: inner.sequence,
|
|
1393
1551
|
rows,
|
|
@@ -1428,7 +1586,13 @@ impl FeltDb {
|
|
|
1428
1586
|
}
|
|
1429
1587
|
|
|
1430
1588
|
// All authorized: proceed with atomic transaction (no partial application)
|
|
1431
|
-
self.apply_atomic_transaction(
|
|
1589
|
+
self.apply_atomic_transaction(
|
|
1590
|
+
transaction_id,
|
|
1591
|
+
expected_parent,
|
|
1592
|
+
preconditions,
|
|
1593
|
+
mutations,
|
|
1594
|
+
audit,
|
|
1595
|
+
)
|
|
1432
1596
|
}
|
|
1433
1597
|
|
|
1434
1598
|
fn insert_internal<T: Serialize>(
|
|
@@ -1480,6 +1644,7 @@ impl FeltDb {
|
|
|
1480
1644
|
inner.change_log.add_operation(op.clone());
|
|
1481
1645
|
|
|
1482
1646
|
append_event(&inner.path, &row)?;
|
|
1647
|
+
inner.authority_revision += 1;
|
|
1483
1648
|
inner
|
|
1484
1649
|
.rows
|
|
1485
1650
|
.entry(capability.clone())
|
|
@@ -1552,6 +1717,7 @@ impl FeltDb {
|
|
|
1552
1717
|
inner.change_log.add_operation(op.clone());
|
|
1553
1718
|
|
|
1554
1719
|
append_event(&inner.path, &row)?;
|
|
1720
|
+
inner.authority_revision += 1;
|
|
1555
1721
|
inner
|
|
1556
1722
|
.rows
|
|
1557
1723
|
.entry(capability.clone())
|
|
@@ -1620,6 +1786,7 @@ impl FeltDb {
|
|
|
1620
1786
|
operation: Some(op),
|
|
1621
1787
|
};
|
|
1622
1788
|
append_event(&inner.path, &tombstone)?;
|
|
1789
|
+
inner.authority_revision += 1;
|
|
1623
1790
|
|
|
1624
1791
|
// Remove from in-memory store
|
|
1625
1792
|
if let Some(bucket) = inner.rows.get_mut(&capability) {
|
|
@@ -1723,20 +1890,39 @@ impl FeltDb {
|
|
|
1723
1890
|
}
|
|
1724
1891
|
|
|
1725
1892
|
/// Traverse one collection in immutable record-key order without materializing it.
|
|
1726
|
-
pub fn list_collection_page(
|
|
1893
|
+
pub fn list_collection_page(
|
|
1894
|
+
&self,
|
|
1895
|
+
capability: &str,
|
|
1896
|
+
after: Option<&str>,
|
|
1897
|
+
limit: usize,
|
|
1898
|
+
) -> Result<Vec<StoredRow>> {
|
|
1727
1899
|
let inner = self.inner.lock().expect("lock poisoned");
|
|
1728
|
-
let Some(rows) = inner.rows.get(capability) else {
|
|
1900
|
+
let Some(rows) = inner.rows.get(capability) else {
|
|
1901
|
+
return Ok(vec![]);
|
|
1902
|
+
};
|
|
1729
1903
|
let values: Box<dyn Iterator<Item = &StoredRow>> = match after {
|
|
1730
|
-
Some(key) => Box::new(
|
|
1904
|
+
Some(key) => Box::new(
|
|
1905
|
+
rows.range::<str, _>((Excluded(key), Unbounded))
|
|
1906
|
+
.map(|(_, row)| row),
|
|
1907
|
+
),
|
|
1731
1908
|
None => Box::new(rows.values()),
|
|
1732
1909
|
};
|
|
1733
|
-
Ok(values
|
|
1910
|
+
Ok(values
|
|
1911
|
+
.filter(|row| !row.deleted)
|
|
1912
|
+
.take(limit)
|
|
1913
|
+
.cloned()
|
|
1914
|
+
.collect())
|
|
1734
1915
|
}
|
|
1735
1916
|
|
|
1736
1917
|
/// Fetch one live collection record without materializing collection state.
|
|
1737
1918
|
pub fn get_collection_record(&self, capability: &str, key: &str) -> Result<Option<StoredRow>> {
|
|
1738
1919
|
let inner = self.inner.lock().expect("lock poisoned");
|
|
1739
|
-
Ok(inner
|
|
1920
|
+
Ok(inner
|
|
1921
|
+
.rows
|
|
1922
|
+
.get(capability)
|
|
1923
|
+
.and_then(|rows| rows.get(key))
|
|
1924
|
+
.filter(|row| !row.deleted)
|
|
1925
|
+
.cloned())
|
|
1740
1926
|
}
|
|
1741
1927
|
|
|
1742
1928
|
/// Subscribe to canonical mutation events.
|
|
@@ -1820,6 +2006,22 @@ impl FeltDb {
|
|
|
1820
2006
|
.collect())
|
|
1821
2007
|
}
|
|
1822
2008
|
|
|
2009
|
+
/// Materialized rows belonging to one application state namespace.
|
|
2010
|
+
///
|
|
2011
|
+
/// State-contract capabilities are encoded as `<state namespace>:<collection>`.
|
|
2012
|
+
/// Selecting the capability buckets before cloning prevents one application's
|
|
2013
|
+
/// read snapshot from materializing every other application's durable rows.
|
|
2014
|
+
pub fn state_rows_for_namespace(&self, state_namespace: &str) -> Result<Vec<StoredRow>> {
|
|
2015
|
+
let inner = self.inner.lock().expect("lock poisoned");
|
|
2016
|
+
let prefix = format!("{state_namespace}:");
|
|
2017
|
+
Ok(inner
|
|
2018
|
+
.rows
|
|
2019
|
+
.iter()
|
|
2020
|
+
.filter(|(capability, _)| capability.starts_with(&prefix))
|
|
2021
|
+
.flat_map(|(_, bucket)| bucket.values().cloned())
|
|
2022
|
+
.collect())
|
|
2023
|
+
}
|
|
2024
|
+
|
|
1823
2025
|
pub fn can_install_snapshot(&self) -> Result<bool> {
|
|
1824
2026
|
let inner = self.inner.lock().expect("lock poisoned");
|
|
1825
2027
|
Ok(!inner.bootstrapped && inner.rows.is_empty() && inner.change_log.pending.is_empty())
|
|
@@ -2095,6 +2297,7 @@ impl FeltDb {
|
|
|
2095
2297
|
{
|
|
2096
2298
|
history_row.operation = Some(op.clone());
|
|
2097
2299
|
append_event(&inner.path, &history_row)?;
|
|
2300
|
+
inner.authority_revision += 1;
|
|
2098
2301
|
}
|
|
2099
2302
|
inner.change_log.add_operation(op);
|
|
2100
2303
|
inner.sync_state.increment_received(1);
|
|
@@ -2120,6 +2323,7 @@ impl FeltDb {
|
|
|
2120
2323
|
match op_to_apply.op_type {
|
|
2121
2324
|
OperationType::Insert | OperationType::Update => {
|
|
2122
2325
|
append_event(&inner.path, &row)?;
|
|
2326
|
+
inner.authority_revision += 1;
|
|
2123
2327
|
inner
|
|
2124
2328
|
.rows
|
|
2125
2329
|
.entry(op_to_apply.capability.clone())
|
|
@@ -2128,6 +2332,7 @@ impl FeltDb {
|
|
|
2128
2332
|
}
|
|
2129
2333
|
OperationType::Delete => {
|
|
2130
2334
|
append_event(&inner.path, &row)?;
|
|
2335
|
+
inner.authority_revision += 1;
|
|
2131
2336
|
if let Some(bucket) = inner.rows.get_mut(&op_to_apply.capability) {
|
|
2132
2337
|
bucket.remove(&op_to_apply.key);
|
|
2133
2338
|
}
|
|
@@ -2530,8 +2735,29 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
2530
2735
|
}
|
|
2531
2736
|
if value.get("record_type").and_then(Value::as_str) == Some("feltdb.transaction.v1") {
|
|
2532
2737
|
let transaction: TransactionLogRecord = serde_json::from_value(value)?;
|
|
2738
|
+
// Older records did not carry a commit revision. Their durable
|
|
2739
|
+
// order in the log supplies an unambiguous upgrade path.
|
|
2740
|
+
inner.authority_revision =
|
|
2741
|
+
inner
|
|
2742
|
+
.authority_revision
|
|
2743
|
+
.max(if transaction.commit_revision == 0 {
|
|
2744
|
+
inner.authority_revision + 1
|
|
2745
|
+
} else {
|
|
2746
|
+
transaction.commit_revision
|
|
2747
|
+
});
|
|
2748
|
+
let base_revision = if transaction.commit_revision == 0 {
|
|
2749
|
+
inner.authority_revision.saturating_sub(1)
|
|
2750
|
+
} else {
|
|
2751
|
+
transaction.base_revision
|
|
2752
|
+
};
|
|
2753
|
+
inner.transaction_revisions.insert(
|
|
2754
|
+
transaction.transaction_id.clone(),
|
|
2755
|
+
(base_revision, inner.authority_revision),
|
|
2756
|
+
);
|
|
2533
2757
|
if let Some(hash) = transaction.payload_hash.clone() {
|
|
2534
|
-
inner
|
|
2758
|
+
inner
|
|
2759
|
+
.transaction_payload_hashes
|
|
2760
|
+
.insert(transaction.transaction_id.clone(), hash);
|
|
2535
2761
|
}
|
|
2536
2762
|
if inner
|
|
2537
2763
|
.applied_transactions
|
|
@@ -2578,6 +2804,7 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
2578
2804
|
// store. Preserve a monotonic sequence when upgrading them.
|
|
2579
2805
|
inner.sequence += 1;
|
|
2580
2806
|
}
|
|
2807
|
+
inner.authority_revision += 1;
|
|
2581
2808
|
if let Some(id) = row
|
|
2582
2809
|
.key
|
|
2583
2810
|
.rsplit_once(':')
|
|
@@ -2605,7 +2832,9 @@ fn replay_log(inner: &mut Inner) -> Result<()> {
|
|
|
2605
2832
|
// Compute collection cardinality from final row state (count non-deleted rows per capability)
|
|
2606
2833
|
for (capability, bucket) in &inner.rows {
|
|
2607
2834
|
let count = bucket.values().filter(|row| !row.deleted).count() as u64;
|
|
2608
|
-
inner
|
|
2835
|
+
inner
|
|
2836
|
+
.collection_cardinality
|
|
2837
|
+
.insert(capability.clone(), count);
|
|
2609
2838
|
}
|
|
2610
2839
|
|
|
2611
2840
|
Ok(())
|
|
@@ -3331,7 +3560,8 @@ mod tests {
|
|
|
3331
3560
|
},
|
|
3332
3561
|
];
|
|
3333
3562
|
|
|
3334
|
-
db.apply_atomic_transaction("txn-1", None, &[], &mutations, None)
|
|
3563
|
+
db.apply_atomic_transaction("txn-1", None, &[], &mutations, None)
|
|
3564
|
+
.unwrap();
|
|
3335
3565
|
|
|
3336
3566
|
let cardinality = db.collection_cardinality(capability).unwrap();
|
|
3337
3567
|
assert_eq!(cardinality, 2, "Cardinality should be 2 after 2 inserts");
|
|
@@ -3352,9 +3582,14 @@ mod tests {
|
|
|
3352
3582
|
value: Some(serde_json::json!({"id": i, "name": format!("Record {}", i)})),
|
|
3353
3583
|
}];
|
|
3354
3584
|
|
|
3355
|
-
db.apply_atomic_transaction(&format!("txn-{}", i), None, &[], &mutations, None)
|
|
3585
|
+
db.apply_atomic_transaction(&format!("txn-{}", i), None, &[], &mutations, None)
|
|
3586
|
+
.unwrap();
|
|
3356
3587
|
let cardinality = db.collection_cardinality(capability).unwrap();
|
|
3357
|
-
assert_eq!(
|
|
3588
|
+
assert_eq!(
|
|
3589
|
+
cardinality, i as u64,
|
|
3590
|
+
"Cardinality should be {} after {} inserts",
|
|
3591
|
+
i, i
|
|
3592
|
+
);
|
|
3358
3593
|
}
|
|
3359
3594
|
|
|
3360
3595
|
let _ = fs::remove_file(path);
|
|
@@ -3384,19 +3619,23 @@ mod tests {
|
|
|
3384
3619
|
},
|
|
3385
3620
|
];
|
|
3386
3621
|
|
|
3387
|
-
db.apply_atomic_transaction("txn-insert", None, &[], &insert_mutations, None)
|
|
3622
|
+
db.apply_atomic_transaction("txn-insert", None, &[], &insert_mutations, None)
|
|
3623
|
+
.unwrap();
|
|
3388
3624
|
assert_eq!(db.collection_cardinality(capability).unwrap(), 3);
|
|
3389
3625
|
|
|
3390
|
-
let delete_mutations = vec![
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
},
|
|
3396
|
-
];
|
|
3626
|
+
let delete_mutations = vec![AtomicMutation {
|
|
3627
|
+
capability: capability.to_string(),
|
|
3628
|
+
key: "rec-1".to_string(),
|
|
3629
|
+
value: None,
|
|
3630
|
+
}];
|
|
3397
3631
|
|
|
3398
|
-
db.apply_atomic_transaction("txn-delete", None, &[], &delete_mutations, None)
|
|
3399
|
-
|
|
3632
|
+
db.apply_atomic_transaction("txn-delete", None, &[], &delete_mutations, None)
|
|
3633
|
+
.unwrap();
|
|
3634
|
+
assert_eq!(
|
|
3635
|
+
db.collection_cardinality(capability).unwrap(),
|
|
3636
|
+
2,
|
|
3637
|
+
"Cardinality should be 2 after deleting 1 record"
|
|
3638
|
+
);
|
|
3400
3639
|
|
|
3401
3640
|
let _ = fs::remove_file(path);
|
|
3402
3641
|
}
|
|
@@ -3413,7 +3652,8 @@ mod tests {
|
|
|
3413
3652
|
value: Some(serde_json::json!({"id": 1, "status": "active"})),
|
|
3414
3653
|
}];
|
|
3415
3654
|
|
|
3416
|
-
db.apply_atomic_transaction("txn-insert", None, &[], &insert_mutations, None)
|
|
3655
|
+
db.apply_atomic_transaction("txn-insert", None, &[], &insert_mutations, None)
|
|
3656
|
+
.unwrap();
|
|
3417
3657
|
assert_eq!(db.collection_cardinality(capability).unwrap(), 1);
|
|
3418
3658
|
|
|
3419
3659
|
let update_mutations = vec![AtomicMutation {
|
|
@@ -3422,8 +3662,13 @@ mod tests {
|
|
|
3422
3662
|
value: Some(serde_json::json!({"id": 1, "status": "inactive"})),
|
|
3423
3663
|
}];
|
|
3424
3664
|
|
|
3425
|
-
db.apply_atomic_transaction("txn-update", None, &[], &update_mutations, None)
|
|
3426
|
-
|
|
3665
|
+
db.apply_atomic_transaction("txn-update", None, &[], &update_mutations, None)
|
|
3666
|
+
.unwrap();
|
|
3667
|
+
assert_eq!(
|
|
3668
|
+
db.collection_cardinality(capability).unwrap(),
|
|
3669
|
+
1,
|
|
3670
|
+
"Cardinality should remain 1 after update"
|
|
3671
|
+
);
|
|
3427
3672
|
|
|
3428
3673
|
let _ = fs::remove_file(path);
|
|
3429
3674
|
}
|
|
@@ -3449,14 +3694,18 @@ mod tests {
|
|
|
3449
3694
|
},
|
|
3450
3695
|
];
|
|
3451
3696
|
|
|
3452
|
-
db.apply_atomic_transaction("txn-1", None, &[], &mutations, None)
|
|
3697
|
+
db.apply_atomic_transaction("txn-1", None, &[], &mutations, None)
|
|
3698
|
+
.unwrap();
|
|
3453
3699
|
assert_eq!(db.collection_cardinality(capability).unwrap(), 2);
|
|
3454
3700
|
}
|
|
3455
3701
|
|
|
3456
3702
|
let reopened = open(&path).unwrap();
|
|
3457
3703
|
let capability = "test_collection";
|
|
3458
3704
|
let cardinality = reopened.collection_cardinality(capability).unwrap();
|
|
3459
|
-
assert_eq!(
|
|
3705
|
+
assert_eq!(
|
|
3706
|
+
cardinality, 2,
|
|
3707
|
+
"Cardinality should survive restart and be recomputed from rows"
|
|
3708
|
+
);
|
|
3460
3709
|
|
|
3461
3710
|
let _ = fs::remove_file(path);
|
|
3462
3711
|
}
|
|
@@ -3488,9 +3737,13 @@ mod tests {
|
|
|
3488
3737
|
});
|
|
3489
3738
|
}
|
|
3490
3739
|
|
|
3491
|
-
db.apply_atomic_transaction("txn-large", None, &[], &mutations, None)
|
|
3740
|
+
db.apply_atomic_transaction("txn-large", None, &[], &mutations, None)
|
|
3741
|
+
.unwrap();
|
|
3492
3742
|
let cardinality = db.collection_cardinality(capability).unwrap();
|
|
3493
|
-
assert_eq!(
|
|
3743
|
+
assert_eq!(
|
|
3744
|
+
cardinality, 100,
|
|
3745
|
+
"Large collection with 100 records should have cardinality 100"
|
|
3746
|
+
);
|
|
3494
3747
|
|
|
3495
3748
|
let _ = fs::remove_file(path);
|
|
3496
3749
|
}
|
|
@@ -3516,7 +3769,8 @@ mod tests {
|
|
|
3516
3769
|
},
|
|
3517
3770
|
];
|
|
3518
3771
|
|
|
3519
|
-
db.apply_atomic_transaction("txn-col1", None, &[], &mutations1, None)
|
|
3772
|
+
db.apply_atomic_transaction("txn-col1", None, &[], &mutations1, None)
|
|
3773
|
+
.unwrap();
|
|
3520
3774
|
|
|
3521
3775
|
let mutations2 = vec![
|
|
3522
3776
|
AtomicMutation {
|
|
@@ -3536,7 +3790,8 @@ mod tests {
|
|
|
3536
3790
|
},
|
|
3537
3791
|
];
|
|
3538
3792
|
|
|
3539
|
-
db.apply_atomic_transaction("txn-col2", None, &[], &mutations2, None)
|
|
3793
|
+
db.apply_atomic_transaction("txn-col2", None, &[], &mutations2, None)
|
|
3794
|
+
.unwrap();
|
|
3540
3795
|
|
|
3541
3796
|
assert_eq!(db.collection_cardinality(col1).unwrap(), 2);
|
|
3542
3797
|
assert_eq!(db.collection_cardinality(col2).unwrap(), 3);
|