bp_core/service.rs
1//! Service definitions for BillPouch.
2//!
3//! Three service types, each maps to a different role in the pelican metaphor:
4//!
5//! - `Pouch` — bids local storage into the network (the pelican's pouch)
6//! - `Bill` — personal I/O interface to the user's files (the pelican's bill)
7//! - `Post` — pure routing / relay node; contributes only CPU + RAM
8
9use crate::error::{BpError, BpResult};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::str::FromStr;
13use uuid::Uuid;
14
15/// The role a service plays in the network.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum ServiceType {
19 /// Bids local storage into the network.
20 Pouch,
21 /// Personal file I/O interface.
22 Bill,
23 /// Routing / relay node (CPU + RAM only).
24 Post,
25}
26
27impl std::fmt::Display for ServiceType {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 ServiceType::Pouch => write!(f, "pouch"),
31 ServiceType::Bill => write!(f, "bill"),
32 ServiceType::Post => write!(f, "post"),
33 }
34 }
35}
36
37impl FromStr for ServiceType {
38 type Err = BpError;
39 fn from_str(s: &str) -> BpResult<Self> {
40 match s.to_lowercase().as_str() {
41 "pouch" => Ok(ServiceType::Pouch),
42 "bill" => Ok(ServiceType::Bill),
43 "post" => Ok(ServiceType::Post),
44 other => Err(BpError::Service(format!(
45 "Unknown service type '{}'. Use: bill | pouch | post",
46 other
47 ))),
48 }
49 }
50}
51
52/// Runtime status of a local service instance.
53///
54/// Transitions: `Starting` → `Running` → `Stopping` → `Stopped`.
55/// Any state can transition to `Error` if an unrecoverable fault occurs.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "lowercase")]
58pub enum ServiceStatus {
59 /// Service is initialising (e.g. subscribing to gossip topics).
60 Starting,
61 /// Service is fully operational and announcing itself to the network.
62 Running,
63 /// Service is temporarily paused for maintenance.
64 ///
65 /// The service will resume within `eta_minutes`.
66 /// `paused_at` is a Unix timestamp (seconds).
67 Paused { eta_minutes: u64, paused_at: u64 },
68 /// Graceful shutdown has been requested but not yet completed.
69 Stopping,
70 /// Service has shut down cleanly.
71 Stopped,
72 /// Service encountered an unrecoverable error; payload is a description.
73 Error(String),
74}
75
76impl std::fmt::Display for ServiceStatus {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 ServiceStatus::Starting => write!(f, "starting"),
80 ServiceStatus::Running => write!(f, "running"),
81 ServiceStatus::Paused { eta_minutes, .. } => {
82 write!(f, "paused (ETA {} min)", eta_minutes)
83 }
84 ServiceStatus::Stopping => write!(f, "stopping"),
85 ServiceStatus::Stopped => write!(f, "stopped"),
86 ServiceStatus::Error(msg) => write!(f, "error: {}", msg),
87 }
88 }
89}
90
91/// Metadata for a *local* service instance (daemon-side).
92///
93/// Created by [`ServiceInfo::new`] when a `Hatch` request is dispatched.
94/// Stored in the [`ServiceRegistry`] for the lifetime of the running service.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ServiceInfo {
97 /// Unique ID for this service instance (UUID v4).
98 pub id: String,
99 /// Whether this node offers storage, file I/O, or pure relay.
100 pub service_type: ServiceType,
101 /// Identifier of the BillPouch network this service participates in.
102 pub network_id: String,
103 /// Current lifecycle state of the service.
104 pub status: ServiceStatus,
105 /// UTC timestamp when the service was started.
106 pub started_at: chrono::DateTime<chrono::Utc>,
107 /// Extensible metadata (e.g. `storage_bytes` for Pouch, `mount_path` for Bill).
108 pub metadata: HashMap<String, serde_json::Value>,
109}
110
111impl ServiceInfo {
112 /// Construct a new `ServiceInfo` in the [`ServiceStatus::Starting`] state.
113 ///
114 /// Generates a fresh UUID v4 `service_id` and records the current UTC time
115 /// as `started_at`.
116 pub fn new(
117 service_type: ServiceType,
118 network_id: String,
119 metadata: HashMap<String, serde_json::Value>,
120 ) -> Self {
121 Self {
122 id: Uuid::new_v4().to_string(),
123 service_type,
124 network_id,
125 status: ServiceStatus::Starting,
126 started_at: chrono::Utc::now(),
127 metadata,
128 }
129 }
130}
131
132/// In-process registry of all running service instances (held inside `DaemonState`).
133///
134/// Keyed by `service_id` (UUID v4 string). All mutations go through `DaemonState`'s
135/// `RwLock<ServiceRegistry>` so that concurrent control requests are safe.
136#[derive(Default)]
137pub struct ServiceRegistry {
138 services: HashMap<String, ServiceInfo>,
139}
140
141impl ServiceRegistry {
142 /// Create an empty registry.
143 pub fn new() -> Self {
144 Self::default()
145 }
146
147 /// Insert or replace a service entry. The entry's `id` is used as the key.
148 pub fn register(&mut self, info: ServiceInfo) {
149 self.services.insert(info.id.clone(), info);
150 }
151
152 /// Look up a service by its UUID. Returns `None` if not found.
153 pub fn get(&self, id: &str) -> Option<&ServiceInfo> {
154 self.services.get(id)
155 }
156
157 /// Look up a service mutably (e.g. to update its [`ServiceStatus`]).
158 pub fn get_mut(&mut self, id: &str) -> Option<&mut ServiceInfo> {
159 self.services.get_mut(id)
160 }
161
162 /// Remove and return a service by UUID. Returns `None` if not found.
163 pub fn remove(&mut self, id: &str) -> Option<ServiceInfo> {
164 self.services.remove(id)
165 }
166
167 /// Return references to all registered services (order is unspecified).
168 pub fn all(&self) -> Vec<&ServiceInfo> {
169 self.services.values().collect()
170 }
171}