zephyrfs/zephyrfs-node / 9cc8ddf

Browse files

integrate coordinator client for distributed node management

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
9cc8ddf6fdf825ad2bd7e9768e4895201dffde2c
Parents
de09132
Tree
3b74901

14 changed files

StatusFile+-
M Cargo.toml 5 0
A Dockerfile.test 48 0
M build.rs 13 0
A config.test.yaml 25 0
A docker-compose.test.yml 90 0
M src/config.rs 1 1
A src/coordinator/client.rs 339 0
A src/coordinator/mod.rs 260 0
A src/coordinator/types.rs 215 0
M src/lib.rs 1 0
M src/main.rs 1 0
M src/node_manager.rs 227 25
M src/storage/encrypted_chunk_store.rs 2 2
M src/storage/storage_manager.rs 17 0
Cargo.tomlmodified
@@ -69,6 +69,11 @@ clap = { version = "4.5", features = ["derive"] }
6969
 futures = "0.3"
7070
 async-trait = "0.1"
7171
 
72
+# Coordinator integration
73
+uuid = { version = "1.6", features = ["v4"] }
74
+chrono = { version = "0.4", features = ["serde"] }
75
+hyper = "1.0"
76
+
7277
 [build-dependencies]
7378
 tonic-build = "0.12"
7479
 
