Skip to main content

appcore_contracts/policy/
profile.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: profile.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/07/22 15:41:18 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/07/22 15:41:18 by dnettoRaw
8//      ###########      S: 1.0.1-rc.8
9// =============================================================================
10
11use super::*;
12
13/// Generic role assigned to one executable core.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum CoreRole {
17    /// General-purpose application host.
18    GeneralPurpose,
19    /// Coordination and control workload.
20    Control,
21    /// Background worker workload.
22    Worker,
23    /// Storage-oriented workload.
24    Storage,
25    /// Compute-intensive workload.
26    Compute,
27    /// Extensible role unknown to the base runtime.
28    Custom(String),
29}
30
31/// Resource availability or requirement of one core.
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
33pub struct ResourceProfile {
34    cpu_cores: Option<u16>,
35    memory_bytes: Option<u64>,
36    gpu_count: u16,
37}
38
39impl ResourceProfile {
40    /// Creates a resource profile.
41    pub fn new(cpu_cores: Option<u16>, memory_bytes: Option<u64>, gpu_count: u16) -> Self {
42        Self {
43            cpu_cores,
44            memory_bytes,
45            gpu_count,
46        }
47    }
48
49    /// Returns known CPU core count.
50    pub fn cpu_cores(&self) -> Option<u16> {
51        self.cpu_cores
52    }
53
54    /// Returns known memory in bytes.
55    pub fn memory_bytes(&self) -> Option<u64> {
56        self.memory_bytes
57    }
58
59    /// Returns available GPU count.
60    pub fn gpu_count(&self) -> u16 {
61        self.gpu_count
62    }
63
64    fn validate(&self) -> ContractResult<()> {
65        if self.cpu_cores == Some(0) || self.memory_bytes == Some(0) {
66            return Err(ContractError::InvalidValue {
67                field: "resources",
68                reason: "known CPU and memory values must be greater than zero",
69            });
70        }
71        Ok(())
72    }
73}
74
75/// Broad workload category used as one scheduler signal.
76#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum WorkloadClass {
79    /// Mixed or unspecified workload.
80    #[default]
81    General,
82    /// Latency-sensitive interactive work.
83    Interactive,
84    /// Background batch work.
85    Batch,
86    /// Compute-intensive work.
87    Compute,
88    /// I/O-intensive work.
89    Io,
90}
91
92/// Scheduler inputs advertised by one core.
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct SchedulingProfile {
95    weight: u16,
96    priority: i16,
97    max_concurrency: u32,
98    available: bool,
99    workload: WorkloadClass,
100    affinity: BTreeSet<String>,
101}
102
103impl SchedulingProfile {
104    /// Creates scheduler inputs with an empty affinity set.
105    pub fn new(
106        weight: u16,
107        priority: i16,
108        max_concurrency: u32,
109        workload: WorkloadClass,
110    ) -> ContractResult<Self> {
111        if weight == 0 || max_concurrency == 0 {
112            return Err(ContractError::InvalidValue {
113                field: "scheduling",
114                reason: "weight and max concurrency must be greater than zero",
115            });
116        }
117        Ok(Self {
118            weight,
119            priority,
120            max_concurrency,
121            available: true,
122            workload,
123            affinity: BTreeSet::new(),
124        })
125    }
126
127    /// Marks current scheduling availability.
128    pub fn with_availability(mut self, available: bool) -> Self {
129        self.available = available;
130        self
131    }
132
133    /// Adds a validated affinity label.
134    pub fn with_affinity(mut self, affinity: impl Into<String>) -> ContractResult<Self> {
135        let affinity = affinity.into();
136        validate_text("scheduling.affinity", &affinity, 128)?;
137        self.affinity.insert(affinity);
138        Ok(self)
139    }
140
141    /// Returns relative scheduler weight.
142    pub fn weight(&self) -> u16 {
143        self.weight
144    }
145
146    /// Returns scheduler priority.
147    pub fn priority(&self) -> i16 {
148        self.priority
149    }
150
151    /// Returns maximum accepted concurrency.
152    pub fn max_concurrency(&self) -> u32 {
153        self.max_concurrency
154    }
155
156    /// Reports whether this core currently accepts work.
157    pub fn is_available(&self) -> bool {
158        self.available
159    }
160
161    /// Returns the workload class.
162    pub fn workload(&self) -> WorkloadClass {
163        self.workload
164    }
165
166    /// Returns scheduler affinity labels.
167    pub fn affinity(&self) -> &BTreeSet<String> {
168        &self.affinity
169    }
170
171    pub(crate) fn validate(&self) -> ContractResult<()> {
172        if self.weight == 0 || self.max_concurrency == 0 {
173            return Err(ContractError::InvalidValue {
174                field: "scheduling",
175                reason: "weight and max concurrency must be greater than zero",
176            });
177        }
178        for affinity in &self.affinity {
179            validate_text("scheduling.affinity", affinity, 128)?;
180        }
181        Ok(())
182    }
183}
184
185/// Provider-independent profile advertised by one executable core.
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct CoreProfile {
188    role: CoreRole,
189    service_id: ServiceId,
190    capabilities: BTreeSet<CapabilityId>,
191    leadership: LeadershipRequirement,
192    resources: ResourceProfile,
193    scheduling: SchedulingProfile,
194}
195
196impl CoreProfile {
197    /// Creates a profile whose leadership lease is scoped to the same service.
198    pub fn new(
199        role: CoreRole,
200        service_id: ServiceId,
201        capabilities: impl IntoIterator<Item = CapabilityId>,
202        leadership: LeadershipRequirement,
203        resources: ResourceProfile,
204        scheduling: SchedulingProfile,
205    ) -> ContractResult<Self> {
206        if leadership.service_id() != &service_id {
207            return Err(ContractError::InvalidValue {
208                field: "core_profile.leadership",
209                reason: "leadership must be scoped to the profile service",
210            });
211        }
212        let profile = Self {
213            role,
214            service_id,
215            capabilities: capabilities.into_iter().collect(),
216            leadership,
217            resources,
218            scheduling,
219        };
220        profile.validate()?;
221        Ok(profile)
222    }
223
224    /// Returns the generic core role.
225    pub fn role(&self) -> &CoreRole {
226        &self.role
227    }
228
229    /// Returns the service coordinated by this profile.
230    pub fn service_id(&self) -> &ServiceId {
231        &self.service_id
232    }
233
234    /// Returns advertised capabilities.
235    pub fn capabilities(&self) -> &BTreeSet<CapabilityId> {
236        &self.capabilities
237    }
238
239    /// Returns service-scoped leadership requirements.
240    pub fn leadership(&self) -> &LeadershipRequirement {
241        &self.leadership
242    }
243
244    /// Returns the resource profile.
245    pub fn resources(&self) -> &ResourceProfile {
246        &self.resources
247    }
248
249    /// Returns scheduler inputs.
250    pub fn scheduling(&self) -> &SchedulingProfile {
251        &self.scheduling
252    }
253
254    pub(crate) fn validate(&self) -> ContractResult<()> {
255        if let CoreRole::Custom(role) = &self.role {
256            validate_text("core_profile.role", role, 128)?;
257        }
258        self.leadership.validate()?;
259        self.resources.validate()?;
260        self.scheduling.validate()?;
261        if self.leadership.service_id() != &self.service_id {
262            return Err(ContractError::InvalidValue {
263                field: "core_profile.leadership",
264                reason: "leadership must be scoped to the profile service",
265            });
266        }
267        Ok(())
268    }
269}