@feltdb/core 0.4.11 → 0.4.13

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.
Files changed (164) hide show
  1. package/dist/cli/commands.js +5 -2
  2. package/dist/cli/index.js +1 -1
  3. package/dist/create/cli.js +16 -1
  4. package/dist/create/create.js +30 -16
  5. package/dist/create/docker-compose-generator.js +48 -9
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/Cargo.lock +2238 -0
  8. package/dist/create/server-source/Cargo.toml +7 -0
  9. package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
  10. package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
  11. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  28. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  29. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
  30. package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
  31. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  32. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  33. package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
  34. package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
  35. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  36. package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
  37. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  38. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  39. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  40. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  41. package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
  42. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  43. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  44. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  45. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  46. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  47. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  48. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  49. package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
  50. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  51. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  52. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  53. package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
  54. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  55. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  56. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  57. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  58. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  59. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  60. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  61. package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
  62. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  63. package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
  64. package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
  65. package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
  66. package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
  67. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  68. package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
  69. package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
  70. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  71. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
  72. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  73. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  74. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  75. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  76. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  77. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  78. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  79. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  80. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  81. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  82. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  83. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  84. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  85. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  86. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  87. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  88. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  89. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  90. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  91. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  92. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  93. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  94. package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
  95. package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
  96. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
  97. package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
  98. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  99. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  100. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  101. package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
  102. package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
  103. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  104. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
  105. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  106. package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
  107. package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
  108. package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
  109. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  110. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  111. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  112. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  113. package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
  114. package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
  115. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  116. package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
  117. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  118. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  119. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  120. package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
  121. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
  122. package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
  123. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  124. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  125. package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  126. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
  127. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
  128. package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  129. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
  130. package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
  131. package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
  132. package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
  133. package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  134. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  135. package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
  136. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
  137. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  138. package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  139. package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
  140. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
  141. package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
  142. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  143. package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  144. package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
  145. package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
  146. package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  147. package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  149. package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
  150. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  151. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  152. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  153. package/dist/flowspec.d.ts +2 -0
  154. package/dist/flowspec.d.ts.map +1 -1
  155. package/dist/flowspec.js +28 -4
  156. package/dist/state-contract.d.ts +7 -1
  157. package/dist/state-contract.d.ts.map +1 -1
  158. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  159. package/dist/studio/components/index.js +1 -1
  160. package/dist/studio/{components-q43NSTTH.js → components-bHTARBin.js} +160 -89
  161. package/dist/studio/index.js +31 -28
  162. package/dist/studio-app/assets/{index-DZpBoVMp.js → index-BqKquLuU.js} +8 -8
  163. package/dist/studio-app/index.html +1 -1
  164. package/package.json +1 -1
