1use alloc::format;
2use alloc::vec::Vec;
3
4use crate::SdkError;
5
6use super::{
7 ConnectionEvent, ConnectionLifecycle, ResumeRequest, SubscriptionId, SubscriptionRecovery,
8};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct ConnectionPoolConfig {
17 pub max_connections: usize,
19 pub timeout_millis: u64,
21 pub buffer_size: usize,
23}
24
25impl ConnectionPoolConfig {
26 #[must_use]
28 pub const fn new(max_connections: usize, timeout_millis: u64, buffer_size: usize) -> Self {
29 Self {
30 max_connections,
31 timeout_millis,
32 buffer_size,
33 }
34 }
35
36 pub fn validate(self) -> Result<Self, SdkError> {
42 if self.max_connections == 0 {
43 return Err(SdkError::Connection {
44 description: "connection pool max_connections must be greater than zero".into(),
45 });
46 }
47
48 Ok(self)
49 }
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct PoolConnectionId(usize);
55
56impl PoolConnectionId {
57 #[must_use]
59 pub const fn new(value: usize) -> Self {
60 Self(value)
61 }
62
63 #[must_use]
65 pub const fn get(self) -> usize {
66 self.0
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub struct SubscriptionAssignment {
73 pub subscription_id: SubscriptionId,
75 pub connection_id: PoolConnectionId,
77}
78
79#[derive(Debug)]
80struct PoolConnection {
81 id: PoolConnectionId,
82 subscription_count: usize,
83 lifecycle: ConnectionLifecycle,
84 recovery: SubscriptionRecovery,
85}
86
87impl PoolConnection {
88 fn new(id: PoolConnectionId) -> Self {
89 Self {
90 id,
91 subscription_count: 0,
92 lifecycle: ConnectionLifecycle::new(),
93 recovery: SubscriptionRecovery::new(),
94 }
95 }
96}
97
98#[derive(Debug)]
100pub struct ConnectionPool {
101 config: ConnectionPoolConfig,
102 connections: Vec<PoolConnection>,
103}
104
105impl ConnectionPool {
106 pub fn new(config: ConnectionPoolConfig) -> Result<Self, SdkError> {
112 let config = config.validate()?;
113 let mut connections = Vec::with_capacity(config.max_connections);
114
115 for slot in 0..config.max_connections {
116 connections.push(PoolConnection::new(PoolConnectionId::new(slot)));
117 }
118
119 Ok(Self {
120 config,
121 connections,
122 })
123 }
124
125 #[must_use]
127 pub const fn config(&self) -> ConnectionPoolConfig {
128 self.config
129 }
130
131 #[must_use]
133 pub const fn max_connections(&self) -> usize {
134 self.config.max_connections
135 }
136
137 #[must_use]
139 pub fn connection_count(&self) -> usize {
140 self.connections.len()
141 }
142
143 pub fn assign_subscription(
149 &mut self,
150 subscription_id: SubscriptionId,
151 ) -> Result<SubscriptionAssignment, SdkError> {
152 if let Some(existing) = self.connection_for_subscription(subscription_id) {
153 return Ok(SubscriptionAssignment {
154 subscription_id,
155 connection_id: existing,
156 });
157 }
158
159 let connection = self
160 .connections
161 .iter_mut()
162 .min_by_key(|connection| (connection.subscription_count, connection.id))
163 .ok_or_else(|| SdkError::Connection {
164 description: "connection pool has no connections".into(),
165 })?;
166
167 connection.subscription_count = connection.subscription_count.saturating_add(1);
168 connection.recovery.track_subscription(subscription_id);
169
170 Ok(SubscriptionAssignment {
171 subscription_id,
172 connection_id: connection.id,
173 })
174 }
175
176 pub fn acknowledge(
182 &mut self,
183 subscription_id: SubscriptionId,
184 sequence: u64,
185 ) -> Result<(), SdkError> {
186 let connection = self.connection_for_subscription_mut(subscription_id)?;
187 connection.recovery.acknowledge(subscription_id, sequence);
188 Ok(())
189 }
190
191 pub fn unsubscribe(&mut self, subscription_id: SubscriptionId) -> Result<(), SdkError> {
197 let connection = self.connection_for_subscription_mut(subscription_id)?;
198 connection.recovery.unsubscribe(subscription_id);
199 connection.subscription_count = connection.subscription_count.saturating_sub(1);
200 Ok(())
201 }
202
203 pub fn resume_requests_for_transition(
209 &self,
210 event: &ConnectionEvent,
211 ) -> Result<Vec<ResumeRequest>, SdkError> {
212 let mut requests = Vec::new();
213 for connection in &self.connections {
214 requests.extend(connection.recovery.resume_requests_for_transition(event)?);
215 }
216 Ok(requests)
217 }
218
219 #[must_use]
221 pub fn connection_for_subscription(
222 &self,
223 subscription_id: SubscriptionId,
224 ) -> Option<PoolConnectionId> {
225 self.connections
226 .iter()
227 .find(|connection| connection.recovery.is_active(subscription_id))
228 .map(|connection| connection.id)
229 }
230
231 pub fn subscription_count(&self, connection_id: PoolConnectionId) -> Result<usize, SdkError> {
237 self.connection(connection_id)
238 .map(|connection| connection.subscription_count)
239 }
240
241 pub fn recovery(
247 &self,
248 connection_id: PoolConnectionId,
249 ) -> Result<&SubscriptionRecovery, SdkError> {
250 self.connection(connection_id)
251 .map(|connection| &connection.recovery)
252 }
253
254 pub fn lifecycle(
260 &self,
261 connection_id: PoolConnectionId,
262 ) -> Result<&ConnectionLifecycle, SdkError> {
263 self.connection(connection_id)
264 .map(|connection| &connection.lifecycle)
265 }
266
267 fn connection(&self, connection_id: PoolConnectionId) -> Result<&PoolConnection, SdkError> {
268 self.connections
269 .iter()
270 .find(|connection| connection.id == connection_id)
271 .ok_or_else(|| SdkError::Connection {
272 description: format!("unknown pooled connection {}", connection_id.get()),
273 })
274 }
275
276 fn connection_for_subscription_mut(
277 &mut self,
278 subscription_id: SubscriptionId,
279 ) -> Result<&mut PoolConnection, SdkError> {
280 self.connections
281 .iter_mut()
282 .find(|connection| connection.recovery.is_active(subscription_id))
283 .ok_or_else(|| SdkError::Connection {
284 description: format!("unknown subscription {}", subscription_id.get()),
285 })
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::connection::{ConnectionState, DisconnectReason};
293
294 #[test]
295 fn invalid_pool_size_is_rejected() {
296 let config = ConnectionPoolConfig::new(0, 10, 16);
297
298 assert!(ConnectionPool::new(config).is_err());
299 }
300
301 #[test]
302 fn subscriptions_are_distributed_across_connections() -> Result<(), SdkError> {
303 let config = ConnectionPoolConfig::new(2, 10, 16);
304 let mut pool = ConnectionPool::new(config)?;
305
306 let first = pool.assign_subscription(SubscriptionId::new(1))?;
307 let second = pool.assign_subscription(SubscriptionId::new(2))?;
308 let third = pool.assign_subscription(SubscriptionId::new(3))?;
309
310 assert_ne!(first.connection_id, second.connection_id);
311 assert_eq!(third.connection_id, first.connection_id);
312 assert_eq!(pool.subscription_count(first.connection_id)?, 2);
313 assert_eq!(pool.subscription_count(second.connection_id)?, 1);
314 Ok(())
315 }
316
317 #[test]
318 fn multiple_subscriptions_share_configured_connections() -> Result<(), SdkError> {
319 let config = ConnectionPoolConfig::new(1, 20, 32);
320 let mut pool = ConnectionPool::new(config)?;
321
322 let first = pool.assign_subscription(SubscriptionId::new(10))?;
323 let second = pool.assign_subscription(SubscriptionId::new(11))?;
324
325 assert_eq!(first.connection_id, second.connection_id);
326 assert_eq!(pool.max_connections(), 1);
327 assert_eq!(pool.subscription_count(first.connection_id)?, 2);
328 Ok(())
329 }
330
331 #[test]
332 fn pooled_recovery_builds_resume_requests_on_reconnect() -> Result<(), SdkError> {
333 let config = ConnectionPoolConfig::new(2, 10, 16);
334 let mut pool = ConnectionPool::new(config)?;
335 let first = SubscriptionId::new(21);
336 let second = SubscriptionId::new(22);
337 let event = ConnectionEvent::new(ConnectionState::Reconnecting, ConnectionState::Connected);
338
339 pool.assign_subscription(first)?;
340 pool.assign_subscription(second)?;
341 pool.acknowledge(first, 4)?;
342
343 let requests = pool.resume_requests_for_transition(&event)?;
344
345 assert_eq!(
346 requests,
347 vec![ResumeRequest::new(first, 5), ResumeRequest::new(second, 0)]
348 );
349 Ok(())
350 }
351
352 #[test]
353 fn unsubscribe_removes_assignment() -> Result<(), SdkError> {
354 let config = ConnectionPoolConfig::new(2, 10, 16);
355 let mut pool = ConnectionPool::new(config)?;
356 let subscription_id = SubscriptionId::new(31);
357 let assignment = pool.assign_subscription(subscription_id)?;
358
359 pool.unsubscribe(subscription_id)?;
360
361 assert_eq!(pool.connection_for_subscription(subscription_id), None);
362 assert_eq!(pool.subscription_count(assignment.connection_id)?, 0);
363 assert!(pool.unsubscribe(subscription_id).is_err());
364 Ok(())
365 }
366
367 #[test]
368 fn non_reconnect_transition_does_not_resume() -> Result<(), SdkError> {
369 let config = ConnectionPoolConfig::new(2, 10, 16);
370 let mut pool = ConnectionPool::new(config)?;
371 let event = ConnectionEvent::new(
372 ConnectionState::Connected,
373 ConnectionState::Disconnected {
374 reason: DisconnectReason::Normal,
375 },
376 );
377
378 pool.assign_subscription(SubscriptionId::new(41))?;
379
380 assert!(pool.resume_requests_for_transition(&event)?.is_empty());
381 Ok(())
382 }
383}