Dockerfile.testadded
@@ -0,0 +1,48 @@
1
+# Test Dockerfile for ZephyrFS Node
2
+FROM rust:1.75-slim as builder
3
+
4
+# Install dependencies
5
+RUN apt-get update && apt-get install -y \
6
+    pkg-config \
7
+    libssl-dev \
8
+    protobuf-compiler \
9
+    && rm -rf /var/lib/apt/lists/*
10
+
11
+WORKDIR /app
12
+
13
+# Copy source code
14
+COPY . .
15
+
16
+# Build the application
17
+RUN cargo build --release
18
+
19
+# Runtime stage
20
+FROM debian:bookworm-slim
21
+
22
+# Install runtime dependencies
23
+RUN apt-get update && apt-get install -y \
24
+    ca-certificates \
25
+    wget \
26
+    && rm -rf /var/lib/apt/lists/*
27
+
28
+WORKDIR /app
29
+
30
+# Copy binary from builder
31
+COPY --from=builder /app/target/release/zephyrfs-node .
32
+
33
+# Create data directory
34
+RUN mkdir -p /data
35
+
36
+# Set environment variables
37
+ENV RUST_LOG=debug
38
+ENV RUST_BACKTRACE=1
39
+
40
+# Expose ports
41
+EXPOSE 4001 8080
42
+
43
+# Health check
44
+HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
45
+    CMD ./zephyrfs-node health-check || exit 1
46
+
47
+# Run the node
48
+CMD ["./zephyrfs-node", "start"]
build.rsmodified
@@ -1,4 +1,5 @@
11
 fn main() -> Result<(), Box<dyn std::error::Error>> {
2
+    // Generate protobuf code for node service (existing)
23
     tonic_build::configure()
34
         .build_server(true)
45
         .build_client(true)
@@ -6,5 +7,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
67
             &["../zephyrfs-proto/protobuff/node.proto"],
78
             &["../zephyrfs-proto/protobuff"],
89
         )?;
10
+
11
+    // Generate protobuf code for coordinator service (new)
12
+    tonic_build::configure()
13
+        .build_server(false) // We only need the client
14
+        .build_client(true)
15
+        .compile(
16
+            &["../zephyrfs-proto/protobuff/coordinator.proto"],
17
+            &["../zephyrfs-proto/protobuff"],
18
+        )?;
19
+
20
+    println!("cargo:rerun-if-changed=../zephyrfs-proto/protobuff/coordinator.proto");
21
+    println!("cargo:rerun-if-changed=../zephyrfs-proto/protobuff/node.proto");
922
     Ok(())
1023
 }
config.test.yamladded
@@ -0,0 +1,25 @@
1
+# Test configuration for ZephyrFS Node
2
+network:
3
+  p2p_port: 4001
4
+  api_port: 8080
5
+  enable_mdns: true
6
+  bootstrap_peers: []
7
+  max_peers: 50
8
+  enable_nat_traversal: false  # Disabled for Docker testing
9
+
10
+storage:
11
+  data_dir: "/data"
12
+  max_storage: 1073741824  # 1GB
13
+  chunk_size: 1048576      # 1MB
14
+  encrypt_at_rest: true
15
+
16
+coordinator:
17
+  url: "http://coordinator:8080"
18
+  timeout: 30
19
+  heartbeat_interval: 10
20
+
21
+security:
22
+  strict_tls: false        # Disabled for local testing
23
+  min_peer_reputation: 0.0
24
+  max_unknown_peers: 10
25
+  enable_rate_limiting: false
docker-compose.test.ymladded
@@ -0,0 +1,90 @@
1
+version: '3.8'
2
+
3
+services:
4
+  # ZephyrFS Coordinator
5
+  coordinator:
6
+    build:
7
+      context: ../zephyrfs-coordinator
8
+      dockerfile: Dockerfile
9
+    ports:
10
+      - "8080:8080"  # gRPC port
11
+      - "8090:8090"  # HTTP API port
12
+      - "8091:8091"  # Metrics port
13
+    environment:
14
+      - LOG_LEVEL=debug
15
+    volumes:
16
+      - coordinator_data:/data
17
+    networks:
18
+      - zephyr_network
19
+    healthcheck:
20
+      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8091/health"]
21
+      interval: 10s
22
+      timeout: 5s
23
+      retries: 3
24
+
25
+  # ZephyrFS Node 1
26
+  node1:
27
+    build:
28
+      context: .
29
+      dockerfile: Dockerfile.test
30
+    depends_on:
31
+      coordinator:
32
+        condition: service_healthy
33
+    environment:
34
+      - ZEPHYR_COORDINATOR_URL=http://coordinator:8080
35
+      - ZEPHYR_P2P_PORT=4001
36
+      - ZEPHYR_API_PORT=8081
37
+      - ZEPHYR_NODE_ID=node-1
38
+      - LOG_LEVEL=debug
39
+    volumes:
40
+      - node1_data:/data
41
+    networks:
42
+      - zephyr_network
43
+
44
+  # ZephyrFS Node 2
45
+  node2:
46
+    build:
47
+      context: .
48
+      dockerfile: Dockerfile.test
49
+    depends_on:
50
+      coordinator:
51
+        condition: service_healthy
52
+    environment:
53
+      - ZEPHYR_COORDINATOR_URL=http://coordinator:8080
54
+      - ZEPHYR_P2P_PORT=4002
55
+      - ZEPHYR_API_PORT=8082
56
+      - ZEPHYR_NODE_ID=node-2
57
+      - LOG_LEVEL=debug
58
+    volumes:
59
+      - node2_data:/data
60
+    networks:
61
+      - zephyr_network
62
+
63
+  # ZephyrFS Node 3
64
+  node3:
65
+    build:
66
+      context: .
67
+      dockerfile: Dockerfile.test
68
+    depends_on:
69
+      coordinator:
70
+        condition: service_healthy
71
+    environment:
72
+      - ZEPHYR_COORDINATOR_URL=http://coordinator:8080
73
+      - ZEPHYR_P2P_PORT=4003
74
+      - ZEPHYR_API_PORT=8083
75
+      - ZEPHYR_NODE_ID=node-3
76
+      - LOG_LEVEL=debug
77
+    volumes:
78
+      - node3_data:/data
79
+    networks:
80
+      - zephyr_network
81
+
82
+volumes:
83
+  coordinator_data:
84
+  node1_data:
85
+  node2_data:
86
+  node3_data:
87
+
88
+networks:
89
+  zephyr_network:
90
+    driver: bridge
src/config.rsmodified
@@ -132,7 +132,7 @@ impl Default for StorageConfig {
132132
 impl Default for CoordinatorConfig {
133133
     fn default() -> Self {
134134
         Self {
135
-            url: "http://localhost:9090".to_string(),
135
+            url: "http://localhost:8080".to_string(),
136136
             timeout: 30,
137137
             heartbeat_interval: 60,
138138
         }
src/coordinator/client.rsadded
@@ -0,0 +1,339 @@
1
+use anyhow::{Result, Context};
2
+use std::time::Duration;
3
+use tonic::transport::{Channel, Endpoint};
4
+use tonic::{Request, Response, Status};
5
+use tracing::{debug, warn};
6
+
7
+use super::types::*;
8
+
9
+/// Generated gRPC client code
10
+pub mod coordinator_service {
11
+    tonic::include_proto!("zephyrfs.coordinator");
12
+}
13
+
14
+use coordinator_service::{
15
+    coordinator_service_client::CoordinatorServiceClient,
16
+    RegisterNodeRequest as ProtoRegisterNodeRequest,
17
+    RegisterNodeResponse as ProtoRegisterNodeResponse,
18
+    UnregisterNodeRequest as ProtoUnregisterNodeRequest,
19
+    UnregisterNodeResponse as ProtoUnregisterNodeResponse,
20
+    GetActiveNodesRequest as ProtoGetActiveNodesRequest,
21
+    GetActiveNodesResponse as ProtoGetActiveNodesResponse,
22
+    NodeHeartbeatRequest as ProtoNodeHeartbeatRequest,
23
+    NodeHeartbeatResponse as ProtoNodeHeartbeatResponse,
24
+    RegisterFileRequest as ProtoRegisterFileRequest,
25
+    RegisterFileResponse as ProtoRegisterFileResponse,
26
+    GetFileInfoRequest as ProtoGetFileInfoRequest,
27
+    GetFileInfoResponse as ProtoGetFileInfoResponse,
28
+    UpdateChunkLocationsRequest as ProtoUpdateChunkLocationsRequest,
29
+    UpdateChunkLocationsResponse as ProtoUpdateChunkLocationsResponse,
30
+    FindChunkLocationsRequest as ProtoFindChunkLocationsRequest,
31
+    FindChunkLocationsResponse as ProtoFindChunkLocationsResponse,
32
+    GetNetworkStatusRequest as ProtoGetNetworkStatusRequest,
33
+    GetNetworkStatusResponse as ProtoGetNetworkStatusResponse,
34
+};
35
+
36
+/// Coordinator gRPC client
37
+#[derive(Clone)]
38
+pub struct CoordinatorClient {
39
+    client: CoordinatorServiceClient<Channel>,
40
+}
41
+
42
+impl CoordinatorClient {
43
+    /// Create a new coordinator client
44
+    pub async fn new(coordinator_url: &str) -> Result<Self> {
45
+        debug!("Connecting to coordinator at: {}", coordinator_url);
46
+
47
+        let endpoint = Endpoint::from_shared(coordinator_url.to_string())
48
+            .context("Invalid coordinator URL")?
49
+            .timeout(Duration::from_secs(10))
50
+            .connect_timeout(Duration::from_secs(5));
51
+
52
+        let channel = endpoint.connect().await
53
+            .context("Failed to connect to coordinator")?;
54
+
55
+        let client = CoordinatorServiceClient::new(channel);
56
+
57
+        debug!("Successfully connected to coordinator");
58
+        Ok(Self { client })
59
+    }
60
+
61
+    /// Register node with coordinator
62
+    pub async fn register_node(&self, request: RegisterNodeRequest) -> Result<RegisterNodeResponse> {
63
+        let proto_request = ProtoRegisterNodeRequest {
64
+            node_id: request.node_id,
65
+            addresses: request.addresses,
66
+            storage_capacity: request.storage_capacity,
67
+            capabilities: request.capabilities,
68
+        };
69
+
70
+        let response = self.client.clone()
71
+            .register_node(Request::new(proto_request))
72
+            .await
73
+            .context("gRPC call failed")?
74
+            .into_inner();
75
+
76
+        Ok(RegisterNodeResponse {
77
+            success: response.success,
78
+            message: response.message,
79
+            assigned_node_id: response.assigned_node_id,
80
+            bootstrap_peers: response.bootstrap_peers,
81
+        })
82
+    }
83
+
84
+    /// Unregister node from coordinator
85
+    pub async fn unregister_node(&self, request: UnregisterNodeRequest) -> Result<UnregisterNodeResponse> {
86
+        let proto_request = ProtoUnregisterNodeRequest {
87
+            node_id: request.node_id,
88
+            reason: request.reason,
89
+        };
90
+
91
+        let response = self.client.clone()
92
+            .unregister_node(Request::new(proto_request))
93
+            .await
94
+            .context("gRPC call failed")?
95
+            .into_inner();
96
+
97
+        Ok(UnregisterNodeResponse {
98
+            success: response.success,
99
+            message: response.message,
100
+        })
101
+    }
102
+
103
+    /// Get active nodes from coordinator
104
+    pub async fn get_active_nodes(&self, request: GetActiveNodesRequest) -> Result<GetActiveNodesResponse> {
105
+        let proto_request = ProtoGetActiveNodesRequest {
106
+            limit: request.limit,
107
+            exclude_nodes: request.exclude_nodes,
108
+        };
109
+
110
+        let response = self.client.clone()
111
+            .get_active_nodes(Request::new(proto_request))
112
+            .await
113
+            .context("gRPC call failed")?
114
+            .into_inner();
115
+
116
+        let nodes = response.nodes.into_iter()
117
+            .map(|node| NodeStatus {
118
+                node_id: node.node_id,
119
+                addresses: node.addresses,
120
+                stats: node.stats.map(|stats| NodeStats {
121
+                    storage_used: stats.storage_used,
122
+                    storage_available: stats.storage_available,
123
+                    chunks_stored: stats.chunks_stored,
124
+                    bandwidth_up: stats.bandwidth_up,
125
+                    bandwidth_down: stats.bandwidth_down,
126
+                    cpu_usage: stats.cpu_usage,
127
+                    memory_usage: stats.memory_usage,
128
+                    uptime_seconds: stats.uptime_seconds,
129
+                }),
130
+                last_heartbeat: node.last_heartbeat,
131
+                status: node.status,
132
+            })
133
+            .collect();
134
+
135
+        Ok(GetActiveNodesResponse {
136
+            nodes,
137
+            total_nodes: response.total_nodes,
138
+        })
139
+    }
140
+
141
+    /// Send heartbeat to coordinator
142
+    pub async fn node_heartbeat(&self, request: NodeHeartbeatRequest) -> Result<NodeHeartbeatResponse> {
143
+        let proto_stats = request.stats.map(|stats| coordinator_service::NodeStats {
144
+            storage_used: stats.storage_used,
145
+            storage_available: stats.storage_available,
146
+            chunks_stored: stats.chunks_stored,
147
+            bandwidth_up: stats.bandwidth_up,
148
+            bandwidth_down: stats.bandwidth_down,
149
+            cpu_usage: stats.cpu_usage,
150
+            memory_usage: stats.memory_usage,
151
+            uptime_seconds: stats.uptime_seconds,
152
+        });
153
+
154
+        let proto_request = ProtoNodeHeartbeatRequest {
155
+            node_id: request.node_id,
156
+            stats: proto_stats,
157
+        };
158
+
159
+        let response = self.client.clone()
160
+            .node_heartbeat(Request::new(proto_request))
161
+            .await
162
+            .context("gRPC call failed")?
163
+            .into_inner();
164
+
165
+        Ok(NodeHeartbeatResponse {
166
+            success: response.success,
167
+            message: response.message,
168
+            tasks: response.tasks,
169
+        })
170
+    }
171
+
172
+    /// Register file with coordinator
173
+    pub async fn register_file(&self, request: RegisterFileRequest) -> Result<RegisterFileResponse> {
174
+        let proto_chunks = request.chunks.into_iter()
175
+            .map(|chunk| coordinator_service::ChunkMetadata {
176
+                chunk_id: chunk.chunk_id,
177
+                hash: chunk.hash,
178
+                size: chunk.size,
179
+                index: chunk.index,
180
+            })
181
+            .collect();
182
+
183
+        let proto_request = ProtoRegisterFileRequest {
184
+            file_id: request.file_id,
185
+            file_name: request.file_name,
186
+            file_size: request.file_size,
187
+            file_hash: request.file_hash,
188
+            chunks: proto_chunks,
189
+            owner_node_id: request.owner_node_id,
190
+        };
191
+
192
+        let response = self.client.clone()
193
+            .register_file(Request::new(proto_request))
194
+            .await
195
+            .context("gRPC call failed")?
196
+            .into_inner();
197
+
198
+        let chunk_placements = response.chunk_placements.into_iter()
199
+            .map(|placement| ChunkPlacement {
200
+                chunk_id: placement.chunk_id,
201
+                target_nodes: placement.target_nodes,
202
+                replication_factor: placement.replication_factor,
203
+            })
204
+            .collect();
205
+
206
+        Ok(RegisterFileResponse {
207
+            success: response.success,
208
+            message: response.message,
209
+            chunk_placements,
210
+        })
211
+    }
212
+
213
+    /// Get file info from coordinator
214
+    pub async fn get_file_info(&self, request: GetFileInfoRequest) -> Result<GetFileInfoResponse> {
215
+        let proto_request = ProtoGetFileInfoRequest {
216
+            file_id: request.file_id,
217
+        };
218
+
219
+        let response = self.client.clone()
220
+            .get_file_info(Request::new(proto_request))
221
+            .await
222
+            .context("gRPC call failed")?
223
+            .into_inner();
224
+
225
+        let file_info = response.file_info.map(|info| FileRecord {
226
+            file_id: info.file_id,
227
+            file_name: info.file_name,
228
+            file_size: info.file_size,
229
+            file_hash: info.file_hash,
230
+            chunks: info.chunks.into_iter()
231
+                .map(|chunk| ChunkRecord {
232
+                    chunk_id: chunk.chunk_id,
233
+                    hash: chunk.hash,
234
+                    size: chunk.size,
235
+                    index: chunk.index,
236
+                    stored_at_nodes: chunk.stored_at_nodes,
237
+                    replication_count: chunk.replication_count,
238
+                })
239
+                .collect(),
240
+            owner_node_id: info.owner_node_id,
241
+            created_at: info.created_at,
242
+            last_accessed: info.last_accessed,
243
+        });
244
+
245
+        Ok(GetFileInfoResponse {
246
+            success: response.success,
247
+            message: response.message,
248
+            file_info,
249
+        })
250
+    }
251
+
252
+    /// Update chunk locations
253
+    pub async fn update_chunk_locations(&self, request: UpdateChunkLocationsRequest) -> Result<UpdateChunkLocationsResponse> {
254
+        let proto_request = ProtoUpdateChunkLocationsRequest {
255
+            chunk_id: request.chunk_id,
256
+            node_ids: request.node_ids,
257
+            operation: request.operation,
258
+        };
259
+
260
+        let response = self.client.clone()
261
+            .update_chunk_locations(Request::new(proto_request))
262
+            .await
263
+            .context("gRPC call failed")?
264
+            .into_inner();
265
+
266
+        Ok(UpdateChunkLocationsResponse {
267
+            success: response.success,
268
+            message: response.message,
269
+        })
270
+    }
271
+
272
+    /// Find chunk locations
273
+    pub async fn find_chunk_locations(&self, request: FindChunkLocationsRequest) -> Result<FindChunkLocationsResponse> {
274
+        let proto_request = ProtoFindChunkLocationsRequest {
275
+            chunk_id: request.chunk_id,
276
+            preferred_count: request.preferred_count,
277
+        };
278
+
279
+        let response = self.client.clone()
280
+            .find_chunk_locations(Request::new(proto_request))
281
+            .await
282
+            .context("gRPC call failed")?
283
+            .into_inner();
284
+
285
+        Ok(FindChunkLocationsResponse {
286
+            success: response.success,
287
+            message: response.message,
288
+            node_ids: response.node_ids,
289
+            node_addresses: response.node_addresses,
290
+        })
291
+    }
292
+
293
+    /// Get network status
294
+    pub async fn get_network_status(&self, request: GetNetworkStatusRequest) -> Result<GetNetworkStatusResponse> {
295
+        let proto_request = ProtoGetNetworkStatusRequest {};
296
+
297
+        let response = self.client.clone()
298
+            .get_network_status(Request::new(proto_request))
299
+            .await
300
+            .context("gRPC call failed")?
301
+            .into_inner();
302
+
303
+        let network_stats = response.network_stats.map(|stats| NetworkStats {
304
+            total_nodes: stats.total_nodes,
305
+            active_nodes: stats.active_nodes,
306
+            total_storage_capacity: stats.total_storage_capacity,
307
+            total_storage_used: stats.total_storage_used,
308
+            total_files: stats.total_files,
309
+            total_chunks: stats.total_chunks,
310
+            average_node_uptime: stats.average_node_uptime,
311
+            network_uptime_seconds: stats.network_uptime_seconds,
312
+        });
313
+
314
+        let active_nodes = response.active_nodes.into_iter()
315
+            .map(|node| NodeStatus {
316
+                node_id: node.node_id,
317
+                addresses: node.addresses,
318
+                stats: node.stats.map(|stats| NodeStats {
319
+                    storage_used: stats.storage_used,
320
+                    storage_available: stats.storage_available,
321
+                    chunks_stored: stats.chunks_stored,
322
+                    bandwidth_up: stats.bandwidth_up,
323
+                    bandwidth_down: stats.bandwidth_down,
324
+                    cpu_usage: stats.cpu_usage,
325
+                    memory_usage: stats.memory_usage,
326
+                    uptime_seconds: stats.uptime_seconds,
327
+                }),
328
+                last_heartbeat: node.last_heartbeat,
329
+                status: node.status,
330
+            })
331
+            .collect();
332
+
333
+        Ok(GetNetworkStatusResponse {
334
+            network_stats,
335
+            active_nodes,
336
+            timestamp: response.timestamp,
337
+        })
338
+    }
339
+}
src/coordinator/mod.rsadded
@@ -0,0 +1,260 @@
1
+use anyhow::{Result, Context};
2
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
3
+use tokio::time::{interval, sleep};
4
+use tracing::{debug, info, warn, error};
5
+use uuid::Uuid;
6
+
7
+pub mod client;
8
+pub mod types;
9
+
10
+pub use client::CoordinatorClient;
11
+pub use types::*;
12
+
13
+/// Coordinator integration for node registration and coordination
14
+pub struct CoordinatorManager {
15
+    client: CoordinatorClient,
16
+    node_id: String,
17
+    coordinator_url: String,
18
+    heartbeat_interval: Duration,
19
+    registration_status: RegistrationStatus,
20
+}
21
+
22
+#[derive(Debug, Clone)]
23
+pub enum RegistrationStatus {
24
+    NotRegistered,
25
+    Registering,
26
+    Registered,
27
+    Failed(String),
28
+}
29
+
30
+impl CoordinatorManager {
31
+    /// Create a new coordinator manager
32
+    pub async fn new(coordinator_url: String) -> Result<Self> {
33
+        let client = CoordinatorClient::new(&coordinator_url).await
34
+            .context("Failed to create coordinator client")?;
35
+
36
+        let node_id = Uuid::new_v4().to_string();
37
+        let heartbeat_interval = Duration::from_secs(10);
38
+
39
+        Ok(Self {
40
+            client,
41
+            node_id,
42
+            coordinator_url,
43
+            heartbeat_interval,
44
+            registration_status: RegistrationStatus::NotRegistered,
45
+        })
46
+    }
47
+
48
+    /// Register this node with the coordinator
49
+    pub async fn register_node(
50
+        &mut self,
51
+        addresses: Vec<String>,
52
+        storage_capacity: u64,
53
+        capabilities: std::collections::HashMap<String, String>,
54
+    ) -> Result<RegisterNodeResponse> {
55
+        info!("Registering node {} with coordinator at {}", self.node_id, self.coordinator_url);
56
+        self.registration_status = RegistrationStatus::Registering;
57
+
58
+        let request = RegisterNodeRequest {
59
+            node_id: self.node_id.clone(),
60
+            addresses,
61
+            storage_capacity: storage_capacity as i64,
62
+            capabilities,
63
+        };
64
+
65
+        match self.client.register_node(request).await {
66
+            Ok(response) => {
67
+                if response.success {
68
+                    self.registration_status = RegistrationStatus::Registered;
69
+                    info!("Successfully registered with coordinator. Assigned ID: {}", response.assigned_node_id);
70
+
71
+                    // Update node ID if coordinator assigned a different one
72
+                    if !response.assigned_node_id.is_empty() {
73
+                        self.node_id = response.assigned_node_id.clone();
74
+                    }
75
+
76
+                    let success_response = RegisterNodeResponse {
77
+                        success: response.success,
78
+                        message: response.message,
79
+                        assigned_node_id: response.assigned_node_id,
80
+                        bootstrap_peers: response.bootstrap_peers,
81
+                    };
82
+                    Ok(success_response)
83
+                } else {
84
+                    let error_msg = format!("Registration failed: {}", response.message);
85
+                    self.registration_status = RegistrationStatus::Failed(error_msg.clone());
86
+                    warn!("{}", error_msg);
87
+                    Ok(response)
88
+                }
89
+            }
90
+            Err(e) => {
91
+                let error_msg = format!("Failed to register with coordinator: {}", e);
92
+                self.registration_status = RegistrationStatus::Failed(error_msg.clone());
93
+                error!("{}", error_msg);
94
+                Err(e)
95
+            }
96
+        }
97
+    }
98
+
99
+    /// Start heartbeat loop
100
+    pub async fn start_heartbeat(&self, stats_provider: impl Fn() -> NodeStats + Send + 'static) {
101
+        let client = self.client.clone();
102
+        let node_id = self.node_id.clone();
103
+        let heartbeat_interval = self.heartbeat_interval;
104
+
105
+        tokio::spawn(async move {
106
+            let mut interval = interval(heartbeat_interval);
107
+
108
+            loop {
109
+                interval.tick().await;
110
+
111
+                let stats = stats_provider();
112
+                let request = NodeHeartbeatRequest {
113
+                    node_id: node_id.clone(),
114
+                    stats: Some(stats),
115
+                };
116
+
117
+                match client.node_heartbeat(request).await {
118
+                    Ok(response) => {
119
+                        if response.success {
120
+                            debug!("Heartbeat sent successfully");
121
+                            if !response.tasks.is_empty() {
122
+                                debug!("Coordinator assigned {} tasks", response.tasks.len());
123
+                                // TODO: Handle assigned tasks
124
+                            }
125
+                        } else {
126
+                            warn!("Heartbeat failed: {}", response.message);
127
+                        }
128
+                    }
129
+                    Err(e) => {
130
+                        warn!("Failed to send heartbeat: {}", e);
131
+                        // TODO: Implement exponential backoff
132
+                        sleep(Duration::from_secs(5)).await;
133
+                    }
134
+                }
135
+            }
136
+        });
137
+    }
138
+
139
+    /// Register a file with the coordinator
140
+    pub async fn register_file(
141
+        &self,
142
+        file_id: String,
143
+        file_name: String,
144
+        file_size: u64,
145
+        file_hash: String,
146
+        chunks: Vec<ChunkMetadata>,
147
+    ) -> Result<RegisterFileResponse> {
148
+        debug!("Registering file {} with coordinator", file_id);
149
+
150
+        let request = RegisterFileRequest {
151
+            file_id,
152
+            file_name,
153
+            file_size: file_size as i64,
154
+            file_hash,
155
+            chunks,
156
+            owner_node_id: self.node_id.clone(),
157
+        };
158
+
159
+        self.client.register_file(request).await
160
+            .context("Failed to register file with coordinator")
161
+    }
162
+
163
+    /// Find chunk locations from coordinator
164
+    pub async fn find_chunk_locations(&self, chunk_id: String, preferred_count: i32) -> Result<FindChunkLocationsResponse> {
165
+        debug!("Finding locations for chunk {} from coordinator", chunk_id);
166
+
167
+        let request = FindChunkLocationsRequest {
168
+            chunk_id,
169
+            preferred_count,
170
+        };
171
+
172
+        self.client.find_chunk_locations(request).await
173
+            .context("Failed to find chunk locations from coordinator")
174
+    }
175
+
176
+    /// Get active nodes from coordinator
177
+    pub async fn get_active_nodes(&self, limit: Option<i32>, exclude_nodes: Vec<String>) -> Result<GetActiveNodesResponse> {
178
+        debug!("Getting active nodes from coordinator");
179
+
180
+        let request = GetActiveNodesRequest {
181
+            limit: limit.unwrap_or(10),
182
+            exclude_nodes,
183
+        };
184
+
185
+        self.client.get_active_nodes(request).await
186
+            .context("Failed to get active nodes from coordinator")
187
+    }
188
+
189
+    /// Get network status from coordinator
190
+    pub async fn get_network_status(&self) -> Result<GetNetworkStatusResponse> {
191
+        debug!("Getting network status from coordinator");
192
+
193
+        let request = GetNetworkStatusRequest {};
194
+
195
+        self.client.get_network_status(request).await
196
+            .context("Failed to get network status from coordinator")
197
+    }
198
+
199
+    /// Update chunk locations
200
+    pub async fn update_chunk_locations(
201
+        &self,
202
+        chunk_id: String,
203
+        node_ids: Vec<String>,
204
+        operation: String,
205
+    ) -> Result<UpdateChunkLocationsResponse> {
206
+        debug!("Updating chunk locations for {} (operation: {})", chunk_id, operation);
207
+
208
+        let request = UpdateChunkLocationsRequest {
209
+            chunk_id,
210
+            node_ids,
211
+            operation,
212
+        };
213
+
214
+        self.client.update_chunk_locations(request).await
215
+            .context("Failed to update chunk locations")
216
+    }
217
+
218
+    /// Unregister this node from coordinator
219
+    pub async fn unregister_node(&mut self, reason: Option<String>) -> Result<UnregisterNodeResponse> {
220
+        info!("Unregistering node {} from coordinator", self.node_id);
221
+
222
+        let request = UnregisterNodeRequest {
223
+            node_id: self.node_id.clone(),
224
+            reason: reason.unwrap_or_else(|| "Normal shutdown".to_string()),
225
+        };
226
+
227
+        match self.client.unregister_node(request).await {
228
+            Ok(response) => {
229
+                if response.success {
230
+                    self.registration_status = RegistrationStatus::NotRegistered;
231
+                    info!("Successfully unregistered from coordinator");
232
+                } else {
233
+                    warn!("Unregistration failed: {}", response.message);
234
+                }
235
+                Ok(response)
236
+            }
237
+            Err(e) => {
238
+                error!("Failed to unregister from coordinator: {}", e);
239
+                Err(e)
240
+            }
241
+        }
242
+    }
243
+
244
+    /// Get current registration status
245
+    pub fn get_registration_status(&self) -> &RegistrationStatus {
246
+        &self.registration_status
247
+    }
248
+
249
+    /// Get node ID
250
+    pub fn get_node_id(&self) -> &str {
251
+        &self.node_id
252
+    }
253
+}
254
+
255
+/// Convert system time to Unix timestamp
256
+pub fn system_time_to_unix_timestamp(time: SystemTime) -> i64 {
257
+    time.duration_since(UNIX_EPOCH)
258
+        .unwrap_or_default()
259
+        .as_secs() as i64
260
+}
src/coordinator/types.rsadded
@@ -0,0 +1,215 @@
1
+use std::collections::HashMap;
2
+
3
+/// Request to register a node with the coordinator
4
+#[derive(Debug, Clone)]
5
+pub struct RegisterNodeRequest {
6
+    pub node_id: String,
7
+    pub addresses: Vec<String>,
8
+    pub storage_capacity: i64,
9
+    pub capabilities: HashMap<String, String>,
10
+}
11
+
12
+/// Response from node registration
13
+#[derive(Debug, Clone)]
14
+pub struct RegisterNodeResponse {
15
+    pub success: bool,
16
+    pub message: String,
17
+    pub assigned_node_id: String,
18
+    pub bootstrap_peers: Vec<String>,
19
+}
20
+
21
+/// Request to unregister a node
22
+#[derive(Debug, Clone)]
23
+pub struct UnregisterNodeRequest {
24
+    pub node_id: String,
25
+    pub reason: String,
26
+}
27
+
28
+/// Response from node unregistration
29
+#[derive(Debug, Clone)]
30
+pub struct UnregisterNodeResponse {
31
+    pub success: bool,
32
+    pub message: String,
33
+}
34
+
35
+/// Request to get active nodes
36
+#[derive(Debug, Clone)]
37
+pub struct GetActiveNodesRequest {
38
+    pub limit: i32,
39
+    pub exclude_nodes: Vec<String>,
40
+}
41
+
42
+/// Response with active nodes
43
+#[derive(Debug, Clone)]
44
+pub struct GetActiveNodesResponse {
45
+    pub nodes: Vec<NodeStatus>,
46
+    pub total_nodes: i32,
47
+}
48
+
49
+/// Node heartbeat request
50
+#[derive(Debug, Clone)]
51
+pub struct NodeHeartbeatRequest {
52
+    pub node_id: String,
53
+    pub stats: Option<NodeStats>,
54
+}
55
+
56
+/// Node heartbeat response
57
+#[derive(Debug, Clone)]
58
+pub struct NodeHeartbeatResponse {
59
+    pub success: bool,
60
+    pub message: String,
61
+    pub tasks: Vec<String>,
62
+}
63
+
64
+/// Request to register a file
65
+#[derive(Debug, Clone)]
66
+pub struct RegisterFileRequest {
67
+    pub file_id: String,
68
+    pub file_name: String,
69
+    pub file_size: i64,
70
+    pub file_hash: String,
71
+    pub chunks: Vec<ChunkMetadata>,
72
+    pub owner_node_id: String,
73
+}
74
+
75
+/// Response from file registration
76
+#[derive(Debug, Clone)]
77
+pub struct RegisterFileResponse {
78
+    pub success: bool,
79
+    pub message: String,
80
+    pub chunk_placements: Vec<ChunkPlacement>,
81
+}
82
+
83
+/// Request to get file information
84
+#[derive(Debug, Clone)]
85
+pub struct GetFileInfoRequest {
86
+    pub file_id: String,
87
+}
88
+
89
+/// Response with file information
90
+#[derive(Debug, Clone)]
91
+pub struct GetFileInfoResponse {
92
+    pub success: bool,
93
+    pub message: String,
94
+    pub file_info: Option<FileRecord>,
95
+}
96
+
97
+/// Request to update chunk locations
98
+#[derive(Debug, Clone)]
99
+pub struct UpdateChunkLocationsRequest {
100
+    pub chunk_id: String,
101
+    pub node_ids: Vec<String>,
102
+    pub operation: String, // "add" or "remove"
103
+}
104
+
105
+/// Response from chunk location update
106
+#[derive(Debug, Clone)]
107
+pub struct UpdateChunkLocationsResponse {
108
+    pub success: bool,
109
+    pub message: String,
110
+}
111
+
112
+/// Request to find chunk locations
113
+#[derive(Debug, Clone)]
114
+pub struct FindChunkLocationsRequest {
115
+    pub chunk_id: String,
116
+    pub preferred_count: i32,
117
+}
118
+
119
+/// Response with chunk locations
120
+#[derive(Debug, Clone)]
121
+pub struct FindChunkLocationsResponse {
122
+    pub success: bool,
123
+    pub message: String,
124
+    pub node_ids: Vec<String>,
125
+    pub node_addresses: Vec<String>,
126
+}
127
+
128
+/// Request to get network status
129
+#[derive(Debug, Clone)]
130
+pub struct GetNetworkStatusRequest {}
131
+
132
+/// Response with network status
133
+#[derive(Debug, Clone)]
134
+pub struct GetNetworkStatusResponse {
135
+    pub network_stats: Option<NetworkStats>,
136
+    pub active_nodes: Vec<NodeStatus>,
137
+    pub timestamp: i64,
138
+}
139
+
140
+/// Node status information
141
+#[derive(Debug, Clone)]
142
+pub struct NodeStatus {
143
+    pub node_id: String,
144
+    pub addresses: Vec<String>,
145
+    pub stats: Option<NodeStats>,
146
+    pub last_heartbeat: i64,
147
+    pub status: String, // "active", "inactive", "maintenance"
148
+}
149
+
150
+/// Node statistics
151
+#[derive(Debug, Clone)]
152
+pub struct NodeStats {
153
+    pub storage_used: i64,
154
+    pub storage_available: i64,
155
+    pub chunks_stored: i64,
156
+    pub bandwidth_up: i64,
157
+    pub bandwidth_down: i64,
158
+    pub cpu_usage: f64,
159
+    pub memory_usage: f64,
160
+    pub uptime_seconds: i64,
161
+}
162
+
163
+/// Chunk metadata
164
+#[derive(Debug, Clone)]
165
+pub struct ChunkMetadata {
166
+    pub chunk_id: String,
167
+    pub hash: String,
168
+    pub size: i64,
169
+    pub index: i32,
170
+}
171
+
172
+/// Chunk placement information
173
+#[derive(Debug, Clone)]
174
+pub struct ChunkPlacement {
175
+    pub chunk_id: String,
176
+    pub target_nodes: Vec<String>,
177
+    pub replication_factor: i32,
178
+}
179
+
180
+/// File record information
181
+#[derive(Debug, Clone)]
182
+pub struct FileRecord {
183
+    pub file_id: String,
184
+    pub file_name: String,
185
+    pub file_size: i64,
186
+    pub file_hash: String,
187
+    pub chunks: Vec<ChunkRecord>,
188
+    pub owner_node_id: String,
189
+    pub created_at: i64,
190
+    pub last_accessed: i64,
191
+}
192
+
193
+/// Chunk record with location information
194
+#[derive(Debug, Clone)]
195
+pub struct ChunkRecord {
196
+    pub chunk_id: String,
197
+    pub hash: String,
198
+    pub size: i64,
199
+    pub index: i32,
200
+    pub stored_at_nodes: Vec<String>,
201
+    pub replication_count: i32,
202
+}
203
+
204
+/// Network statistics
205
+#[derive(Debug, Clone)]
206
+pub struct NetworkStats {
207
+    pub total_nodes: i32,
208
+    pub active_nodes: i32,
209
+    pub total_storage_capacity: i64,
210
+    pub total_storage_used: i64,
211
+    pub total_files: i64,
212
+    pub total_chunks: i64,
213
+    pub average_node_uptime: f64,
214
+    pub network_uptime_seconds: i64,
215
+}
src/lib.rsmodified
@@ -9,6 +9,7 @@ pub mod storage;
99
 pub mod protocol;
1010
 pub mod node_manager;
1111
 pub mod crypto;
12
+pub mod coordinator;
1213
 
1314
 pub use crypto::{
1415
     ZephyrCrypto, CryptoParams, ScryptParams, AesParams, HashParams,
src/main.rsmodified
@@ -9,6 +9,7 @@ mod storage;
99
 mod protocol;
1010
 mod node_manager;
1111
 mod crypto;
12
+mod coordinator;
1213
 
1314
 #[cfg(test)]
1415
 mod integration_tests;
src/node_manager.rsmodified
@@ -7,31 +7,35 @@ use tracing::{debug, info, warn, error};
77
 use crate::config::Config;
88
 use crate::network::{NetworkManager, message_handler::{ZephyrMessage, NodeInfo}};
99
 use crate::storage::{StorageManager, StorageConfig as StorageManagerConfig};
10
+use crate::coordinator::{CoordinatorManager, RegistrationStatus};
1011
 
1112
 /// Integrated node manager coordinating networking and storage
12
-/// 
13
+///
1314
 /// Safety: Coordinates secure operations between network and storage layers
1415
 /// Transparency: Provides comprehensive node status and metrics
1516
 /// Privacy: Handles secure chunk distribution and encrypted metadata
1617
 pub struct NodeManager {
1718
     /// Network layer manager
1819
     network_manager: NetworkManager,
19
-    
20
+
2021
     /// Storage layer manager
2122
     pub storage_manager: Arc<StorageManager>,
22
-    
23
+
24
+    /// Coordinator manager for network coordination
25
+    coordinator_manager: Option<CoordinatorManager>,
26
+
2327
     /// Configuration
2428
     config: Config,
25
-    
29
+
2630
     /// Message channel from network to node manager
2731
     message_rx: mpsc::Receiver<ZephyrMessage>,
28
-    
32
+
2933
     /// Message channel from node manager to network
3034
     message_tx: mpsc::Sender<ZephyrMessage>,
31
-    
35
+
3236
     /// Node statistics
3337
     node_stats: Arc<RwLock<NodeStats>>,
34
-    
38
+
3539
     /// Base storage path
3640
     storage_path: PathBuf,
3741
 }
@@ -81,14 +85,14 @@ pub enum DistributionStrategy {
8185
 
8286
 impl NodeManager {
8387
     /// Create a new integrated node manager
84
-    /// 
88
+    ///
8589
     /// Safety: Initializes both network and storage with secure configurations
8690
     pub async fn new(config: Config, storage_path: PathBuf) -> Result<Self> {
8791
         info!("Initializing NodeManager with integrated network and storage");
88
-        
92
+
8993
         // Create message channel for network-storage communication
9094
         let (message_tx, message_rx) = mpsc::channel::<ZephyrMessage>(1000);
91
-        
95
+
9296
         // Initialize storage manager
9397
         let storage_config = StorageManagerConfig {
9498
             max_capacity: config.storage.max_storage,
@@ -99,16 +103,33 @@ impl NodeManager {
99103
             enable_gc: true,
100104
             gc_interval: 3600, // 1 hour
101105
         };
102
-        
106
+
103107
         let storage_manager = Arc::new(
104108
             StorageManager::new(&storage_path, storage_config).await
105109
                 .context("Failed to initialize storage manager")?
106110
         );
107
-        
111
+
108112
         // Initialize network manager with message channel
109113
         let network_manager = NetworkManager::new(config.clone()).await
110114
             .context("Failed to initialize network manager")?;
111
-        
115
+
116
+        // Initialize coordinator manager if URL is provided
117
+        let coordinator_manager = if !config.coordinator.url.is_empty() {
118
+            match CoordinatorManager::new(config.coordinator.url.clone()).await {
119
+                Ok(manager) => {
120
+                    info!("Successfully connected to coordinator at {}", config.coordinator.url);
121
+                    Some(manager)
122
+                }
123
+                Err(e) => {
124
+                    warn!("Failed to connect to coordinator at {}: {}. Running in standalone mode.", config.coordinator.url, e);
125
+                    None
126
+                }
127
+            }
128
+        } else {
129
+            info!("No coordinator URL configured. Running in standalone mode.");
130
+            None
131
+        };
132
+
112133
         let node_stats = Arc::new(RwLock::new(NodeStats {
113134
             chunks_served: 0,
114135
             chunks_retrieved: 0,
@@ -119,10 +140,11 @@ impl NodeManager {
119140
             uptime_seconds: 0,
120141
             start_time: std::time::Instant::now(),
121142
         }));
122
-        
143
+
123144
         Ok(Self {
124145
             network_manager,
125146
             storage_manager,
147
+            coordinator_manager,
126148
             config,
127149
             message_rx,
128150
             message_tx,
@@ -132,21 +154,55 @@ impl NodeManager {
132154
     }
133155
     
134156
     /// Start the integrated node
135
-    /// 
157
+    ///
136158
     /// Safety: Starts both network and storage services with proper error handling
137159
     pub async fn start(&mut self) -> Result<()> {
138160
         info!("Starting integrated ZephyrFS node");
139
-        
161
+
140162
         // Start storage manager background tasks (if any)
141163
         self.start_storage_tasks().await?;
142
-        
164
+
143165
         // Start network manager
144166
         self.network_manager.start().await
145167
             .context("Failed to start network manager")?;
146
-        
168
+
169
+        // Register with coordinator if available
170
+        if self.coordinator_manager.is_some() {
171
+            let node_status = self.get_node_status().await;
172
+            let addresses = vec![
173
+                format!("{}:{}", "127.0.0.1", self.config.network.p2p_port),
174
+                format!("{}:{}", "127.0.0.1", self.config.network.api_port),
175
+            ];
176
+
177
+            let mut capabilities = std::collections::HashMap::new();
178
+            capabilities.insert("version".to_string(), node_status.version);
179
+            capabilities.insert("storage".to_string(), "true".to_string());
180
+            capabilities.insert("encryption".to_string(), "true".to_string());
181
+
182
+            if let Some(coordinator) = self.coordinator_manager.as_mut() {
183
+                let response = coordinator.register_node(
184
+                    addresses,
185
+                    node_status.storage_capacity,
186
+                    capabilities,
187
+                ).await?;
188
+
189
+                if response.success {
190
+                    info!("Successfully registered with coordinator. Node ID: {}", coordinator.get_node_id());
191
+                    if !response.bootstrap_peers.is_empty() {
192
+                        info!("Received {} bootstrap peers from coordinator", response.bootstrap_peers.len());
193
+                        // TODO: Connect to bootstrap peers
194
+                    }
195
+                } else {
196
+                    warn!("Failed to register with coordinator: {}", response.message);
197
+                }
198
+            }
199
+
200
+            self.start_coordinator_heartbeat().await;
201
+        }
202
+
147203
         // Start message processing loop
148204
         self.start_message_processing().await;
149
-        
205
+
150206
         info!("ZephyrFS node started successfully");
151207
         Ok(())
152208
     }
@@ -187,11 +243,16 @@ impl NodeManager {
187243
             }
188244
         }
189245
         
246
+        // Register file with coordinator if available
247
+        if let Err(e) = self.register_file_with_coordinator(file_id, &file_hash, data.len() as u64, filename).await {
248
+            warn!("Failed to register file with coordinator: {}", e);
249
+        }
250
+
190251
         // Announce file availability to peers
191252
         if let Err(e) = self.announce_file_to_peers(file_id, &file_hash).await {
192253
             warn!("Failed to announce file to peers: {}", e);
193254
         }
194
-        
255
+
195256
         info!("Successfully stored and distributed file: {} with hash: {}", file_id, file_hash);
196257
         Ok(file_hash)
197258
     }
@@ -268,8 +329,14 @@ impl NodeManager {
268329
         // Calculate uptime
269330
         let uptime_seconds = stats.start_time.elapsed().as_secs();
270331
         
332
+        let node_id = if let Some(coordinator) = &self.coordinator_manager {
333
+            coordinator.get_node_id().to_string()
334
+        } else {
335
+            self.config.node_id.clone().unwrap_or_else(|| "local_node".to_string())
336
+        };
337
+
271338
         NodeStatus {
272
-            node_id: "local_node".to_string(), // TODO: Generate proper node ID
339
+            node_id,
273340
             version: env!("CARGO_PKG_VERSION").to_string(),
274341
             uptime_seconds,
275342
             peer_connections: stats.peer_connections,
@@ -287,18 +354,25 @@ impl NodeManager {
287354
     }
288355
     
289356
     /// Shutdown the node gracefully
290
-    /// 
357
+    ///
291358
     /// Safety: Ensures clean shutdown of both network and storage
292359
     pub async fn shutdown(&mut self) -> Result<()> {
293360
         info!("Shutting down ZephyrFS node");
294
-        
361
+
362
+        // Unregister from coordinator if connected
363
+        if let Some(coordinator) = &mut self.coordinator_manager {
364
+            if let Err(e) = coordinator.unregister_node(Some("Normal shutdown".to_string())).await {
365
+                warn!("Failed to unregister from coordinator: {}", e);
366
+            }
367
+        }
368
+
295369
         // Shutdown network manager
296370
         self.network_manager.shutdown().await
297371
             .context("Failed to shutdown network manager")?;
298
-        
372
+
299373
         // Storage manager cleanup (if needed)
300374
         // Currently storage manager doesn't need explicit cleanup
301
-        
375
+
302376
         info!("ZephyrFS node shutdown complete");
303377
         Ok(())
304378
     }
@@ -411,6 +485,134 @@ impl NodeManager {
411485
         
412486
         Ok(())
413487
     }
488
+
489
+    /// Register this node with the coordinator
490
+    async fn register_with_coordinator(&self, coordinator: &mut CoordinatorManager) -> Result<()> {
491
+        info!("Registering node with coordinator");
492
+
493
+        let node_status = self.get_node_status().await;
494
+        let addresses = vec![
495
+            format!("{}:{}", "127.0.0.1", self.config.network.p2p_port),
496
+            format!("{}:{}", "127.0.0.1", self.config.network.api_port),
497
+        ];
498
+
499
+        let mut capabilities = std::collections::HashMap::new();
500
+        capabilities.insert("version".to_string(), node_status.version);
501
+        capabilities.insert("storage".to_string(), "true".to_string());
502
+        capabilities.insert("encryption".to_string(), "true".to_string());
503
+
504
+        let response = coordinator.register_node(
505
+            addresses,
506
+            node_status.storage_capacity,
507
+            capabilities,
508
+        ).await?;
509
+
510
+        if response.success {
511
+            info!("Successfully registered with coordinator. Node ID: {}", coordinator.get_node_id());
512
+            if !response.bootstrap_peers.is_empty() {
513
+                info!("Received {} bootstrap peers from coordinator", response.bootstrap_peers.len());
514
+                // TODO: Connect to bootstrap peers
515
+            }
516
+        } else {
517
+            warn!("Failed to register with coordinator: {}", response.message);
518
+        }
519
+
520
+        Ok(())
521
+    }
522
+
523
+    /// Start coordinator heartbeat loop
524
+    async fn start_coordinator_heartbeat(&self) {
525
+        if let Some(coordinator) = &self.coordinator_manager {
526
+            let node_stats = Arc::clone(&self.node_stats);
527
+            let storage_manager = Arc::clone(&self.storage_manager);
528
+
529
+            coordinator.start_heartbeat(move || {
530
+                let stats = tokio::task::block_in_place(|| {
531
+                    tokio::runtime::Handle::current().block_on(async {
532
+                        let node_stats = node_stats.read().await;
533
+                        let capacity_info = storage_manager.get_capacity_info().await;
534
+                        let uptime = node_stats.start_time.elapsed().as_secs() as i64;
535
+
536
+                        crate::coordinator::types::NodeStats {
537
+                            storage_used: capacity_info.used_space as i64,
538
+                            storage_available: capacity_info.available_space as i64,
539
+                            chunks_stored: capacity_info.file_count as i64, // Approximation
540
+                            bandwidth_up: node_stats.bytes_sent as i64,
541
+                            bandwidth_down: node_stats.bytes_received as i64,
542
+                            cpu_usage: 0.0, // TODO: Implement CPU monitoring
543
+                            memory_usage: 0.0, // TODO: Implement memory monitoring
544
+                            uptime_seconds: uptime,
545
+                        }
546
+                    })
547
+                });
548
+                stats
549
+            }).await;
550
+
551
+            info!("Started coordinator heartbeat");
552
+        }
553
+    }
554
+
555
+    /// Register a file with the coordinator if available
556
+    async fn register_file_with_coordinator(&self, file_id: &str, file_hash: &str, file_size: u64, filename: &str) -> Result<()> {
557
+        if let Some(coordinator) = &self.coordinator_manager {
558
+            // Get file chunks from storage manager
559
+            let chunks = match self.storage_manager.get_file_chunks(file_id).await {
560
+                Ok(Some(chunk_list)) => {
561
+                    chunk_list.into_iter().enumerate().map(|(index, chunk_id)| {
562
+                        crate::coordinator::types::ChunkMetadata {
563
+                            chunk_id: chunk_id.clone(),
564
+                            hash: chunk_id, // For now, use chunk_id as hash
565
+                            size: self.config.storage.chunk_size as i64, // Default chunk size
566
+                            index: index as i32,
567
+                        }
568
+                    }).collect()
569
+                }
570
+                _ => Vec::new(),
571
+            };
572
+
573
+            let response = coordinator.register_file(
574
+                file_id.to_string(),
575
+                filename.to_string(),
576
+                file_size,
577
+                file_hash.to_string(),
578
+                chunks,
579
+            ).await?;
580
+
581
+            if response.success {
582
+                debug!("Successfully registered file {} with coordinator", file_id);
583
+                if !response.chunk_placements.is_empty() {
584
+                    debug!("Coordinator provided {} chunk placement recommendations", response.chunk_placements.len());
585
+                    // TODO: Handle chunk placement recommendations
586
+                }
587
+            } else {
588
+                warn!("Failed to register file with coordinator: {}", response.message);
589
+            }
590
+        }
591
+
592
+        Ok(())
593
+    }
594
+
595
+    /// Find chunk locations using coordinator
596
+    async fn find_chunk_locations_via_coordinator(&self, chunk_id: &str) -> Result<Vec<String>> {
597
+        if let Some(coordinator) = &self.coordinator_manager {
598
+            let response = coordinator.find_chunk_locations(chunk_id.to_string(), 3).await?;
599
+
600
+            if response.success {
601
+                debug!("Found {} locations for chunk {} via coordinator", response.node_addresses.len(), chunk_id);
602
+                Ok(response.node_addresses)
603
+            } else {
604
+                debug!("Coordinator couldn't find locations for chunk {}: {}", chunk_id, response.message);
605
+                Ok(Vec::new())
606
+            }
607
+        } else {
608
+            Ok(Vec::new())
609
+        }
610
+    }
611
+
612
+    /// Get coordinator registration status
613
+    pub fn get_coordinator_status(&self) -> Option<&RegistrationStatus> {
614
+        self.coordinator_manager.as_ref().map(|c| c.get_registration_status())
615
+    }
414616
 }
415617
 
416618
 /// Comprehensive node status information
src/storage/encrypted_chunk_store.rsmodified
@@ -504,7 +504,7 @@ mod tests {
504504
     #[tokio::test]
505505
     async fn test_encrypted_chunk_store_creation() {
506506
         let temp_dir = tempdir().unwrap();
507
-        let store = EncryptedChunkStore::new(temp_dir.path()).await.unwrap();
507
+        let store = EncryptedChunkStore::new(temp_dir.path()).unwrap();
508508
         let stats = store.get_encrypted_stats().await;
509509
         
510510
         assert_eq!(stats.total_encrypted_chunks, 0);
@@ -515,7 +515,7 @@ mod tests {
515515
     #[tokio::test]
516516
     async fn test_encrypted_chunk_deduplication() {
517517
         let temp_dir = tempdir().unwrap();
518
-        let store = EncryptedChunkStore::new(temp_dir.path()).await.unwrap();
518
+        let store = EncryptedChunkStore::new(temp_dir.path()).unwrap();
519519
         
520520
         let encrypted_data = EncryptedData {
521521
             segment_index: 0,
src/storage/storage_manager.rsmodified
@@ -504,6 +504,23 @@ impl StorageManager {
504504
             None
505505
         }
506506
     }
507
+
508
+    /// Get chunk IDs for a file (needed for coordinator integration)
509
+    ///
510
+    /// Returns the list of chunk IDs that comprise the given file
511
+    pub async fn get_file_chunks(&self, file_id: &str) -> Result<Option<Vec<String>>> {
512
+        debug!("Getting chunk list for file: {}", file_id);
513
+
514
+        match self.metadata_store.get_file(file_id).await? {
515
+            Some(metadata) => {
516
+                Ok(Some(metadata.chunk_ids))
517
+            }
518
+            None => {
519
+                debug!("File not found: {}", file_id);
520
+                Ok(None)
521
+            }
522
+        }
523
+    }
507524
 }
508525
 
509526
 #[cfg(test)]