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}