photoncloud-monorepo/chainfire/chainfire-client/src/node.rs
centra 8f94aee1fa Fix R8: Convert submodule gitlinks to regular directories
- Remove gitlinks (160000 mode) for chainfire, flaredb, iam
- Add workspace contents as regular tracked files
- Update flake.nix to use simple paths instead of builtins.fetchGit

This resolves the nix build failure where submodule directories
appeared empty in the nix store.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 16:51:20 +09:00

333 lines
9.4 KiB
Rust

//! Node metadata helpers for Chainfire KVS
//!
//! This module provides helpers for storing and retrieving node metadata
//! in the Chainfire distributed KVS.
//!
//! # KVS Key Schema
//!
//! Node metadata is stored with the following key structure:
//! - `/nodes/<id>/info` - JSON-encoded NodeMetadata
//! - `/nodes/<id>/roles` - JSON-encoded roles (raft_role, gossip_role)
//! - `/nodes/<id>/capacity/cpu` - CPU cores (u32)
//! - `/nodes/<id>/capacity/memory_gb` - Memory in GB (u32)
//! - `/nodes/<id>/labels/<key>` - Custom labels (string)
//! - `/nodes/<id>/api_addr` - API address (string)
use crate::error::Result;
use crate::Client;
use chainfire_types::node::NodeRole;
use chainfire_types::RaftRole;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Node metadata stored in KVS
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeMetadata {
/// Unique node identifier
pub id: u64,
/// Human-readable node name
pub name: String,
/// Raft participation role
pub raft_role: RaftRole,
/// Gossip/cluster role
pub gossip_role: NodeRole,
/// API address for client connections
pub api_addr: String,
/// Raft address for inter-node communication (optional for workers)
pub raft_addr: Option<String>,
/// Gossip address for membership protocol
pub gossip_addr: String,
/// Node capacity information
pub capacity: NodeCapacity,
/// Custom labels for node selection
pub labels: HashMap<String, String>,
}
/// Node capacity information
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NodeCapacity {
/// Number of CPU cores
pub cpu_cores: u32,
/// Memory in gigabytes
pub memory_gb: u32,
/// Disk space in gigabytes (optional)
pub disk_gb: Option<u32>,
}
/// Filter for listing nodes
#[derive(Debug, Clone, Default)]
pub struct NodeFilter {
/// Filter by Raft role
pub raft_role: Option<RaftRole>,
/// Filter by gossip role
pub gossip_role: Option<NodeRole>,
/// Filter by labels (all must match)
pub labels: HashMap<String, String>,
}
impl NodeMetadata {
/// Create a new NodeMetadata for a control-plane node
pub fn control_plane(
id: u64,
name: impl Into<String>,
api_addr: impl Into<String>,
raft_addr: impl Into<String>,
gossip_addr: impl Into<String>,
) -> Self {
Self {
id,
name: name.into(),
raft_role: RaftRole::Voter,
gossip_role: NodeRole::ControlPlane,
api_addr: api_addr.into(),
raft_addr: Some(raft_addr.into()),
gossip_addr: gossip_addr.into(),
capacity: NodeCapacity::default(),
labels: HashMap::new(),
}
}
/// Create a new NodeMetadata for a worker node
pub fn worker(
id: u64,
name: impl Into<String>,
api_addr: impl Into<String>,
gossip_addr: impl Into<String>,
) -> Self {
Self {
id,
name: name.into(),
raft_role: RaftRole::None,
gossip_role: NodeRole::Worker,
api_addr: api_addr.into(),
raft_addr: None,
gossip_addr: gossip_addr.into(),
capacity: NodeCapacity::default(),
labels: HashMap::new(),
}
}
/// Set capacity information
pub fn with_capacity(mut self, cpu_cores: u32, memory_gb: u32) -> Self {
self.capacity.cpu_cores = cpu_cores;
self.capacity.memory_gb = memory_gb;
self
}
/// Add a label
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
}
/// Key prefix for all node metadata
const NODE_PREFIX: &str = "/nodes/";
/// Generate the key for node info
fn node_info_key(id: u64) -> String {
format!("{}{}/info", NODE_PREFIX, id)
}
/// Generate the key for a node label
fn node_label_key(id: u64, label: &str) -> String {
format!("{}{}/labels/{}", NODE_PREFIX, id, label)
}
/// Register a node in the cluster by storing its metadata in KVS
///
/// # Arguments
///
/// * `client` - The Chainfire client
/// * `meta` - Node metadata to register
///
/// # Returns
///
/// The revision number of the write operation
pub async fn register_node(client: &mut Client, meta: &NodeMetadata) -> Result<u64> {
let key = node_info_key(meta.id);
let value = serde_json::to_string(meta)
.map_err(|e| crate::error::ClientError::Internal(e.to_string()))?;
client.put_str(&key, &value).await
}
/// Update a specific node attribute
pub async fn update_node_label(
client: &mut Client,
node_id: u64,
label: &str,
value: &str,
) -> Result<u64> {
let key = node_label_key(node_id, label);
client.put_str(&key, value).await
}
/// Get a node's metadata by ID
///
/// # Arguments
///
/// * `client` - The Chainfire client
/// * `node_id` - The node ID to look up
///
/// # Returns
///
/// The node metadata if found, None otherwise
pub async fn get_node(client: &mut Client, node_id: u64) -> Result<Option<NodeMetadata>> {
let key = node_info_key(node_id);
let value = client.get_str(&key).await?;
match value {
Some(json) => {
let meta: NodeMetadata = serde_json::from_str(&json)
.map_err(|e| crate::error::ClientError::Internal(e.to_string()))?;
Ok(Some(meta))
}
None => Ok(None),
}
}
/// List all registered nodes
///
/// # Arguments
///
/// * `client` - The Chainfire client
/// * `filter` - Optional filter criteria
///
/// # Returns
///
/// A list of node metadata matching the filter
pub async fn list_nodes(client: &mut Client, filter: &NodeFilter) -> Result<Vec<NodeMetadata>> {
let prefix = format!("{}", NODE_PREFIX);
let entries = client.get_prefix(&prefix).await?;
let mut nodes = Vec::new();
for (key, value) in entries {
let key_str = String::from_utf8_lossy(&key);
// Only process /nodes/<id>/info keys
if !key_str.ends_with("/info") {
continue;
}
let json = String::from_utf8_lossy(&value);
if let Ok(meta) = serde_json::from_str::<NodeMetadata>(&json) {
// Apply filters
if let Some(ref raft_role) = filter.raft_role {
if meta.raft_role != *raft_role {
continue;
}
}
if let Some(ref gossip_role) = filter.gossip_role {
if meta.gossip_role != *gossip_role {
continue;
}
}
// Check label filters
let mut labels_match = true;
for (k, v) in &filter.labels {
match meta.labels.get(k) {
Some(node_v) if node_v == v => {}
_ => {
labels_match = false;
break;
}
}
}
if labels_match {
nodes.push(meta);
}
}
}
// Sort by node ID for consistent ordering
nodes.sort_by_key(|n| n.id);
Ok(nodes)
}
/// Unregister a node from the cluster
///
/// # Arguments
///
/// * `client` - The Chainfire client
/// * `node_id` - The node ID to unregister
///
/// # Returns
///
/// True if the node was found and deleted
pub async fn unregister_node(client: &mut Client, node_id: u64) -> Result<bool> {
let key = node_info_key(node_id);
client.delete(&key).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_info_key() {
assert_eq!(node_info_key(1), "/nodes/1/info");
assert_eq!(node_info_key(123), "/nodes/123/info");
}
#[test]
fn test_node_label_key() {
assert_eq!(node_label_key(1, "zone"), "/nodes/1/labels/zone");
}
#[test]
fn test_control_plane_metadata() {
let meta = NodeMetadata::control_plane(
1,
"cp-1",
"127.0.0.1:2379",
"127.0.0.1:2380",
"127.0.0.1:2381",
);
assert_eq!(meta.id, 1);
assert_eq!(meta.raft_role, RaftRole::Voter);
assert_eq!(meta.gossip_role, NodeRole::ControlPlane);
assert!(meta.raft_addr.is_some());
}
#[test]
fn test_worker_metadata() {
let meta = NodeMetadata::worker(100, "worker-1", "127.0.0.1:3379", "127.0.0.1:3381");
assert_eq!(meta.id, 100);
assert_eq!(meta.raft_role, RaftRole::None);
assert_eq!(meta.gossip_role, NodeRole::Worker);
assert!(meta.raft_addr.is_none());
}
#[test]
fn test_metadata_with_capacity() {
let meta = NodeMetadata::worker(1, "worker", "addr", "gossip")
.with_capacity(8, 32)
.with_label("zone", "us-west-1");
assert_eq!(meta.capacity.cpu_cores, 8);
assert_eq!(meta.capacity.memory_gb, 32);
assert_eq!(meta.labels.get("zone"), Some(&"us-west-1".to_string()));
}
#[test]
fn test_metadata_serialization() {
let meta = NodeMetadata::control_plane(1, "test", "api", "raft", "gossip")
.with_capacity(4, 16)
.with_label("env", "prod");
let json = serde_json::to_string(&meta).unwrap();
let deserialized: NodeMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(meta.id, deserialized.id);
assert_eq!(meta.raft_role, deserialized.raft_role);
assert_eq!(meta.capacity.cpu_cores, deserialized.capacity.cpu_cores);
}
}