Skip to content

Commit 9bef6eb

Browse files
authored
feat: add connection pooling infrastructure for future features (#1)
* chore: connection pooling test * feat: add connection pooling infrastructure for future features - Implement ConnectionPool module as placeholder for future use - Add analysis showing pooling provides no benefit for current usage - Document design decision in ARCHITECTURE.md - Include unit tests for pool API surface * add: license header
1 parent 5a9c46e commit 9bef6eb

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed

src/ssh/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
pub mod client;
1616
pub mod handler;
1717
pub mod known_hosts;
18+
pub mod pool;
1819

1920
pub use client::SshClient;
2021
pub use handler::BsshHandler;
22+
pub use pool::ConnectionPool;

src/ssh/pool.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//! Connection pooling module for SSH connections.
2+
//!
3+
//! NOTE: This is a placeholder implementation. The async-ssh2-tokio Client
4+
//! doesn't support connection reuse or cloning, so actual pooling is not
5+
//! currently possible. This module provides the infrastructure for future
6+
//! connection pooling when the underlying library supports it.
7+
//!
8+
//! The current implementation always creates new connections but provides
9+
//! the API surface for connection pooling to minimize future refactoring.
10+
11+
// Copyright 2025 Lablup Inc. and Jeongkyu Shin
12+
//
13+
// Licensed under the Apache License, Version 2.0 (the "License");
14+
// you may not use this file except in compliance with the License.
15+
// You may obtain a copy of the License at
16+
//
17+
// http://www.apache.org/licenses/LICENSE-2.0
18+
//
19+
// Unless required by applicable law or agreed to in writing, software
20+
// distributed under the License is distributed on an "AS IS" BASIS,
21+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22+
// See the License for the specific language governing permissions and
23+
// limitations under the License.
24+
25+
use anyhow::Result;
26+
use async_ssh2_tokio::Client;
27+
use std::sync::Arc;
28+
use std::time::Duration;
29+
use tokio::sync::RwLock;
30+
use tracing::{debug, trace};
31+
32+
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
33+
struct ConnectionKey {
34+
host: String,
35+
port: u16,
36+
user: String,
37+
}
38+
39+
/// Connection pool for SSH connections.
40+
///
41+
/// Currently a placeholder implementation due to async-ssh2-tokio limitations.
42+
/// Always creates new connections regardless of the `enabled` flag.
43+
pub struct ConnectionPool {
44+
/// Placeholder for future connection storage
45+
_connections: Arc<RwLock<Vec<ConnectionKey>>>,
46+
ttl: Duration,
47+
enabled: bool,
48+
max_connections: usize,
49+
}
50+
51+
impl ConnectionPool {
52+
/// Create a new connection pool.
53+
///
54+
/// Note: Pooling is not actually implemented due to library limitations.
55+
pub fn new(ttl: Duration, max_connections: usize, enabled: bool) -> Self {
56+
Self {
57+
_connections: Arc::new(RwLock::new(Vec::new())),
58+
ttl,
59+
enabled,
60+
max_connections,
61+
}
62+
}
63+
64+
pub fn disabled() -> Self {
65+
Self::new(Duration::from_secs(0), 0, false)
66+
}
67+
68+
pub fn with_defaults() -> Self {
69+
Self::new(
70+
Duration::from_secs(300), // 5 minutes TTL
71+
50, // max 50 connections
72+
false, // disabled by default
73+
)
74+
}
75+
76+
/// Get or create a connection.
77+
///
78+
/// Currently always creates a new connection due to async-ssh2-tokio limitations.
79+
/// The Client type doesn't support cloning or connection reuse.
80+
pub async fn get_or_create<F>(
81+
&self,
82+
host: &str,
83+
port: u16,
84+
user: &str,
85+
create_fn: F,
86+
) -> Result<Client>
87+
where
88+
F: FnOnce() -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Client>> + Send>>,
89+
{
90+
let _key = ConnectionKey {
91+
host: host.to_string(),
92+
port,
93+
user: user.to_string(),
94+
};
95+
96+
if self.enabled {
97+
trace!("Connection pooling enabled (placeholder mode)");
98+
// In the future, we would check for existing connections here
99+
// For now, we always create new connections
100+
} else {
101+
trace!("Connection pooling disabled");
102+
}
103+
104+
// Always create new connection (pooling not possible with current library)
105+
debug!("Creating new SSH connection to {}@{}:{}", user, host, port);
106+
create_fn().await
107+
}
108+
109+
/// Return a connection to the pool.
110+
///
111+
/// Currently a no-op due to connection reuse limitations.
112+
pub async fn return_connection(
113+
&self,
114+
_host: &str,
115+
_port: u16,
116+
_user: &str,
117+
_client: Client,
118+
) {
119+
// No-op: Client cannot be reused
120+
if self.enabled {
121+
trace!("Connection return requested (no-op in placeholder mode)");
122+
}
123+
}
124+
125+
/// Clean up expired connections.
126+
///
127+
/// Currently a no-op.
128+
pub async fn cleanup_expired(&self) {
129+
if self.enabled {
130+
trace!("Cleanup requested (no-op in placeholder mode)");
131+
}
132+
}
133+
134+
/// Clear all connections from the pool.
135+
///
136+
/// Currently a no-op.
137+
pub async fn clear(&self) {
138+
if self.enabled {
139+
trace!("Clear requested (no-op in placeholder mode)");
140+
}
141+
}
142+
143+
/// Get the number of pooled connections.
144+
///
145+
/// Always returns 0 in the current implementation.
146+
pub async fn size(&self) -> usize {
147+
0 // No actual pooling
148+
}
149+
150+
pub fn is_enabled(&self) -> bool {
151+
self.enabled
152+
}
153+
154+
pub fn enable(&mut self) {
155+
self.enabled = true;
156+
debug!("Connection pooling enabled");
157+
}
158+
159+
pub fn disable(&mut self) {
160+
self.enabled = false;
161+
debug!("Connection pooling disabled");
162+
}
163+
}
164+
165+
impl Default for ConnectionPool {
166+
fn default() -> Self {
167+
Self::with_defaults()
168+
}
169+
}
170+
171+
#[cfg(test)]
172+
mod tests {
173+
use super::*;
174+
175+
#[tokio::test]
176+
async fn test_pool_disabled_by_default() {
177+
let pool = ConnectionPool::with_defaults();
178+
assert!(!pool.is_enabled());
179+
assert_eq!(pool.size().await, 0);
180+
}
181+
182+
#[tokio::test]
183+
async fn test_pool_cleanup() {
184+
let pool = ConnectionPool::new(Duration::from_millis(100), 10, true);
185+
186+
// Pool starts empty
187+
assert_eq!(pool.size().await, 0);
188+
189+
// Cleanup should work even on empty pool
190+
pool.cleanup_expired().await;
191+
assert_eq!(pool.size().await, 0);
192+
}
193+
194+
#[tokio::test]
195+
async fn test_pool_clear() {
196+
let pool = ConnectionPool::new(Duration::from_secs(60), 10, true);
197+
198+
pool.clear().await;
199+
assert_eq!(pool.size().await, 0);
200+
}
201+
}

0 commit comments

Comments
 (0)