@feltdb/core 0.4.12 → 0.4.14
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/cli/commands.js +42 -25
- package/dist/cli/index.js +1 -1
- package/dist/create/cli.js +39 -4
- package/dist/create/create.js +30 -18
- package/dist/create/docker-compose-generator.js +68 -11
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +2238 -0
- package/dist/create/server-source/Cargo.toml +7 -0
- package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
- package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
- package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
- package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
- package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
- package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
- package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
- package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
- package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
- package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
- package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
- package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
- package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
- package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
- package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
- package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
- package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
- package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
- package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
- package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
- package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
- package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
- package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
- package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
- package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
- package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
- package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
- package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
- package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
- package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
- package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
- package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/flowspec.d.ts +2 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +28 -4
- package/dist/state-contract.d.ts +7 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/ConflictExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-Q0Nx6gLE.js → components-9kDSWiGL.js} +163 -47
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-BHOmLZF-.js → index-DVdvmf69.js} +9 -9
- package/dist/studio-app/index.html +1 -1
- 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
|
+
}
|