@zap-js/server 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,404 +0,0 @@
1
- //! IPC Connection Pool
2
- //!
3
- //! Provides a pool of persistent IPC connections to the TypeScript runtime.
4
- //! This eliminates per-request connection overhead, significantly improving
5
- //! throughput for handler invocations.
6
- //!
7
- //! Features:
8
- //! - Pool of N persistent connections (default: 4)
9
- //! - Health checks before use
10
- //! - Automatic reconnection on failure
11
- //! - Connection timeout handling
12
- //! - Fair connection distribution
13
-
14
- use crate::error::{ZapError, ZapResult};
15
- use crate::ipc::{IpcClient, IpcEncoding, IpcMessage};
16
- use std::sync::atomic::{AtomicUsize, Ordering};
17
- use std::sync::Arc;
18
- use std::time::Duration;
19
- use tokio::sync::{Mutex, Semaphore};
20
- use tracing::{debug, error, warn};
21
-
22
- /// Default number of connections in the pool
23
- const DEFAULT_POOL_SIZE: usize = 4;
24
-
25
- /// Default connection timeout in seconds
26
- const DEFAULT_CONNECT_TIMEOUT_SECS: u64 = 5;
27
-
28
- /// Default health check interval in seconds
29
- const HEALTH_CHECK_INTERVAL_SECS: u64 = 30;
30
-
31
- /// A pooled connection wrapper
32
- struct PooledConnection {
33
- client: Option<IpcClient>,
34
- last_used: std::time::Instant,
35
- healthy: bool,
36
- }
37
-
38
- impl PooledConnection {
39
- fn new() -> Self {
40
- Self {
41
- client: None,
42
- last_used: std::time::Instant::now(),
43
- healthy: false,
44
- }
45
- }
46
-
47
- fn is_valid(&self) -> bool {
48
- self.client.is_some() && self.healthy
49
- }
50
- }
51
-
52
- /// Configuration for the connection pool
53
- #[derive(Clone)]
54
- pub struct PoolConfig {
55
- /// Number of connections in the pool
56
- pub size: usize,
57
- /// Connection timeout
58
- pub connect_timeout: Duration,
59
- /// Socket path for IPC
60
- pub socket_path: String,
61
- /// IPC encoding format
62
- pub encoding: IpcEncoding,
63
- /// Health check interval
64
- pub health_check_interval: Duration,
65
- }
66
-
67
- impl Default for PoolConfig {
68
- fn default() -> Self {
69
- Self {
70
- size: DEFAULT_POOL_SIZE,
71
- connect_timeout: Duration::from_secs(DEFAULT_CONNECT_TIMEOUT_SECS),
72
- socket_path: String::new(),
73
- encoding: IpcEncoding::default(),
74
- health_check_interval: Duration::from_secs(HEALTH_CHECK_INTERVAL_SECS),
75
- }
76
- }
77
- }
78
-
79
- impl PoolConfig {
80
- /// Create a new pool configuration with the given socket path
81
- pub fn new(socket_path: String) -> Self {
82
- Self {
83
- socket_path,
84
- ..Default::default()
85
- }
86
- }
87
-
88
- /// Set the pool size
89
- pub fn size(mut self, size: usize) -> Self {
90
- self.size = size;
91
- self
92
- }
93
-
94
- /// Set the connect timeout
95
- pub fn connect_timeout(mut self, timeout: Duration) -> Self {
96
- self.connect_timeout = timeout;
97
- self
98
- }
99
-
100
- /// Set the encoding format
101
- pub fn encoding(mut self, encoding: IpcEncoding) -> Self {
102
- self.encoding = encoding;
103
- self
104
- }
105
- }
106
-
107
- /// IPC Connection Pool
108
- ///
109
- /// Manages a pool of persistent connections to the TypeScript IPC server.
110
- /// Connections are reused across requests to eliminate connection overhead.
111
- pub struct ConnectionPool {
112
- /// Pool configuration
113
- config: PoolConfig,
114
- /// Pooled connections (each wrapped in Mutex for exclusive access)
115
- connections: Vec<Arc<Mutex<PooledConnection>>>,
116
- /// Semaphore to limit concurrent connection acquisition
117
- semaphore: Arc<Semaphore>,
118
- /// Round-robin index for fair distribution
119
- next_index: AtomicUsize,
120
- /// Whether the pool is initialized
121
- initialized: std::sync::atomic::AtomicBool,
122
- }
123
-
124
- impl ConnectionPool {
125
- /// Create a new connection pool with the given configuration
126
- pub fn new(config: PoolConfig) -> Self {
127
- let connections = (0..config.size)
128
- .map(|_| Arc::new(Mutex::new(PooledConnection::new())))
129
- .collect();
130
-
131
- Self {
132
- semaphore: Arc::new(Semaphore::new(config.size)),
133
- connections,
134
- config,
135
- next_index: AtomicUsize::new(0),
136
- initialized: std::sync::atomic::AtomicBool::new(false),
137
- }
138
- }
139
-
140
- /// Create a pool with the given socket path and default settings
141
- pub fn with_socket(socket_path: String) -> Self {
142
- Self::new(PoolConfig::new(socket_path))
143
- }
144
-
145
- /// Initialize the connection pool by establishing all connections
146
- pub async fn initialize(&self) -> ZapResult<()> {
147
- if self.initialized.load(Ordering::Acquire) {
148
- return Ok(());
149
- }
150
-
151
- debug!("Initializing connection pool with {} connections", self.config.size);
152
-
153
- let mut init_count = 0;
154
- for (i, conn_mutex) in self.connections.iter().enumerate() {
155
- let mut conn = conn_mutex.lock().await;
156
- match self.create_connection().await {
157
- Ok(client) => {
158
- conn.client = Some(client);
159
- conn.healthy = true;
160
- conn.last_used = std::time::Instant::now();
161
- init_count += 1;
162
- debug!("Connection {} initialized", i);
163
- }
164
- Err(e) => {
165
- warn!("Failed to initialize connection {}: {}", i, e);
166
- // Continue - we'll try to reconnect later
167
- }
168
- }
169
- }
170
-
171
- if init_count == 0 {
172
- return Err(ZapError::ipc("Failed to initialize any pool connections"));
173
- }
174
-
175
- self.initialized.store(true, Ordering::Release);
176
- debug!("Connection pool initialized with {}/{} connections", init_count, self.config.size);
177
-
178
- Ok(())
179
- }
180
-
181
- /// Create a new IPC connection
182
- async fn create_connection(&self) -> ZapResult<IpcClient> {
183
- let timeout = self.config.connect_timeout;
184
-
185
- tokio::time::timeout(
186
- timeout,
187
- IpcClient::connect_with_encoding(&self.config.socket_path, self.config.encoding),
188
- )
189
- .await
190
- .map_err(|_| ZapError::timeout("Connection pool connect timeout", timeout.as_millis() as u64))?
191
- }
192
-
193
- /// Get a connection from the pool, reconnecting if necessary
194
- async fn get_connection_index(&self) -> ZapResult<usize> {
195
- // Round-robin selection with wrap-around
196
- let index = self.next_index.fetch_add(1, Ordering::Relaxed) % self.config.size;
197
- Ok(index)
198
- }
199
-
200
- /// Execute a request-response operation using a pooled connection
201
- ///
202
- /// This method handles:
203
- /// - Connection acquisition from pool
204
- /// - Automatic reconnection on failure
205
- /// - Connection release back to pool
206
- pub async fn send_recv(&self, message: IpcMessage) -> ZapResult<IpcMessage> {
207
- // Acquire semaphore permit (limits concurrent usage)
208
- let _permit = self.semaphore.acquire().await.map_err(|_| {
209
- ZapError::ipc("Connection pool semaphore closed")
210
- })?;
211
-
212
- // Get a connection index
213
- let index = self.get_connection_index().await?;
214
- let conn_mutex = &self.connections[index];
215
-
216
- // Try with the existing connection first
217
- let mut conn = conn_mutex.lock().await;
218
-
219
- // Check if connection is valid
220
- if !conn.is_valid() {
221
- debug!("Connection {} invalid, reconnecting", index);
222
- match self.create_connection().await {
223
- Ok(client) => {
224
- conn.client = Some(client);
225
- conn.healthy = true;
226
- }
227
- Err(e) => {
228
- conn.healthy = false;
229
- return Err(e);
230
- }
231
- }
232
- }
233
-
234
- // Send and receive
235
- if let Some(client) = &mut conn.client {
236
- match client.send_recv(message.clone()).await {
237
- Ok(response) => {
238
- conn.last_used = std::time::Instant::now();
239
- Ok(response)
240
- }
241
- Err(e) => {
242
- // Connection failed, mark as unhealthy
243
- warn!("Connection {} failed: {}, marking unhealthy", index, e);
244
- conn.healthy = false;
245
- conn.client = None;
246
-
247
- // Try to reconnect and retry once
248
- match self.create_connection().await {
249
- Ok(mut new_client) => {
250
- match new_client.send_recv(message).await {
251
- Ok(response) => {
252
- conn.client = Some(new_client);
253
- conn.healthy = true;
254
- conn.last_used = std::time::Instant::now();
255
- Ok(response)
256
- }
257
- Err(retry_err) => {
258
- error!("Retry also failed: {}", retry_err);
259
- Err(retry_err)
260
- }
261
- }
262
- }
263
- Err(reconnect_err) => {
264
- error!("Reconnect failed: {}", reconnect_err);
265
- Err(reconnect_err)
266
- }
267
- }
268
- }
269
- }
270
- } else {
271
- Err(ZapError::ipc("No connection available"))
272
- }
273
- }
274
-
275
- /// Perform health check on all connections
276
- pub async fn health_check(&self) -> (usize, usize) {
277
- let mut healthy = 0;
278
- let mut total = 0;
279
-
280
- for conn_mutex in &self.connections {
281
- total += 1;
282
- let conn = conn_mutex.lock().await;
283
- if conn.is_valid() {
284
- healthy += 1;
285
- }
286
- }
287
-
288
- (healthy, total)
289
- }
290
-
291
- /// Close all connections in the pool
292
- pub async fn close(&self) {
293
- debug!("Closing connection pool");
294
-
295
- for conn_mutex in &self.connections {
296
- let mut conn = conn_mutex.lock().await;
297
- conn.client = None;
298
- conn.healthy = false;
299
- }
300
-
301
- self.initialized.store(false, Ordering::Release);
302
- }
303
-
304
- /// Get pool configuration
305
- pub fn config(&self) -> &PoolConfig {
306
- &self.config
307
- }
308
-
309
- /// Get pool statistics
310
- pub fn stats(&self) -> PoolStats {
311
- PoolStats {
312
- size: self.config.size,
313
- initialized: self.initialized.load(Ordering::Acquire),
314
- }
315
- }
316
- }
317
-
318
- /// Pool statistics
319
- #[derive(Debug, Clone)]
320
- pub struct PoolStats {
321
- pub size: usize,
322
- pub initialized: bool,
323
- }
324
-
325
- /// Global connection pool singleton
326
- static GLOBAL_POOL: std::sync::OnceLock<Arc<ConnectionPool>> = std::sync::OnceLock::new();
327
-
328
- /// Initialize the global connection pool
329
- pub fn init_global_pool(socket_path: String) -> ZapResult<Arc<ConnectionPool>> {
330
- let pool = Arc::new(ConnectionPool::with_socket(socket_path));
331
-
332
- match GLOBAL_POOL.set(pool.clone()) {
333
- Ok(()) => Ok(pool),
334
- Err(_) => {
335
- // Pool already initialized, return existing
336
- Ok(GLOBAL_POOL.get().unwrap().clone())
337
- }
338
- }
339
- }
340
-
341
- /// Initialize the global connection pool with custom config
342
- pub fn init_global_pool_with_config(config: PoolConfig) -> ZapResult<Arc<ConnectionPool>> {
343
- let pool = Arc::new(ConnectionPool::new(config));
344
-
345
- match GLOBAL_POOL.set(pool.clone()) {
346
- Ok(()) => Ok(pool),
347
- Err(_) => {
348
- // Pool already initialized, return existing
349
- Ok(GLOBAL_POOL.get().unwrap().clone())
350
- }
351
- }
352
- }
353
-
354
- /// Get the global connection pool (must be initialized first)
355
- pub fn get_global_pool() -> Option<Arc<ConnectionPool>> {
356
- GLOBAL_POOL.get().cloned()
357
- }
358
-
359
- #[cfg(test)]
360
- mod tests {
361
- use super::*;
362
-
363
- #[test]
364
- fn test_pool_config_builder() {
365
- let config = PoolConfig::new("/tmp/test.sock".to_string())
366
- .size(8)
367
- .connect_timeout(Duration::from_secs(10))
368
- .encoding(IpcEncoding::Json);
369
-
370
- assert_eq!(config.size, 8);
371
- assert_eq!(config.connect_timeout, Duration::from_secs(10));
372
- assert_eq!(config.encoding, IpcEncoding::Json);
373
- assert_eq!(config.socket_path, "/tmp/test.sock");
374
- }
375
-
376
- #[test]
377
- fn test_pool_creation() {
378
- let pool = ConnectionPool::with_socket("/tmp/test.sock".to_string());
379
-
380
- assert_eq!(pool.config().size, DEFAULT_POOL_SIZE);
381
- assert_eq!(pool.connections.len(), DEFAULT_POOL_SIZE);
382
- assert!(!pool.initialized.load(Ordering::Acquire));
383
- }
384
-
385
- #[test]
386
- fn test_pool_stats() {
387
- let pool = ConnectionPool::with_socket("/tmp/test.sock".to_string());
388
- let stats = pool.stats();
389
-
390
- assert_eq!(stats.size, DEFAULT_POOL_SIZE);
391
- assert!(!stats.initialized);
392
- }
393
-
394
- #[tokio::test]
395
- async fn test_round_robin_index() {
396
- let pool = ConnectionPool::new(PoolConfig::new("/tmp/test.sock".to_string()).size(4));
397
-
398
- // Test round-robin distribution
399
- for expected in 0..12 {
400
- let index = pool.get_connection_index().await.unwrap();
401
- assert_eq!(index, expected % 4);
402
- }
403
- }
404
- }