@feltdb/core 0.8.4 → 0.8.6
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/Cargo.lock +165 -0
- package/dist/create/server-source/Cargo.toml +9 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +3 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +44 -44
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +12 -24
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +7 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +13 -10
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +12 -9
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +78 -25
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +16 -13
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +13 -5
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +41 -22
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +33 -15
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +100 -33
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +56 -20
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +116 -41
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +186 -55
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +64 -26
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +46 -15
- package/dist/create/server-source/crates/feltdb/src/admission.rs +8 -15
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +43 -13
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +15 -42
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +65 -19
- package/dist/create/server-source/crates/feltdb/src/application.rs +113 -30
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +475 -140
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +17 -15
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/causal_backlog_bound.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +266 -114
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +25 -7
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +10 -15
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +3 -11
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +14 -5
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +21 -25
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +14 -11
- package/dist/create/server-source/crates/feltdb/src/dedup_bound_investigation.rs +103 -22
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +18 -15
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +12 -8
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +1 -5
- package/dist/create/server-source/crates/feltdb/src/durable_operation_identity.rs +87 -23
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +3 -7
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +10 -9
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +1 -6
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +35 -38
- package/dist/create/server-source/crates/feltdb/src/lib.rs +1648 -46
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +0 -1
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +1 -2
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +107 -30
- package/dist/create/server-source/crates/feltdb/src/observability.rs +19 -6
- package/dist/create/server-source/crates/feltdb/src/operation.rs +39 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +12 -11
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +9 -4
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +65 -18
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +193 -57
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +37 -27
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +38 -10
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +20 -14
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +394 -229
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +8 -6
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +11 -13
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +79 -70
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +3 -3
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +33 -11
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +6 -6
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +14 -56
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +29 -27
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +35 -17
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +8 -2
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +59 -15
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +70 -17
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +23 -6
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +12 -3
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +41 -11
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +56 -25
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +701 -245
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +31 -13
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +5 -5
- package/dist/create/server-source/crates/feltdb/src/replica_acknowledgements.rs +48 -18
- package/dist/create/server-source/crates/feltdb/src/replica_membership.rs +30 -11
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +4 -3
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +36 -10
- package/dist/create/server-source/crates/feltdb/src/state_conflict_contract.rs +516 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +13 -4
- package/dist/create/server-source/crates/feltdb/src/state_diff_contract.rs +222 -0
- package/dist/create/server-source/crates/feltdb/src/state_facade.rs +82 -54
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +2 -2
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +1565 -536
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +6 -3
- package/dist/create/server-source/crates/feltdb/src/state_trigger.rs +672 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +9 -3
- package/dist/create/server-source/crates/feltdb/src/submission.rs +5 -11
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +6 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +24 -35
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +24 -8
- package/dist/create/server-source/crates/feltdb/src/transaction_preconditions.rs +248 -59
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +17 -20
- package/dist/create/server-source/crates/feltdb/src/trigger_contract.rs +749 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +1 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +512 -4
- package/dist/create/server-source/crates/feltdb/src/workload_diagnostics.rs +3 -4
- package/dist/create/server-source/crates/feltdb/tests/bounded_read_contract.rs +132 -0
- package/dist/create/server-source/crates/feltdb/tests/branching_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/compaction_stall_contract.rs +272 -0
- package/dist/create/server-source/crates/feltdb/tests/crash_durability_contract.rs +467 -0
- package/dist/create/server-source/crates/feltdb/tests/current_revision_authority_evidence.rs +309 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_backup_contract.rs +445 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_corruption_contract.rs +518 -0
- package/dist/create/server-source/crates/feltdb/tests/durable_format_compatibility.rs +392 -0
- package/dist/create/server-source/crates/feltdb/tests/feltdb_state_boundary_tests.rs +436 -220
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_conflict_contract_corpus.json +1916 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/state_diff_contract_corpus.json +1878 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/trigger_contract_corpus.json +1862 -0
- package/dist/create/server-source/crates/feltdb/tests/operational_health_contract.rs +278 -0
- package/dist/create/server-source/crates/feltdb/tests/pr34_query_collection.rs +2 -1
- package/dist/create/server-source/crates/feltdb/tests/pr35_equality_index.rs +80 -25
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +5 -8
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +52 -44
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +33 -16
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +22 -7
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +41 -22
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +25 -8
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +9 -6
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +120 -69
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +21 -10
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +1365 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_application.rs +868 -0
- package/dist/create/server-source/crates/feltdb/tests/reconciliation_wire_format_evidence.rs +221 -0
- package/dist/create/server-source/crates/feltdb/tests/replicated_history_contract.rs +417 -0
- package/dist/create/server-source/crates/feltdb/tests/resource_scoped_revisions.rs +338 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_identity_contract.rs +1039 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_model_decision.rs +739 -0
- package/dist/create/server-source/crates/feltdb/tests/revision_retention_boundary_evidence.rs +427 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +3 -3
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +25 -22
- package/dist/create/server-source/crates/feltdb/tests/state_conflict_contract_conformance.rs +1799 -0
- package/dist/create/server-source/crates/feltdb/tests/state_diff_contract_conformance.rs +1316 -0
- package/dist/create/server-source/crates/feltdb/tests/state_model_integration.rs +53 -61
- package/dist/create/server-source/crates/feltdb/tests/state_persistence_integration.rs +156 -61
- package/dist/create/server-source/crates/feltdb/tests/state_store_boundary_evidence.rs +299 -0
- package/dist/create/server-source/crates/feltdb/tests/sync_divergence_evidence.rs +255 -0
- package/dist/create/server-source/crates/feltdb/tests/three_way_input_boundary_evidence.rs +249 -0
- package/dist/create/server-source/crates/feltdb/tests/trigger_contract_conformance.rs +994 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +442 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +16 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +164 -13
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +695 -47
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +21 -0
- package/dist/studio-app/assets/{feltdb_wasm-CVQWgXO-.js → feltdb_wasm-C1VhI-U5.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8HXbAXb.wasm +0 -0
- package/dist/studio-app/assets/{index-DwgNAIIX.js → index-Bbos1m2U.js} +1 -1
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workload.d.ts +2 -1
- package/dist/workload.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-CNVpvaZV.wasm +0 -0
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
/// Persistent storage of executed state transitions.
|
|
4
4
|
/// Transitions are immutable once committed; this is the authoritative record of
|
|
5
5
|
/// what state changes have occurred and in what order.
|
|
6
|
-
|
|
7
6
|
use crate::transactions::{StateTransition, StateVersion};
|
|
8
7
|
use std::collections::HashMap;
|
|
9
8
|
use std::sync::{Arc, Mutex};
|
|
@@ -195,10 +194,14 @@ impl Default for MemoryStateTransitionStore {
|
|
|
195
194
|
mod tests {
|
|
196
195
|
use super::*;
|
|
197
196
|
use crate::convergence::VectorClock;
|
|
198
|
-
use crate::transactions::{ConsistencyContract, OperationCommand, OperationId
|
|
197
|
+
use crate::transactions::{ConsistencyContract, Operation, OperationCommand, OperationId};
|
|
199
198
|
use std::collections::HashMap;
|
|
200
199
|
|
|
201
|
-
fn create_test_transition(
|
|
200
|
+
fn create_test_transition(
|
|
201
|
+
from_hash: &str,
|
|
202
|
+
to_hash: &str,
|
|
203
|
+
txn_id: Option<&str>,
|
|
204
|
+
) -> StateTransition {
|
|
202
205
|
let mut from_vc = VectorClock::new();
|
|
203
206
|
from_vc.increment("node1");
|
|
204
207
|
let from_version = StateVersion::new(from_vc, from_hash.to_string());
|
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
//! Durable evaluation of committed state changes into workloads.
|
|
2
|
+
//!
|
|
3
|
+
//! This module is intentionally only the causal bridge from an already committed
|
|
4
|
+
//! operation to `WorkloadStore::create`. It is not a registry, queue, worker, or
|
|
5
|
+
//! execution runtime.
|
|
6
|
+
|
|
7
|
+
use crate::authorization::{SignedGrant, Subject};
|
|
8
|
+
use crate::trigger_contract;
|
|
9
|
+
use crate::workload::{
|
|
10
|
+
CreateWorkload, WorkloadError, WorkloadRetryPolicy, WorkloadStore, WorkloadTimeouts,
|
|
11
|
+
};
|
|
12
|
+
use crate::{FeltDb, FlowError, Operation};
|
|
13
|
+
use serde::{Deserialize, Serialize};
|
|
14
|
+
use serde_json::{json, Value};
|
|
15
|
+
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
16
|
+
use std::fs;
|
|
17
|
+
use std::path::{Path, PathBuf};
|
|
18
|
+
|
|
19
|
+
pub const STATE_TRIGGER_CONTRACT_VERSION: u32 = 1;
|
|
20
|
+
|
|
21
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
22
|
+
#[serde(rename_all = "snake_case")]
|
|
23
|
+
pub enum StateTriggerChangeKind {
|
|
24
|
+
Insert,
|
|
25
|
+
Update,
|
|
26
|
+
Delete,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl StateTriggerChangeKind {
|
|
30
|
+
fn as_str(self) -> &'static str {
|
|
31
|
+
match self {
|
|
32
|
+
Self::Insert => "insert",
|
|
33
|
+
Self::Update => "update",
|
|
34
|
+
Self::Delete => "delete",
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
40
|
+
pub struct StateTriggerPredicate {
|
|
41
|
+
pub field: String,
|
|
42
|
+
pub equals: Value,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
46
|
+
pub struct StateTrigger {
|
|
47
|
+
pub trigger_id: String,
|
|
48
|
+
pub source_collection: String,
|
|
49
|
+
pub change_kind: StateTriggerChangeKind,
|
|
50
|
+
#[serde(default)]
|
|
51
|
+
pub predicate: Option<StateTriggerPredicate>,
|
|
52
|
+
pub target_capability: String,
|
|
53
|
+
pub tenant_id: String,
|
|
54
|
+
pub application_id: String,
|
|
55
|
+
#[serde(default = "default_environment")]
|
|
56
|
+
pub environment: String,
|
|
57
|
+
#[serde(default = "default_revision")]
|
|
58
|
+
pub definition_revision: String,
|
|
59
|
+
pub requested_by: Subject,
|
|
60
|
+
pub authorization_snapshot: SignedGrant,
|
|
61
|
+
#[serde(default)]
|
|
62
|
+
pub capability_snapshot: BTreeSet<String>,
|
|
63
|
+
#[serde(default)]
|
|
64
|
+
pub connection_scopes: BTreeSet<String>,
|
|
65
|
+
#[serde(default)]
|
|
66
|
+
pub priority: i32,
|
|
67
|
+
#[serde(default)]
|
|
68
|
+
pub retry_policy: WorkloadRetryPolicy,
|
|
69
|
+
#[serde(default)]
|
|
70
|
+
pub timeouts: WorkloadTimeouts,
|
|
71
|
+
#[serde(default)]
|
|
72
|
+
pub not_before: Option<i64>,
|
|
73
|
+
#[serde(default = "enabled")]
|
|
74
|
+
pub enabled: bool,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn default_environment() -> String {
|
|
78
|
+
"production".into()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn default_revision() -> String {
|
|
82
|
+
"trigger".into()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn enabled() -> bool {
|
|
86
|
+
true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
impl StateTrigger {
|
|
90
|
+
/// Whether this trigger matches a committed operation.
|
|
91
|
+
///
|
|
92
|
+
/// The decision itself lives in [`crate::trigger_contract`], the
|
|
93
|
+
/// runtime-independent deterministic contract. This method is the
|
|
94
|
+
/// projection from FeltDB's durable types onto that contract's input; it
|
|
95
|
+
/// holds no matching logic of its own, so the native and WASM contract
|
|
96
|
+
/// implementations decide production matching identically by construction.
|
|
97
|
+
pub fn matches(&self, operation: &Operation) -> bool {
|
|
98
|
+
trigger_contract::evaluate_trigger(&trigger_contract::TriggerMatchInput::for_operation(
|
|
99
|
+
self, operation,
|
|
100
|
+
))
|
|
101
|
+
.matched()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// The contract input this trigger and operation produce, for provenance.
|
|
105
|
+
pub fn contract_input(&self, operation: &Operation) -> trigger_contract::TriggerMatchInput {
|
|
106
|
+
trigger_contract::TriggerMatchInput::for_operation(self, operation)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl StateTriggerPredicate {
|
|
111
|
+
pub fn matches(&self, value: Option<&Value>) -> bool {
|
|
112
|
+
let Some(value) = value else { return false };
|
|
113
|
+
get_path(value, &self.field) == Some(&self.equals)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
118
|
+
pub struct TriggeredWorkload {
|
|
119
|
+
pub trigger_id: String,
|
|
120
|
+
pub source_change_id: String,
|
|
121
|
+
pub workload_id: String,
|
|
122
|
+
pub duplicate: bool,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
126
|
+
pub struct StateTriggerStore {
|
|
127
|
+
#[serde(skip)]
|
|
128
|
+
path: PathBuf,
|
|
129
|
+
#[serde(default = "contract_version")]
|
|
130
|
+
pub contract_version: u32,
|
|
131
|
+
#[serde(default)]
|
|
132
|
+
pub triggers: BTreeMap<String, StateTrigger>,
|
|
133
|
+
#[serde(default)]
|
|
134
|
+
pub cursors: HashMap<String, u64>,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn contract_version() -> u32 {
|
|
138
|
+
STATE_TRIGGER_CONTRACT_VERSION
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
impl StateTriggerStore {
|
|
142
|
+
pub fn load(path: impl AsRef<Path>) -> Result<Self, StateTriggerError> {
|
|
143
|
+
let path = path.as_ref().to_path_buf();
|
|
144
|
+
if !path.exists() {
|
|
145
|
+
return Ok(Self {
|
|
146
|
+
path,
|
|
147
|
+
contract_version: STATE_TRIGGER_CONTRACT_VERSION,
|
|
148
|
+
triggers: BTreeMap::new(),
|
|
149
|
+
cursors: HashMap::new(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
let mut value: Self = serde_json::from_slice(&fs::read(&path).map_err(io)?).map_err(io)?;
|
|
153
|
+
value.path = path;
|
|
154
|
+
if value.contract_version == 0 {
|
|
155
|
+
value.contract_version = STATE_TRIGGER_CONTRACT_VERSION;
|
|
156
|
+
}
|
|
157
|
+
Ok(value)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pub fn define(&mut self, trigger: StateTrigger) -> Result<(), StateTriggerError> {
|
|
161
|
+
validate_trigger(&trigger)?;
|
|
162
|
+
self.triggers.insert(trigger.trigger_id.clone(), trigger);
|
|
163
|
+
self.persist()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
pub fn list(&self) -> Vec<StateTrigger> {
|
|
167
|
+
self.triggers.values().cloned().collect()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pub fn evaluate(
|
|
171
|
+
&mut self,
|
|
172
|
+
db: &FeltDb,
|
|
173
|
+
workloads: &mut WorkloadStore,
|
|
174
|
+
now: i64,
|
|
175
|
+
) -> Result<Vec<TriggeredWorkload>, StateTriggerError> {
|
|
176
|
+
let mut produced = Vec::new();
|
|
177
|
+
let mut operations = db
|
|
178
|
+
.operations_since(&self.cursors)
|
|
179
|
+
.map_err(StateTriggerError::from)?;
|
|
180
|
+
operations.sort_by(|left, right| {
|
|
181
|
+
left.instance_id
|
|
182
|
+
.cmp(&right.instance_id)
|
|
183
|
+
.then_with(|| left.sequence.cmp(&right.sequence))
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
for operation in operations {
|
|
187
|
+
let matches = self
|
|
188
|
+
.triggers
|
|
189
|
+
.values()
|
|
190
|
+
.filter(|trigger| trigger.matches(&operation))
|
|
191
|
+
.cloned()
|
|
192
|
+
.collect::<Vec<_>>();
|
|
193
|
+
for trigger in matches {
|
|
194
|
+
let before = workloads.workloads.len();
|
|
195
|
+
let workload = workloads
|
|
196
|
+
.create(create_workload(&trigger, &operation, now))
|
|
197
|
+
.map_err(StateTriggerError::from)?;
|
|
198
|
+
produced.push(TriggeredWorkload {
|
|
199
|
+
trigger_id: trigger.trigger_id,
|
|
200
|
+
source_change_id: source_change_id(&operation),
|
|
201
|
+
workload_id: workload.workload_id,
|
|
202
|
+
duplicate: workloads.workloads.len() == before,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
self.cursors
|
|
206
|
+
.entry(operation.instance_id.clone())
|
|
207
|
+
.and_modify(|cursor| *cursor = (*cursor).max(operation.sequence))
|
|
208
|
+
.or_insert(operation.sequence);
|
|
209
|
+
self.persist()?;
|
|
210
|
+
}
|
|
211
|
+
Ok(produced)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
pub fn cursor_versions(&self) -> HashMap<String, u64> {
|
|
215
|
+
self.cursors.clone()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn validate_trigger(trigger: &StateTrigger) -> Result<(), StateTriggerError> {
|
|
220
|
+
for (field, value) in [
|
|
221
|
+
("trigger_id", &trigger.trigger_id),
|
|
222
|
+
("source_collection", &trigger.source_collection),
|
|
223
|
+
("target_capability", &trigger.target_capability),
|
|
224
|
+
("tenant_id", &trigger.tenant_id),
|
|
225
|
+
("application_id", &trigger.application_id),
|
|
226
|
+
] {
|
|
227
|
+
if value.trim().is_empty() {
|
|
228
|
+
return Err(StateTriggerError::new(
|
|
229
|
+
"TRIGGER_INVALID",
|
|
230
|
+
format!("{field} is required"),
|
|
231
|
+
));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if trigger.authorization_snapshot.grant.subject != trigger.requested_by
|
|
235
|
+
|| trigger.authorization_snapshot.grant.tenant_id != trigger.tenant_id
|
|
236
|
+
|| trigger.authorization_snapshot.grant.application_id != trigger.application_id
|
|
237
|
+
{
|
|
238
|
+
return Err(StateTriggerError::new(
|
|
239
|
+
"TRIGGER_UNAUTHORIZED",
|
|
240
|
+
"authorization snapshot does not bind the trigger requester",
|
|
241
|
+
));
|
|
242
|
+
}
|
|
243
|
+
Ok(())
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fn create_workload(trigger: &StateTrigger, operation: &Operation, now: i64) -> CreateWorkload {
|
|
247
|
+
let source_change_id = source_change_id(operation);
|
|
248
|
+
let kind = trigger.change_kind.as_str();
|
|
249
|
+
let contract_input = trigger.contract_input(operation);
|
|
250
|
+
let capability_snapshot = if trigger.capability_snapshot.is_empty() {
|
|
251
|
+
[trigger.target_capability.clone()].into()
|
|
252
|
+
} else {
|
|
253
|
+
trigger.capability_snapshot.clone()
|
|
254
|
+
};
|
|
255
|
+
CreateWorkload {
|
|
256
|
+
tenant_id: trigger.tenant_id.clone(),
|
|
257
|
+
application_id: trigger.application_id.clone(),
|
|
258
|
+
environment: trigger.environment.clone(),
|
|
259
|
+
definition_id: trigger.target_capability.clone(),
|
|
260
|
+
definition_revision: trigger.definition_revision.clone(),
|
|
261
|
+
trigger: format!("state_change:{}", trigger.trigger_id),
|
|
262
|
+
input: json!({
|
|
263
|
+
"provenance": {
|
|
264
|
+
"trigger_id": trigger.trigger_id,
|
|
265
|
+
"source_change_id": source_change_id,
|
|
266
|
+
"source_collection": operation.capability,
|
|
267
|
+
"source_key": operation.key,
|
|
268
|
+
"change_kind": kind,
|
|
269
|
+
// Which deterministic contract decided this match, at which
|
|
270
|
+
// version, on which runtime, over which exact input. This is a
|
|
271
|
+
// description of the existing decision, not a second record of
|
|
272
|
+
// it: the digest identifies the same committed change the
|
|
273
|
+
// surrounding provenance already names.
|
|
274
|
+
"contract": {
|
|
275
|
+
"id": trigger_contract::TRIGGER_MATCH_CONTRACT_ID,
|
|
276
|
+
"version": trigger_contract::TRIGGER_MATCH_CONTRACT_VERSION,
|
|
277
|
+
"runtime": trigger_contract::ContractRuntime::Native.as_str(),
|
|
278
|
+
"input_digest": trigger_contract::input_digest(&contract_input),
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
"source": {
|
|
282
|
+
"origin": operation.instance_id,
|
|
283
|
+
"sequence": operation.sequence,
|
|
284
|
+
"key": operation.key,
|
|
285
|
+
"collection": operation.capability,
|
|
286
|
+
"content_hash": operation.content_hash,
|
|
287
|
+
},
|
|
288
|
+
"record": operation.value,
|
|
289
|
+
}),
|
|
290
|
+
requested_by: trigger.requested_by.clone(),
|
|
291
|
+
authorization_snapshot: trigger.authorization_snapshot.clone(),
|
|
292
|
+
capability_snapshot,
|
|
293
|
+
connection_scopes: trigger.connection_scopes.clone(),
|
|
294
|
+
priority: trigger.priority,
|
|
295
|
+
idempotency_key: format!("state-trigger:{}:{}", trigger.trigger_id, source_change_id),
|
|
296
|
+
retry_policy: trigger.retry_policy.clone(),
|
|
297
|
+
timeouts: trigger.timeouts.clone(),
|
|
298
|
+
dependencies: vec![],
|
|
299
|
+
not_before: trigger.not_before,
|
|
300
|
+
created_at: now,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fn source_change_id(operation: &Operation) -> String {
|
|
305
|
+
format!("{}:{}", operation.instance_id, operation.sequence)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fn get_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
|
|
309
|
+
if path.is_empty() {
|
|
310
|
+
return Some(value);
|
|
311
|
+
}
|
|
312
|
+
let mut current = value;
|
|
313
|
+
for part in path.split('.') {
|
|
314
|
+
current = current.get(part)?;
|
|
315
|
+
}
|
|
316
|
+
Some(current)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
impl StateTriggerStore {
|
|
320
|
+
fn persist(&self) -> Result<(), StateTriggerError> {
|
|
321
|
+
if let Some(parent) = self.path.parent() {
|
|
322
|
+
fs::create_dir_all(parent).map_err(io)?;
|
|
323
|
+
}
|
|
324
|
+
let temp = self.path.with_extension("tmp");
|
|
325
|
+
fs::write(&temp, serde_json::to_vec_pretty(self).map_err(io)?).map_err(io)?;
|
|
326
|
+
fs::rename(temp, &self.path).map_err(io)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
331
|
+
pub struct StateTriggerError {
|
|
332
|
+
pub code: String,
|
|
333
|
+
pub message: String,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
impl StateTriggerError {
|
|
337
|
+
fn new(code: &str, message: impl Into<String>) -> Self {
|
|
338
|
+
Self {
|
|
339
|
+
code: code.into(),
|
|
340
|
+
message: message.into(),
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
impl std::fmt::Display for StateTriggerError {
|
|
346
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
347
|
+
write!(f, "{}: {}", self.code, self.message)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
impl std::error::Error for StateTriggerError {}
|
|
352
|
+
|
|
353
|
+
impl From<FlowError> for StateTriggerError {
|
|
354
|
+
fn from(value: FlowError) -> Self {
|
|
355
|
+
Self::new("TRIGGER_STATE_REPLAY_FAILED", value.to_string())
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
impl From<WorkloadError> for StateTriggerError {
|
|
360
|
+
fn from(value: WorkloadError) -> Self {
|
|
361
|
+
Self::new(&value.code, value.message)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
fn io(error: impl std::fmt::Display) -> StateTriggerError {
|
|
366
|
+
StateTriggerError::new("TRIGGER_STORAGE_FAILURE", error.to_string())
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[cfg(test)]
|
|
370
|
+
mod tests {
|
|
371
|
+
use super::*;
|
|
372
|
+
use crate::authorization::{
|
|
373
|
+
Grant, GrantConstraints, GrantSigner, GrantStatus, ResourceScope, ResourceScopeKind,
|
|
374
|
+
};
|
|
375
|
+
use crate::workload::WorkloadState;
|
|
376
|
+
use crate::AtomicMutation;
|
|
377
|
+
use tempfile::tempdir;
|
|
378
|
+
|
|
379
|
+
fn grant(subject: &Subject) -> SignedGrant {
|
|
380
|
+
let signer = GrantSigner::new("k", vec![7; 32]).unwrap();
|
|
381
|
+
signer
|
|
382
|
+
.sign(Grant {
|
|
383
|
+
contract_version: 1,
|
|
384
|
+
id: "trigger-grant".into(),
|
|
385
|
+
issuer: Subject::Human("owner".into()),
|
|
386
|
+
subject: subject.clone(),
|
|
387
|
+
tenant_id: "t".into(),
|
|
388
|
+
application_id: "app".into(),
|
|
389
|
+
capabilities: ["workload:execute".into()].into(),
|
|
390
|
+
resources: vec![ResourceScope {
|
|
391
|
+
kind: ResourceScopeKind::Workload,
|
|
392
|
+
uri: "flow://app/workloads/*".into(),
|
|
393
|
+
}],
|
|
394
|
+
constraints: GrantConstraints::default(),
|
|
395
|
+
issued_at: 1,
|
|
396
|
+
expires_at: 9999,
|
|
397
|
+
parent_grant: None,
|
|
398
|
+
revision: 1,
|
|
399
|
+
status: GrantStatus::Active,
|
|
400
|
+
})
|
|
401
|
+
.unwrap()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
fn trigger(id: &str, collection: &str, kind: StateTriggerChangeKind) -> StateTrigger {
|
|
405
|
+
let subject = Subject::Agent("trigger-agent".into());
|
|
406
|
+
StateTrigger {
|
|
407
|
+
trigger_id: id.into(),
|
|
408
|
+
source_collection: collection.into(),
|
|
409
|
+
change_kind: kind,
|
|
410
|
+
predicate: None,
|
|
411
|
+
target_capability: "identity".into(),
|
|
412
|
+
tenant_id: "t".into(),
|
|
413
|
+
application_id: "app".into(),
|
|
414
|
+
environment: "production".into(),
|
|
415
|
+
definition_revision: "r1".into(),
|
|
416
|
+
requested_by: subject.clone(),
|
|
417
|
+
authorization_snapshot: grant(&subject),
|
|
418
|
+
capability_snapshot: ["identity".into()].into(),
|
|
419
|
+
connection_scopes: BTreeSet::new(),
|
|
420
|
+
priority: 0,
|
|
421
|
+
retry_policy: Default::default(),
|
|
422
|
+
timeouts: Default::default(),
|
|
423
|
+
not_before: None,
|
|
424
|
+
enabled: true,
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
fn stores(name: &str) -> (tempfile::TempDir, FeltDb, StateTriggerStore, WorkloadStore) {
|
|
429
|
+
let dir = tempdir().unwrap();
|
|
430
|
+
let db = FeltDb::open(dir.path().join(format!("{name}.log"))).unwrap();
|
|
431
|
+
let triggers =
|
|
432
|
+
StateTriggerStore::load(dir.path().join(format!("{name}.triggers.json"))).unwrap();
|
|
433
|
+
let workloads =
|
|
434
|
+
WorkloadStore::load(dir.path().join(format!("{name}.workloads.json"))).unwrap();
|
|
435
|
+
(dir, db, triggers, workloads)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn matching_change_creates_workload_with_provenance() {
|
|
440
|
+
let (_dir, db, mut triggers, mut workloads) = stores("matching");
|
|
441
|
+
triggers
|
|
442
|
+
.define(trigger(
|
|
443
|
+
"on-order",
|
|
444
|
+
"orders",
|
|
445
|
+
StateTriggerChangeKind::Insert,
|
|
446
|
+
))
|
|
447
|
+
.unwrap();
|
|
448
|
+
db.insert("orders:1", json!({"status":"new"})).unwrap();
|
|
449
|
+
|
|
450
|
+
let created = triggers.evaluate(&db, &mut workloads, 10).unwrap();
|
|
451
|
+
|
|
452
|
+
assert_eq!(created.len(), 1);
|
|
453
|
+
let workload = workloads.get(&created[0].workload_id).unwrap();
|
|
454
|
+
assert_eq!(workload.state, WorkloadState::Ready);
|
|
455
|
+
assert_eq!(workload.trigger, "state_change:on-order");
|
|
456
|
+
assert_eq!(
|
|
457
|
+
workload.idempotency_key,
|
|
458
|
+
format!("state-trigger:on-order:{}", created[0].source_change_id)
|
|
459
|
+
);
|
|
460
|
+
assert_eq!(workload.input["provenance"]["trigger_id"], "on-order");
|
|
461
|
+
assert_eq!(workload.input["provenance"]["source_collection"], "orders");
|
|
462
|
+
assert_eq!(workload.input["record"]["status"], "new");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#[test]
|
|
466
|
+
fn nonmatching_change_creates_nothing() {
|
|
467
|
+
let (_dir, db, mut triggers, mut workloads) = stores("nonmatching");
|
|
468
|
+
triggers
|
|
469
|
+
.define(trigger(
|
|
470
|
+
"on-order",
|
|
471
|
+
"orders",
|
|
472
|
+
StateTriggerChangeKind::Insert,
|
|
473
|
+
))
|
|
474
|
+
.unwrap();
|
|
475
|
+
db.insert("customers:1", json!({"name":"Ada"})).unwrap();
|
|
476
|
+
|
|
477
|
+
assert!(triggers
|
|
478
|
+
.evaluate(&db, &mut workloads, 10)
|
|
479
|
+
.unwrap()
|
|
480
|
+
.is_empty());
|
|
481
|
+
assert!(workloads.list("t", "app").is_empty());
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#[test]
|
|
485
|
+
fn predicate_accepts_and_rejects_deterministically() {
|
|
486
|
+
let (_dir, db, mut triggers, mut workloads) = stores("predicate");
|
|
487
|
+
let mut accepted = trigger("paid", "orders", StateTriggerChangeKind::Insert);
|
|
488
|
+
accepted.predicate = Some(StateTriggerPredicate {
|
|
489
|
+
field: "status".into(),
|
|
490
|
+
equals: json!("paid"),
|
|
491
|
+
});
|
|
492
|
+
triggers.define(accepted).unwrap();
|
|
493
|
+
db.insert("orders:1", json!({"status":"draft"})).unwrap();
|
|
494
|
+
db.insert("orders:2", json!({"status":"paid"})).unwrap();
|
|
495
|
+
|
|
496
|
+
let created = triggers.evaluate(&db, &mut workloads, 10).unwrap();
|
|
497
|
+
|
|
498
|
+
assert_eq!(created.len(), 1);
|
|
499
|
+
assert_eq!(
|
|
500
|
+
workloads.get(&created[0].workload_id).unwrap().input["record"]["status"],
|
|
501
|
+
"paid"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
#[test]
|
|
506
|
+
fn multiple_triggers_create_independent_workloads() {
|
|
507
|
+
let (_dir, db, mut triggers, mut workloads) = stores("multiple");
|
|
508
|
+
triggers
|
|
509
|
+
.define(trigger("one", "orders", StateTriggerChangeKind::Insert))
|
|
510
|
+
.unwrap();
|
|
511
|
+
triggers
|
|
512
|
+
.define(trigger("two", "orders", StateTriggerChangeKind::Insert))
|
|
513
|
+
.unwrap();
|
|
514
|
+
db.insert("orders:1", json!({"status":"paid"})).unwrap();
|
|
515
|
+
|
|
516
|
+
let created = triggers.evaluate(&db, &mut workloads, 10).unwrap();
|
|
517
|
+
|
|
518
|
+
assert_eq!(created.len(), 2);
|
|
519
|
+
assert_ne!(created[0].workload_id, created[1].workload_id);
|
|
520
|
+
assert_eq!(workloads.list("t", "app").len(), 2);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
#[test]
|
|
524
|
+
fn duplicate_and_replayed_change_create_exactly_one_workload() {
|
|
525
|
+
let (dir, db, mut triggers, mut workloads) = stores("replay");
|
|
526
|
+
let trigger_path = dir.path().join("replay.triggers.json");
|
|
527
|
+
let workload_path = dir.path().join("replay.workloads.json");
|
|
528
|
+
triggers
|
|
529
|
+
.define(trigger("once", "orders", StateTriggerChangeKind::Insert))
|
|
530
|
+
.unwrap();
|
|
531
|
+
db.insert("orders:1", json!({"status":"paid"})).unwrap();
|
|
532
|
+
|
|
533
|
+
let first = triggers.evaluate(&db, &mut workloads, 10).unwrap();
|
|
534
|
+
let again = triggers.evaluate(&db, &mut workloads, 11).unwrap();
|
|
535
|
+
assert_eq!(first.len(), 1);
|
|
536
|
+
assert!(
|
|
537
|
+
again.is_empty(),
|
|
538
|
+
"durable cursor suppresses already evaluated changes"
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
let mut cursor_lost = StateTriggerStore::load(&trigger_path).unwrap();
|
|
542
|
+
cursor_lost.cursors.clear();
|
|
543
|
+
let mut reopened_workloads = WorkloadStore::load(&workload_path).unwrap();
|
|
544
|
+
let replayed = cursor_lost
|
|
545
|
+
.evaluate(&db, &mut reopened_workloads, 12)
|
|
546
|
+
.unwrap();
|
|
547
|
+
assert_eq!(replayed.len(), 1, "lost cursor replays the change");
|
|
548
|
+
assert!(
|
|
549
|
+
replayed[0].duplicate,
|
|
550
|
+
"workload idempotency suppresses the duplicate"
|
|
551
|
+
);
|
|
552
|
+
assert_eq!(reopened_workloads.list("t", "app").len(), 1);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
#[test]
|
|
556
|
+
fn restart_after_observing_before_workload_creation_replays() {
|
|
557
|
+
let (dir, db, mut triggers, mut workloads) = stores("restart-before-create");
|
|
558
|
+
let trigger_path = dir.path().join("restart-before-create.triggers.json");
|
|
559
|
+
let workload_path = dir.path().join("restart-before-create.workloads.json");
|
|
560
|
+
triggers
|
|
561
|
+
.define(trigger("recover", "orders", StateTriggerChangeKind::Insert))
|
|
562
|
+
.unwrap();
|
|
563
|
+
db.insert("orders:1", json!({"status":"paid"})).unwrap();
|
|
564
|
+
drop(triggers);
|
|
565
|
+
drop(workloads);
|
|
566
|
+
|
|
567
|
+
let mut restarted_triggers = StateTriggerStore::load(trigger_path).unwrap();
|
|
568
|
+
let mut restarted_workloads = WorkloadStore::load(workload_path).unwrap();
|
|
569
|
+
let created = restarted_triggers
|
|
570
|
+
.evaluate(&db, &mut restarted_workloads, 10)
|
|
571
|
+
.unwrap();
|
|
572
|
+
|
|
573
|
+
assert_eq!(created.len(), 1);
|
|
574
|
+
assert_eq!(restarted_workloads.list("t", "app").len(), 1);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#[test]
|
|
578
|
+
fn future_eligibility_and_unsupported_capability_are_preserved() {
|
|
579
|
+
let (_dir, db, mut triggers, mut workloads) = stores("eligibility");
|
|
580
|
+
let mut future = trigger("future", "orders", StateTriggerChangeKind::Insert);
|
|
581
|
+
future.target_capability = "missing-capability".into();
|
|
582
|
+
future.capability_snapshot = ["missing-capability".into()].into();
|
|
583
|
+
future.not_before = Some(1_000);
|
|
584
|
+
triggers.define(future).unwrap();
|
|
585
|
+
db.insert("orders:1", json!({"status":"paid"})).unwrap();
|
|
586
|
+
|
|
587
|
+
let created = triggers.evaluate(&db, &mut workloads, 10).unwrap();
|
|
588
|
+
let workload = workloads.get(&created[0].workload_id).unwrap();
|
|
589
|
+
let workload_id = workload.workload_id.clone();
|
|
590
|
+
assert_eq!(workload.next_attempt_at, Some(1_000));
|
|
591
|
+
let caps: BTreeSet<String> = ["identity".into()].into();
|
|
592
|
+
assert_eq!(
|
|
593
|
+
workloads
|
|
594
|
+
.claim(&workload_id, "worker", &caps, 1_001, 30_000)
|
|
595
|
+
.unwrap_err()
|
|
596
|
+
.code,
|
|
597
|
+
"WORKLOAD_CAPABILITY_DENIED"
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
#[test]
|
|
602
|
+
fn cancellation_and_retry_use_normal_workload_semantics() {
|
|
603
|
+
let (_dir, db, mut triggers, mut workloads) = stores("lifecycle");
|
|
604
|
+
let mut lifecycle = trigger("life", "orders", StateTriggerChangeKind::Insert);
|
|
605
|
+
lifecycle.retry_policy.max_attempts = 1;
|
|
606
|
+
triggers.define(lifecycle).unwrap();
|
|
607
|
+
db.insert("orders:1", json!({"status":"paid"})).unwrap();
|
|
608
|
+
db.insert("orders:2", json!({"status":"paid"})).unwrap();
|
|
609
|
+
let created = triggers.evaluate(&db, &mut workloads, 10).unwrap();
|
|
610
|
+
assert_eq!(created.len(), 2);
|
|
611
|
+
|
|
612
|
+
let cancelled = created[0].workload_id.clone();
|
|
613
|
+
assert_eq!(
|
|
614
|
+
workloads.cancel(&cancelled, "operator", 11).unwrap(),
|
|
615
|
+
WorkloadState::Cancelled
|
|
616
|
+
);
|
|
617
|
+
assert_eq!(
|
|
618
|
+
workloads
|
|
619
|
+
.retry(&cancelled, "operator", 12)
|
|
620
|
+
.unwrap_err()
|
|
621
|
+
.code,
|
|
622
|
+
"WORKLOAD_TRANSITION_DENIED"
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
let failed = created[1].workload_id.clone();
|
|
626
|
+
let caps: BTreeSet<String> = ["identity".into()].into();
|
|
627
|
+
let claim = workloads
|
|
628
|
+
.claim(&failed, "worker", &caps, 12, 30_000)
|
|
629
|
+
.unwrap();
|
|
630
|
+
workloads
|
|
631
|
+
.start(&failed, &claim.claim_id, "worker", claim.fencing_token, 13)
|
|
632
|
+
.unwrap();
|
|
633
|
+
assert_eq!(
|
|
634
|
+
workloads
|
|
635
|
+
.fail(
|
|
636
|
+
&failed,
|
|
637
|
+
&claim.claim_id,
|
|
638
|
+
"worker",
|
|
639
|
+
claim.fencing_token,
|
|
640
|
+
"worker_failure",
|
|
641
|
+
"crashed",
|
|
642
|
+
14
|
|
643
|
+
)
|
|
644
|
+
.unwrap(),
|
|
645
|
+
WorkloadState::DeadLetter
|
|
646
|
+
);
|
|
647
|
+
workloads.retry(&failed, "operator", 15).unwrap();
|
|
648
|
+
assert_eq!(workloads.get(&failed).unwrap().state, WorkloadState::Ready);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
#[test]
|
|
652
|
+
fn atomic_transaction_changes_are_replayed_into_triggers() {
|
|
653
|
+
let (_dir, db, mut triggers, mut workloads) = stores("transaction");
|
|
654
|
+
triggers
|
|
655
|
+
.define(trigger("tx", "orders", StateTriggerChangeKind::Insert))
|
|
656
|
+
.unwrap();
|
|
657
|
+
db.apply_atomic_transaction(
|
|
658
|
+
"tx-1",
|
|
659
|
+
None,
|
|
660
|
+
&[],
|
|
661
|
+
&[AtomicMutation {
|
|
662
|
+
capability: "orders".into(),
|
|
663
|
+
key: "orders:1".into(),
|
|
664
|
+
value: Some(json!({"status":"paid"})),
|
|
665
|
+
}],
|
|
666
|
+
None,
|
|
667
|
+
)
|
|
668
|
+
.unwrap();
|
|
669
|
+
|
|
670
|
+
assert_eq!(triggers.evaluate(&db, &mut workloads, 10).unwrap().len(), 1);
|
|
671
|
+
}
|
|
672
|
+
}
|