@@ -0,0 +1,928 @@
1
+ //! Worker and mesh semantics. Compute is replaceable; durable workload state is authority.
2
+ use crate::authorization::{SignedGrant, Subject};
3
+ use crate::workload::{Workload, WorkloadClaim, WorkloadError, WorkloadStore};
4
+ use serde::{Deserialize, Serialize};
5
+ use serde_json::Value;
6
+ use sha2::{Digest, Sha256};
7
+ use std::collections::{BTreeMap, BTreeSet};
8
+ use std::fs;
9
+ use std::path::{Path, PathBuf};
10
+ pub const WORKER_CONTRACT_VERSION: u32 = 1;
11
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
12
+ #[serde(rename_all = "snake_case")]
13
+ pub enum WorkerRuntimeKind {
14
+ Wasm,
15
+ Process,
16
+ Container,
17
+ Browser,
18
+ Remote,
19
+ Hosted,
20
+ Edge,
21
+ Customer,
22
+ }
23
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
24
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
25
+ pub enum WorkerLifecycle {
26
+ Registered,
27
+ Ready,
28
+ Busy,
29
+ Draining,
30
+ Offline,
31
+ Recovering,
32
+ }
33
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
34
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
35
+ pub enum WorkerHealth {
36
+ Healthy,
37
+ Degraded,
38
+ Unhealthy,
39
+ Unknown,
40
+ }
41
+ #[derive(Debug, Clone, Serialize, Deserialize, Default)]
42
+ pub struct WorkerResourceProfile {
43
+ pub cpu_millis: Option<u64>,
44
+ pub memory_bytes: Option<u64>,
45
+ pub disk_bytes: Option<u64>,
46
+ pub gpu_count: Option<u32>,
47
+ }
48
+ #[derive(Debug, Clone, Serialize, Deserialize)]
49
+ pub struct WorkerCapacity {
50
+ pub concurrency: u32,
51
+ #[serde(default)]
52
+ pub active: u32,
53
+ }
54
+ #[derive(Debug, Clone, Serialize, Deserialize)]
55
+ pub struct WorkerRegistration {
56
+ pub tenant_id: String,
57
+ pub application_id: String,
58
+ pub name: String,
59
+ pub runtime: WorkerRuntimeKind,
60
+ pub version: String,
61
+ #[serde(default)]
62
+ pub capabilities: BTreeSet<String>,
63
+ #[serde(default)]
64
+ pub workload_kinds: BTreeSet<String>,
65
+ #[serde(default)]
66
+ pub resource_profile: WorkerResourceProfile,
67
+ #[serde(default)]
68
+ pub labels: BTreeMap<String, String>,
69
+ pub capacity: WorkerCapacity,
70
+ pub authorization_grant: SignedGrant,
71
+ pub registered_by: Subject,
72
+ pub registered_at: i64,
73
+ }
74
+ #[derive(Debug, Clone, Serialize, Deserialize)]
75
+ pub struct Worker {
76
+ pub contract_version: u32,
77
+ pub worker_id: String,
78
+ pub tenant_id: String,
79
+ pub application_id: String,
80
+ pub name: String,
81
+ pub runtime: WorkerRuntimeKind,
82
+ pub version: String,
83
+ pub declared_capabilities: BTreeSet<String>,
84
+ pub granted_capabilities: BTreeSet<String>,
85
+ pub workload_kinds: BTreeSet<String>,
86
+ pub resource_profile: WorkerResourceProfile,
87
+ pub labels: BTreeMap<String, String>,
88
+ pub capacity: WorkerCapacity,
89
+ pub health: WorkerHealth,
90
+ pub lifecycle: WorkerLifecycle,
91
+ pub heartbeat_interval_ms: u64,
92
+ pub heartbeat_timeout_ms: u64,
93
+ pub last_seen: i64,
94
+ pub capability_version: u64,
95
+ pub authorization_grant_id: String,
96
+ #[serde(default)]
97
+ pub claims: BTreeSet<String>,
98
+ }
99
+ #[derive(Debug, Clone, Serialize, Deserialize)]
100
+ pub struct WorkerHeartbeat {
101
+ pub worker_id: String,
102
+ pub version: String,
103
+ pub capability_version: u64,
104
+ pub capacity: WorkerCapacity,
105
+ pub health: WorkerHealth,
106
+ pub timestamp: i64,
107
+ #[serde(default)]
108
+ pub running_claims: BTreeSet<String>,
109
+ }
110
+ #[derive(Debug, Clone, Serialize, Deserialize)]
111
+ pub struct WorkerPool {
112
+ pub pool_id: String,
113
+ pub tenant_id: String,
114
+ pub application_id: Option<String>,
115
+ pub name: String,
116
+ #[serde(default)]
117
+ pub capabilities: BTreeSet<String>,
118
+ #[serde(default)]
119
+ pub required_labels: BTreeMap<String, String>,
120
+ #[serde(default)]
121
+ pub worker_ids: BTreeSet<String>,
122
+ pub max_concurrency: Option<u32>,
123
+ pub created_at: i64,
124
+ }
125
+ #[derive(Debug, Clone, Serialize, Deserialize, Default)]
126
+ pub struct PlacementPolicy {
127
+ #[serde(default)]
128
+ pub required_labels: BTreeMap<String, String>,
129
+ #[serde(default)]
130
+ pub affinity: BTreeMap<String, String>,
131
+ #[serde(default)]
132
+ pub anti_affinity: BTreeMap<String, String>,
133
+ pub pool_id: Option<String>,
134
+ }
135
+ #[derive(Debug, Clone, Serialize, Deserialize)]
136
+ pub struct PlacementDecision {
137
+ pub decision_id: String,
138
+ pub workload_id: String,
139
+ pub eligible_workers: Vec<String>,
140
+ pub selected_worker: Option<String>,
141
+ pub reason: String,
142
+ pub evaluated_at: i64,
143
+ }
144
+ #[derive(Debug, Clone, Serialize, Deserialize)]
145
+ pub struct MeshExecutionClaim {
146
+ pub workload_id: String,
147
+ pub attempt_id: String,
148
+ pub worker_id: String,
149
+ pub lease_id: String,
150
+ pub claim_id: String,
151
+ pub fencing_token: u64,
152
+ pub capability_snapshot: BTreeSet<String>,
153
+ pub execution_context_id: String,
154
+ pub issued_at: i64,
155
+ pub expires_at: i64,
156
+ }
157
+ #[derive(Debug, Clone, Serialize, Deserialize)]
158
+ pub struct RuntimePolicy {
159
+ pub max_cpu_millis: Option<u64>,
160
+ pub max_memory_bytes: Option<u64>,
161
+ pub max_duration_ms: u64,
162
+ pub network_policy: String,
163
+ pub filesystem_policy: String,
164
+ pub secret_policy: String,
165
+ pub artifact_policy: String,
166
+ }
167
+ #[derive(Debug, Clone, Serialize, Deserialize)]
168
+ pub struct RuntimeExecutionResult {
169
+ pub result_id: String,
170
+ pub output: Value,
171
+ #[serde(default)]
172
+ pub logs: Vec<String>,
173
+ #[serde(default)]
174
+ pub artifacts: Vec<String>,
175
+ #[serde(default)]
176
+ pub resource_usage: BTreeMap<String, u64>,
177
+ }
178
+ pub trait WorkerRuntime {
179
+ fn start(
180
+ &mut self,
181
+ claim: &MeshExecutionClaim,
182
+ policy: &RuntimePolicy,
183
+ ) -> Result<(), WorkerMeshError>;
184
+ fn execute(
185
+ &mut self,
186
+ claim: &MeshExecutionClaim,
187
+ input: &Value,
188
+ ) -> Result<RuntimeExecutionResult, WorkerMeshError>;
189
+ fn cancel(&mut self, claim: &MeshExecutionClaim) -> Result<(), WorkerMeshError>;
190
+ fn timeout(&mut self, claim: &MeshExecutionClaim) -> Result<(), WorkerMeshError>;
191
+ }
192
+ pub trait WorkerPlacement {
193
+ fn eligible_workers(
194
+ &self,
195
+ workload: &Workload,
196
+ workers: &[Worker],
197
+ policy: &PlacementPolicy,
198
+ ) -> Vec<String>;
199
+ fn select_worker(
200
+ &self,
201
+ workload: &Workload,
202
+ candidates: &[Worker],
203
+ ) -> Result<String, WorkerMeshError>;
204
+ }
205
+ #[derive(Default)]
206
+ pub struct DeterministicPlacement;
207
+ impl WorkerPlacement for DeterministicPlacement {
208
+ fn eligible_workers(
209
+ &self,
210
+ w: &Workload,
211
+ workers: &[Worker],
212
+ policy: &PlacementPolicy,
213
+ ) -> Vec<String> {
214
+ let mut ids = workers
215
+ .iter()
216
+ .filter(|worker| {
217
+ worker.tenant_id == w.tenant_id
218
+ && worker.application_id == w.application_id
219
+ && worker.lifecycle == WorkerLifecycle::Ready
220
+ && worker.health == WorkerHealth::Healthy
221
+ && worker.capacity.active < worker.capacity.concurrency
222
+ && w.capability_snapshot
223
+ .is_subset(&worker.granted_capabilities)
224
+ && (!worker.workload_kinds.is_empty())
225
+ .then_some(worker.workload_kinds.contains(&w.definition_id))
226
+ .unwrap_or(true)
227
+ && policy
228
+ .required_labels
229
+ .iter()
230
+ .all(|(k, v)| worker.labels.get(k) == Some(v))
231
+ && policy
232
+ .anti_affinity
233
+ .iter()
234
+ .all(|(k, v)| worker.labels.get(k) != Some(v))
235
+ })
236
+ .map(|v| v.worker_id.clone())
237
+ .collect::<Vec<_>>();
238
+ ids.sort();
239
+ ids
240
+ }
241
+ fn select_worker(
242
+ &self,
243
+ _: &Workload,
244
+ candidates: &[Worker],
245
+ ) -> Result<String, WorkerMeshError> {
246
+ candidates
247
+ .iter()
248
+ .min_by_key(|worker| {
249
+ (
250
+ worker.capacity.active,
251
+ std::cmp::Reverse(worker.capacity.concurrency),
252
+ worker.worker_id.clone(),
253
+ )
254
+ })
255
+ .map(|v| v.worker_id.clone())
256
+ .ok_or_else(|| {
257
+ WorkerMeshError::new(
258
+ "MESH_NO_ELIGIBLE_WORKER",
259
+ "no worker satisfies placement policy",
260
+ )
261
+ })
262
+ }
263
+ }
264
+ #[derive(Debug, Clone, Serialize, Deserialize)]
265
+ pub struct WorkerReconciliation {
266
+ pub worker_id: String,
267
+ pub known_claims: BTreeSet<String>,
268
+ pub running_attempts: BTreeSet<String>,
269
+ pub completed_attempts: BTreeSet<String>,
270
+ pub failed_attempts: BTreeSet<String>,
271
+ pub worker_version: String,
272
+ pub capability_version: u64,
273
+ pub timestamp: i64,
274
+ }
275
+ #[derive(Debug, Clone, Serialize, Deserialize)]
276
+ pub struct WorkerReconciliationResult {
277
+ pub authoritative_claims: BTreeSet<String>,
278
+ pub fenced_claims: BTreeSet<String>,
279
+ pub cancel_attempts: BTreeSet<String>,
280
+ pub worker: Worker,
281
+ }
282
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
283
+ pub struct WorkerMeshError {
284
+ pub code: String,
285
+ pub message: String,
286
+ }
287
+ impl WorkerMeshError {
288
+ fn new(code: &str, message: impl Into<String>) -> Self {
289
+ Self {
290
+ code: code.into(),
291
+ message: message.into(),
292
+ }
293
+ }
294
+ }
295
+ impl std::fmt::Display for WorkerMeshError {
296
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297
+ write!(f, "{}: {}", self.code, self.message)
298
+ }
299
+ }
300
+ impl std::error::Error for WorkerMeshError {}
301
+ impl From<WorkloadError> for WorkerMeshError {
302
+ fn from(v: WorkloadError) -> Self {
303
+ Self::new(&v.code, v.message)
304
+ }
305
+ }
306
+
307
+ #[derive(Debug, Clone, Serialize, Deserialize)]
308
+ pub struct WorkerMeshStore {
309
+ #[serde(skip)]
310
+ path: PathBuf,
311
+ #[serde(default)]
312
+ pub workers: BTreeMap<String, Worker>,
313
+ #[serde(default)]
314
+ pub pools: BTreeMap<String, WorkerPool>,
315
+ #[serde(default)]
316
+ pub placements: Vec<PlacementDecision>,
317
+ }
318
+ impl WorkerMeshStore {
319
+ pub fn load(path: impl AsRef<Path>) -> Result<Self, WorkerMeshError> {
320
+ let path = path.as_ref().to_path_buf();
321
+ if !path.exists() {
322
+ return Ok(Self {
323
+ path,
324
+ workers: BTreeMap::new(),
325
+ pools: BTreeMap::new(),
326
+ placements: vec![],
327
+ });
328
+ }
329
+ let mut value: Self = serde_json::from_slice(&fs::read(&path).map_err(io)?).map_err(io)?;
330
+ value.path = path;
331
+ Ok(value)
332
+ }
333
+ fn persist(&self) -> Result<(), WorkerMeshError> {
334
+ if let Some(parent) = self.path.parent() {
335
+ fs::create_dir_all(parent).map_err(io)?;
336
+ }
337
+ let temp = self.path.with_extension("tmp");
338
+ fs::write(&temp, serde_json::to_vec_pretty(self).map_err(io)?).map_err(io)?;
339
+ fs::rename(temp, &self.path).map_err(io)
340
+ }
341
+ pub fn register(&mut self, input: WorkerRegistration) -> Result<Worker, WorkerMeshError> {
342
+ if input.capacity.concurrency == 0 {
343
+ return Err(WorkerMeshError::new(
344
+ "WORKER_INVALID",
345
+ "concurrency must be positive",
346
+ ));
347
+ }
348
+ if input.authorization_grant.grant.subject != input.registered_by
349
+ || input.authorization_grant.grant.tenant_id != input.tenant_id
350
+ || input.authorization_grant.grant.application_id != input.application_id
351
+ {
352
+ return Err(WorkerMeshError::new(
353
+ "WORKER_UNAUTHORIZED",
354
+ "registration grant scope mismatch",
355
+ ));
356
+ }
357
+ let allowed = input
358
+ .authorization_grant
359
+ .grant
360
+ .capabilities
361
+ .iter()
362
+ .filter_map(|v| v.strip_prefix("worker:capability:").map(String::from))
363
+ .collect::<BTreeSet<_>>();
364
+ let granted = input.capabilities.intersection(&allowed).cloned().collect();
365
+ let seed = format!(
366
+ "{}:{}:{}",
367
+ input.tenant_id, input.application_id, input.name
368
+ );
369
+ let id = format!("worker_{}", &hex(&Sha256::digest(seed))[..24]);
370
+ if let Some(existing) = self.workers.get(&id) {
371
+ return Ok(existing.clone());
372
+ }
373
+ let worker = Worker {
374
+ contract_version: 1,
375
+ worker_id: id.clone(),
376
+ tenant_id: input.tenant_id,
377
+ application_id: input.application_id,
378
+ name: input.name,
379
+ runtime: input.runtime,
380
+ version: input.version,
381
+ declared_capabilities: input.capabilities,
382
+ granted_capabilities: granted,
383
+ workload_kinds: input.workload_kinds,
384
+ resource_profile: input.resource_profile,
385
+ labels: input.labels,
386
+ capacity: input.capacity,
387
+ health: WorkerHealth::Unknown,
388
+ lifecycle: WorkerLifecycle::Registered,
389
+ heartbeat_interval_ms: 10_000,
390
+ heartbeat_timeout_ms: 30_000,
391
+ last_seen: input.registered_at,
392
+ capability_version: input.authorization_grant.grant.revision,
393
+ authorization_grant_id: input.authorization_grant.grant.id,
394
+ claims: BTreeSet::new(),
395
+ };
396
+ self.workers.insert(id, worker.clone());
397
+ self.persist()?;
398
+ Ok(worker)
399
+ }
400
+ pub fn heartbeat(
401
+ &mut self,
402
+ id: &str,
403
+ input: WorkerHeartbeat,
404
+ clock_tolerance_seconds: i64,
405
+ ) -> Result<Worker, WorkerMeshError> {
406
+ let worker = self
407
+ .workers
408
+ .get_mut(id)
409
+ .ok_or_else(|| WorkerMeshError::new("WORKER_NOT_FOUND", "worker not found"))?;
410
+ if input.worker_id != id
411
+ || input.capability_version != worker.capability_version
412
+ || input.version != worker.version
413
+ {
414
+ return Err(WorkerMeshError::new(
415
+ "WORKER_HEARTBEAT_REJECTED",
416
+ "worker identity, version, or capability version changed",
417
+ ));
418
+ }
419
+ if input.timestamp + clock_tolerance_seconds < worker.last_seen {
420
+ return Err(WorkerMeshError::new(
421
+ "WORKER_CLOCK_INVALID",
422
+ "heartbeat is outside clock tolerance",
423
+ ));
424
+ }
425
+ if matches!(worker.lifecycle, WorkerLifecycle::Offline) {
426
+ return Err(WorkerMeshError::new(
427
+ "WORKER_RECOVERY_REQUIRED",
428
+ "offline worker must reconcile before heartbeat",
429
+ ));
430
+ }
431
+ worker.last_seen = input.timestamp;
432
+ worker.health = input.health;
433
+ worker.capacity.active = input.capacity.active.min(worker.capacity.concurrency);
434
+ worker.claims = input.running_claims;
435
+ worker.lifecycle = if worker.lifecycle == WorkerLifecycle::Draining {
436
+ if worker.claims.is_empty() {
437
+ WorkerLifecycle::Offline
438
+ } else {
439
+ WorkerLifecycle::Draining
440
+ }
441
+ } else if worker.capacity.active >= worker.capacity.concurrency {
442
+ WorkerLifecycle::Busy
443
+ } else {
444
+ WorkerLifecycle::Ready
445
+ };
446
+ let result = worker.clone();
447
+ self.persist()?;
448
+ Ok(result)
449
+ }
450
+ pub fn transition(&mut self, id: &str, to: WorkerLifecycle) -> Result<Worker, WorkerMeshError> {
451
+ let worker = self
452
+ .workers
453
+ .get_mut(id)
454
+ .ok_or_else(|| WorkerMeshError::new("WORKER_NOT_FOUND", "worker not found"))?;
455
+ if !worker_transition(worker.lifecycle, to) {
456
+ return Err(WorkerMeshError::new(
457
+ "WORKER_TRANSITION_DENIED",
458
+ format!("{:?} cannot transition to {:?}", worker.lifecycle, to),
459
+ ));
460
+ }
461
+ worker.lifecycle = to;
462
+ let result = worker.clone();
463
+ self.persist()?;
464
+ Ok(result)
465
+ }
466
+ pub fn drain(&mut self, id: &str) -> Result<Worker, WorkerMeshError> {
467
+ self.transition(id, WorkerLifecycle::Draining)
468
+ }
469
+ pub fn recover(&mut self, id: &str) -> Result<Worker, WorkerMeshError> {
470
+ self.transition(id, WorkerLifecycle::Recovering)
471
+ }
472
+ pub fn remove(&mut self, id: &str) -> Result<(), WorkerMeshError> {
473
+ let worker = self
474
+ .workers
475
+ .get(id)
476
+ .ok_or_else(|| WorkerMeshError::new("WORKER_NOT_FOUND", "worker not found"))?;
477
+ if !worker.claims.is_empty() {
478
+ return Err(WorkerMeshError::new(
479
+ "WORKER_BUSY",
480
+ "worker with active claims cannot be removed",
481
+ ));
482
+ }
483
+ self.workers.remove(id);
484
+ for pool in self.pools.values_mut() {
485
+ pool.worker_ids.remove(id);
486
+ }
487
+ self.persist()
488
+ }
489
+ pub fn detect_failures(
490
+ &mut self,
491
+ now: i64,
492
+ workloads: &mut WorkloadStore,
493
+ ) -> Result<Vec<String>, WorkerMeshError> {
494
+ let failed = self
495
+ .workers
496
+ .values()
497
+ .filter(|w| {
498
+ !matches!(w.lifecycle, WorkerLifecycle::Offline)
499
+ && w.last_seen + (w.heartbeat_timeout_ms / 1000) as i64 <= now
500
+ })
501
+ .map(|w| w.worker_id.clone())
502
+ .collect::<Vec<_>>();
503
+ for id in &failed {
504
+ if let Some(worker) = self.workers.get_mut(id) {
505
+ worker.lifecycle = WorkerLifecycle::Offline;
506
+ worker.health = WorkerHealth::Unhealthy;
507
+ worker.claims.clear();
508
+ }
509
+ }
510
+ workloads.recover_expired(now)?;
511
+ self.persist()?;
512
+ Ok(failed)
513
+ }
514
+ pub fn create_pool(&mut self, pool: WorkerPool) -> Result<WorkerPool, WorkerMeshError> {
515
+ if self.pools.contains_key(&pool.pool_id) {
516
+ return Err(WorkerMeshError::new(
517
+ "WORKER_POOL_EXISTS",
518
+ "worker pool exists",
519
+ ));
520
+ }
521
+ for worker in &pool.worker_ids {
522
+ if !self.workers.contains_key(worker) {
523
+ return Err(WorkerMeshError::new("WORKER_NOT_FOUND", worker));
524
+ }
525
+ }
526
+ self.pools.insert(pool.pool_id.clone(), pool.clone());
527
+ self.persist()?;
528
+ Ok(pool)
529
+ }
530
+ pub fn eligible(&self, workload: &Workload, policy: &PlacementPolicy) -> Vec<String> {
531
+ let mut workers = self.workers.values().cloned().collect::<Vec<_>>();
532
+ if let Some(pool) = policy.pool_id.as_ref().and_then(|id| self.pools.get(id)) {
533
+ workers.retain(|v| {
534
+ pool.worker_ids.contains(&v.worker_id)
535
+ && pool.capabilities.is_subset(&v.granted_capabilities)
536
+ });
537
+ }
538
+ DeterministicPlacement.eligible_workers(workload, &workers, policy)
539
+ }
540
+ pub fn place_and_claim(
541
+ &mut self,
542
+ workloads: &mut WorkloadStore,
543
+ workload_id: &str,
544
+ worker_id: &str,
545
+ now: i64,
546
+ lease_ms: u64,
547
+ ) -> Result<MeshExecutionClaim, WorkerMeshError> {
548
+ let workload = workloads.get(workload_id)?.clone();
549
+ let worker = self
550
+ .workers
551
+ .get(worker_id)
552
+ .ok_or_else(|| WorkerMeshError::new("WORKER_NOT_FOUND", "worker not found"))?
553
+ .clone();
554
+ if !self
555
+ .eligible(&workload, &PlacementPolicy::default())
556
+ .contains(&worker_id.to_string())
557
+ {
558
+ return Err(WorkerMeshError::new(
559
+ "MESH_PLACEMENT_DENIED",
560
+ "worker is not eligible",
561
+ ));
562
+ }
563
+ let claim = workloads.claim(
564
+ workload_id,
565
+ worker_id,
566
+ &worker.granted_capabilities,
567
+ now,
568
+ lease_ms,
569
+ )?;
570
+ let attempt = workloads
571
+ .get(workload_id)?
572
+ .attempts
573
+ .last()
574
+ .unwrap()
575
+ .attempt_id
576
+ .clone();
577
+ let mesh = mesh_claim(&claim, &attempt);
578
+ let current = self.workers.get_mut(worker_id).unwrap();
579
+ current.claims.insert(claim.claim_id.clone());
580
+ current.capacity.active += 1;
581
+ current.lifecycle = if current.capacity.active >= current.capacity.concurrency {
582
+ WorkerLifecycle::Busy
583
+ } else {
584
+ WorkerLifecycle::Ready
585
+ };
586
+ self.placements.push(PlacementDecision {
587
+ decision_id: format!("placement_{}", self.placements.len() + 1),
588
+ workload_id: workload_id.into(),
589
+ eligible_workers: vec![worker_id.into()],
590
+ selected_worker: Some(worker_id.into()),
591
+ reason: "deterministic capability and capacity match".into(),
592
+ evaluated_at: now,
593
+ });
594
+ self.persist()?;
595
+ Ok(mesh)
596
+ }
597
+ pub fn release_claim(
598
+ &mut self,
599
+ worker_id: &str,
600
+ claim_id: &str,
601
+ ) -> Result<(), WorkerMeshError> {
602
+ let worker = self
603
+ .workers
604
+ .get_mut(worker_id)
605
+ .ok_or_else(|| WorkerMeshError::new("WORKER_NOT_FOUND", "worker not found"))?;
606
+ worker.claims.remove(claim_id);
607
+ worker.capacity.active = worker.capacity.active.saturating_sub(1);
608
+ if worker.lifecycle == WorkerLifecycle::Draining && worker.claims.is_empty() {
609
+ worker.lifecycle = WorkerLifecycle::Offline
610
+ } else if worker.lifecycle == WorkerLifecycle::Busy {
611
+ worker.lifecycle = WorkerLifecycle::Ready
612
+ }
613
+ self.persist()
614
+ }
615
+ pub fn reconcile(
616
+ &mut self,
617
+ input: WorkerReconciliation,
618
+ workloads: &WorkloadStore,
619
+ ) -> Result<WorkerReconciliationResult, WorkerMeshError> {
620
+ let worker = self
621
+ .workers
622
+ .get_mut(&input.worker_id)
623
+ .ok_or_else(|| WorkerMeshError::new("WORKER_NOT_FOUND", "worker not found"))?;
624
+ if input.worker_version != worker.version
625
+ || input.capability_version != worker.capability_version
626
+ {
627
+ return Err(WorkerMeshError::new(
628
+ "WORKER_RECONCILIATION_DENIED",
629
+ "worker contract version changed",
630
+ ));
631
+ }
632
+ let authoritative = workloads
633
+ .workloads
634
+ .values()
635
+ .filter_map(|w| w.current_claim.as_ref())
636
+ .filter(|c| c.worker_id == input.worker_id && c.expires_at > input.timestamp)
637
+ .map(|c| c.claim_id.clone())
638
+ .collect::<BTreeSet<_>>();
639
+ let fenced = input
640
+ .known_claims
641
+ .difference(&authoritative)
642
+ .cloned()
643
+ .collect();
644
+ let cancel = input
645
+ .running_attempts
646
+ .iter()
647
+ .filter(|attempt| {
648
+ !workloads.workloads.values().any(|w| {
649
+ w.attempts.iter().any(|a| &a.attempt_id == *attempt)
650
+ && matches!(
651
+ w.state,
652
+ crate::workload::WorkloadState::Running
653
+ | crate::workload::WorkloadState::Checkpointed
654
+ )
655
+ })
656
+ })
657
+ .cloned()
658
+ .collect();
659
+ worker.claims = authoritative.clone();
660
+ worker.last_seen = input.timestamp;
661
+ worker.lifecycle = WorkerLifecycle::Ready;
662
+ worker.health = WorkerHealth::Healthy;
663
+ let result = WorkerReconciliationResult {
664
+ authoritative_claims: authoritative,
665
+ fenced_claims: fenced,
666
+ cancel_attempts: cancel,
667
+ worker: worker.clone(),
668
+ };
669
+ self.persist()?;
670
+ Ok(result)
671
+ }
672
+ }
673
+ fn mesh_claim(claim: &WorkloadClaim, attempt: &str) -> MeshExecutionClaim {
674
+ MeshExecutionClaim {
675
+ workload_id: claim.workload_id.clone(),
676
+ attempt_id: attempt.into(),
677
+ worker_id: claim.worker_id.clone(),
678
+ lease_id: claim.lease_id.clone(),
679
+ claim_id: claim.claim_id.clone(),
680
+ fencing_token: claim.fencing_token,
681
+ capability_snapshot: claim.capability_snapshot.clone(),
682
+ execution_context_id: format!("context_{}", claim.claim_id),
683
+ issued_at: claim.claimed_at,
684
+ expires_at: claim.expires_at,
685
+ }
686
+ }
687
+ pub fn worker_transition(from: WorkerLifecycle, to: WorkerLifecycle) -> bool {
688
+ use WorkerLifecycle::*;
689
+ matches!(
690
+ (from, to),
691
+ (Registered, Ready)
692
+ | (Ready, Busy)
693
+ | (Busy, Ready)
694
+ | (Ready, Draining)
695
+ | (Busy, Draining)
696
+ | (Draining, Offline)
697
+ | (Ready, Offline)
698
+ | (Busy, Offline)
699
+ | (Offline, Recovering)
700
+ | (Recovering, Ready)
701
+ | (Recovering, Offline)
702
+ )
703
+ }
704
+ pub fn validate_mesh_claim(
705
+ claim: &MeshExecutionClaim,
706
+ worker: &Worker,
707
+ workload: &Workload,
708
+ now: i64,
709
+ ) -> Result<(), WorkerMeshError> {
710
+ if claim.worker_id != worker.worker_id
711
+ || claim.workload_id != workload.workload_id
712
+ || worker.tenant_id != workload.tenant_id
713
+ || worker.application_id != workload.application_id
714
+ {
715
+ return Err(WorkerMeshError::new(
716
+ "MESH_CLAIM_SCOPE_DENIED",
717
+ "claim crosses worker/workload scope",
718
+ ));
719
+ }
720
+ if claim.expires_at <= now
721
+ || workload.current_claim.as_ref().is_none_or(|current| {
722
+ current.claim_id != claim.claim_id || current.fencing_token != claim.fencing_token
723
+ })
724
+ {
725
+ return Err(WorkerMeshError::new(
726
+ "MESH_STALE_FENCE",
727
+ "claim is expired or fenced",
728
+ ));
729
+ }
730
+ if !claim
731
+ .capability_snapshot
732
+ .is_subset(&worker.granted_capabilities)
733
+ || claim.capability_snapshot != workload.capability_snapshot
734
+ {
735
+ return Err(WorkerMeshError::new(
736
+ "MESH_CAPABILITY_DENIED",
737
+ "claim capability snapshot is invalid",
738
+ ));
739
+ }
740
+ Ok(())
741
+ }
742
+ fn io(e: impl std::fmt::Display) -> WorkerMeshError {
743
+ WorkerMeshError::new("MESH_STORAGE_FAILURE", e.to_string())
744
+ }
745
+ fn hex(bytes: &[u8]) -> String {
746
+ bytes.iter().map(|v| format!("{v:02x}")).collect()
747
+ }
748
+
749
+ #[cfg(test)]
750
+ mod tests {
751
+ use super::*;
752
+ use crate::authorization::{
753
+ Grant, GrantConstraints, GrantSigner, GrantStatus, ResourceScope, ResourceScopeKind,
754
+ };
755
+ use crate::workload::{CreateWorkload, WorkloadRetryPolicy, WorkloadTimeouts};
756
+ fn setup(
757
+ name: &str,
758
+ ) -> (
759
+ PathBuf,
760
+ PathBuf,
761
+ WorkerMeshStore,
762
+ WorkloadStore,
763
+ SignedGrant,
764
+ ) {
765
+ let mesh = std::env::temp_dir().join(format!("mesh-{name}-{}.json", std::process::id()));
766
+ let work =
767
+ std::env::temp_dir().join(format!("mesh-work-{name}-{}.json", std::process::id()));
768
+ let signer = GrantSigner::new("k", vec![4; 32]).unwrap();
769
+ let subject = Subject::Service("registrar".into());
770
+ let grant = Grant {
771
+ contract_version: 1,
772
+ id: "g".into(),
773
+ issuer: Subject::Human("h".into()),
774
+ subject,
775
+ tenant_id: "t".into(),
776
+ application_id: "a".into(),
777
+ capabilities: [
778
+ "worker:register".into(),
779
+ "worker:capability:documents.extract".into(),
780
+ ]
781
+ .into(),
782
+ resources: vec![ResourceScope {
783
+ kind: ResourceScopeKind::ResourcePrefix,
784
+ uri: "flow://a/workers/*".into(),
785
+ }],
786
+ constraints: GrantConstraints::default(),
787
+ issued_at: 1,
788
+ expires_at: 999,
789
+ parent_grant: None,
790
+ revision: 1,
791
+ status: GrantStatus::Active,
792
+ };
793
+ (
794
+ mesh.clone(),
795
+ work.clone(),
796
+ WorkerMeshStore::load(&mesh).unwrap(),
797
+ WorkloadStore::load(&work).unwrap(),
798
+ signer.sign(grant).unwrap(),
799
+ )
800
+ }
801
+ fn register(store: &mut WorkerMeshStore, grant: SignedGrant) -> Worker {
802
+ store
803
+ .register(WorkerRegistration {
804
+ tenant_id: "t".into(),
805
+ application_id: "a".into(),
806
+ name: "edge".into(),
807
+ runtime: WorkerRuntimeKind::Customer,
808
+ version: "1".into(),
809
+ capabilities: ["documents.extract".into(), "database.admin".into()].into(),
810
+ workload_kinds: BTreeSet::new(),
811
+ resource_profile: Default::default(),
812
+ labels: [("environment".into(), "production".into())].into(),
813
+ capacity: WorkerCapacity {
814
+ concurrency: 1,
815
+ active: 0,
816
+ },
817
+ registered_by: Subject::Service("registrar".into()),
818
+ authorization_grant: grant,
819
+ registered_at: 10,
820
+ })
821
+ .unwrap()
822
+ }
823
+ #[test]
824
+ fn registration_intersects_capabilities_and_lifecycle_is_strict() {
825
+ let (m, w, mut mesh, _, grant) = setup("register");
826
+ let worker = register(&mut mesh, grant);
827
+ assert_eq!(
828
+ worker.granted_capabilities,
829
+ ["documents.extract".into()].into()
830
+ );
831
+ assert!(mesh
832
+ .transition(&worker.worker_id, WorkerLifecycle::Busy)
833
+ .is_err());
834
+ mesh.heartbeat(
835
+ &worker.worker_id,
836
+ WorkerHeartbeat {
837
+ worker_id: worker.worker_id.clone(),
838
+ version: "1".into(),
839
+ capability_version: 1,
840
+ capacity: WorkerCapacity {
841
+ concurrency: 1,
842
+ active: 0,
843
+ },
844
+ health: WorkerHealth::Healthy,
845
+ timestamp: 11,
846
+ running_claims: BTreeSet::new(),
847
+ },
848
+ 2,
849
+ )
850
+ .unwrap();
851
+ let _ = fs::remove_file(m);
852
+ let _ = fs::remove_file(w);
853
+ }
854
+ #[test]
855
+ fn placement_claim_fencing_failure_and_recovery() {
856
+ let (m, wpath, mut mesh, mut workloads, grant) = setup("claim");
857
+ let worker = register(&mut mesh, grant.clone());
858
+ mesh.heartbeat(
859
+ &worker.worker_id,
860
+ WorkerHeartbeat {
861
+ worker_id: worker.worker_id.clone(),
862
+ version: "1".into(),
863
+ capability_version: 1,
864
+ capacity: WorkerCapacity {
865
+ concurrency: 1,
866
+ active: 0,
867
+ },
868
+ health: WorkerHealth::Healthy,
869
+ timestamp: 11,
870
+ running_claims: BTreeSet::new(),
871
+ },
872
+ 2,
873
+ )
874
+ .unwrap();
875
+ let subject = grant.grant.subject.clone();
876
+ let workload = workloads
877
+ .create(CreateWorkload {
878
+ tenant_id: "t".into(),
879
+ application_id: "a".into(),
880
+ environment: "production".into(),
881
+ definition_id: "extract".into(),
882
+ definition_revision: "r".into(),
883
+ trigger: "manual".into(),
884
+ input: serde_json::json!({"file":"artifact://x"}),
885
+ requested_by: subject,
886
+ authorization_snapshot: grant,
887
+ capability_snapshot: ["documents.extract".into()].into(),
888
+ connection_scopes: BTreeSet::new(),
889
+ priority: 0,
890
+ idempotency_key: "x".into(),
891
+ retry_policy: WorkloadRetryPolicy::default(),
892
+ timeouts: WorkloadTimeouts::default(),
893
+ dependencies: vec![],
894
+ created_at: 11,
895
+ })
896
+ .unwrap();
897
+ let claim = mesh
898
+ .place_and_claim(
899
+ &mut workloads,
900
+ &workload.workload_id,
901
+ &worker.worker_id,
902
+ 12,
903
+ 1000,
904
+ )
905
+ .unwrap();
906
+ assert!(validate_mesh_claim(
907
+ &claim,
908
+ mesh.workers.get(&worker.worker_id).unwrap(),
909
+ workloads.get(&workload.workload_id).unwrap(),
910
+ 12
911
+ )
912
+ .is_ok());
913
+ mesh.detect_failures(50, &mut workloads).unwrap();
914
+ assert_eq!(
915
+ validate_mesh_claim(
916
+ &claim,
917
+ mesh.workers.get(&worker.worker_id).unwrap(),
918
+ workloads.get(&workload.workload_id).unwrap(),
919
+ 50
920
+ )
921
+ .unwrap_err()
922
+ .code,
923
+ "MESH_STALE_FENCE"
924
+ );
925
+ let _ = fs::remove_file(m);
926
+ let _ = fs::remove_file(wpath);
927
+ }
928
+ }