-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathassignment-7.rs
More file actions
280 lines (240 loc) · 8.94 KB
/
Copy pathassignment-7.rs
File metadata and controls
280 lines (240 loc) · 8.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
// Assignment 7: Connection Pool
//
// Objective: Implement a simple async connection pool that manages a fixed set
// of reusable connections, with callers waiting when none are available.
//
// Scenario: You're building a database connection pool. Connections are
// expensive to create, so you want to reuse them. Multiple tasks need
// connections concurrently, but the pool has a fixed size.
//
// Requirements:
//
// 1. Define a Connection struct that simulates a database connection:
// - Has an id: u32 field
// - Has an async fn query(&self, sql: &str) -> String method that simulates
// work (sleep 50-200ms, return a result string)
// 2. Implement a Pool struct with:
// - fn new(size: u32) -> Pool — creates the pool with size
// pre-created connections
// * Note: If the connections were *real* database connections, new would
// need to be async.
// - async fn get(&self) -> PoolGuard — checks out a connection. If none are
// available, waits until one is returned
// - When the PoolGuard is dropped, the connection is automatically returned
// to the pool
// 3. PoolGuard behavior:
// - Wraps a connection and a reference back to the pool
// - Implements Deref so you can call guard.query(...) directly
// - On drop, returns the connection to the pool
// 4. Use these Tokio primitives:
// - tokio::sync::Mutex to protect the pool's internal connection list
// - tokio::sync::Semaphore to limit concurrent checkouts and wake waiters
// (or Notify — your choice)
// 5. Write a main that demonstrates:
// - Create a pool of size 3
// - Spawn 10 tasks that each check out a connection, run a query, and return it
// - Print which connection each task got and when
// - Show that at most 3 tasks hold connections at any time
// 6. Write tests:
// - Pool returns connections up to its capacity without blocking
// - A task blocks when the pool is exhausted, and proceeds once a
// connection is returned
// - Connections are reused (same IDs appear multiple times)
//
// Hints:
//
// - Semaphore is ideal here: initialize with size permits. get() acquires a
// permit, pops a connection. PoolGuard::drop pushes the connection back and
// releases the permit.
// - For returning the connection on drop, PoolGuard needs an
// Option<Connection> (so you can .take() it in Drop) and an Arc reference to
// the pool internals.
// - Drop is synchronous — you can't .await inside it. Use a non-async Mutex
// (like std::sync::Mutex or parking_lot::Mutex) for the connection list, or
// use tokio::spawn to return the connection asynchronously.
// - Deref<Target = Connection> makes the guard ergonomic to use.
//
// Grading criteria:
//
// - Pool correctly limits concurrent access to N connections
// - Callers block (not error) when pool is exhausted
// - Connections are returned and reused
// - Guard pattern with Deref and Drop is implemented
// - Tests verify the above behaviors
// - No deadlocks, no panics
//
// Why this matters: Connection pools are everywhere — databases, HTTP clients,
// gRPC channels. Understanding the async primitives behind them (semaphore +
// mutex + guard pattern) is fundamental to building production Rust services.
//
// This is the toughest assignment yet — the Drop + async interaction is tricky.
// Take your time!
use std::collections::VecDeque;
use std::ops::Deref;
use std::sync::{Arc, Mutex};
use tokio::{
sync::{OwnedSemaphorePermit, Semaphore},
task::JoinSet,
};
struct Connection {
id: u32,
}
impl Connection {
fn new(id: u32) -> Self {
Self { id }
}
async fn query(&self, sql: &str) -> String {
// Create a random delay to simulate work
let delay = rand::random_range(50..=200);
let duration = tokio::time::Duration::from_millis(delay);
// Sleep for the duration
let _ = tokio::time::sleep(duration).await;
// Return a query result
format!(
r#"connection {}: query "{}" completed in {:?}"#,
self.id, sql, duration
)
}
}
struct Pool {
// Thread safe pool for taking and returning connections
connections: Arc<Mutex<VecDeque<Connection>>>,
// Semaphore tracks the number borrowed connection
semaphore: Arc<Semaphore>,
}
impl Pool {
fn new(size: u32) -> Self {
// Create the connections
let connections: VecDeque<Connection> =
(0..size).map(Connection::new).collect();
// Wrap the connections in a synchronization primitive
let connections = Arc::new(Mutex::new(connections));
// Use a semaphore to facilitate borrowing connections
let semaphore = Arc::new(Semaphore::new(size as usize));
Self {
connections,
semaphore,
}
}
async fn get(&self) -> PoolGuard {
// Create an owned permit to track in the PoolGuard
let semaphore_clone = self.semaphore.clone();
let permit = semaphore_clone
.acquire_owned()
.await
.expect("could not acquire permit");
let connection = self
.connections
.lock()
.expect("poisoned lock in PoolGuard::get")
.pop_front()
.expect("connection queue empty!");
let connections = Arc::clone(&self.connections);
PoolGuard {
connections,
connection: Some(connection),
_permit: permit,
}
}
}
struct PoolGuard {
connections: Arc<Mutex<VecDeque<Connection>>>,
connection: Option<Connection>,
_permit: OwnedSemaphorePermit,
}
impl Drop for PoolGuard {
fn drop(&mut self) {
let connection = self.connection.take();
self.connections
.lock()
.expect("poisoned lock in drop")
.push_back(connection.expect("missing connection"));
}
}
impl Deref for PoolGuard {
type Target = Connection;
fn deref(&self) -> &Self::Target {
self.connection.as_ref().expect("Connection taken")
}
}
// Simulate some database work
async fn simulate_database_query(task: u32, pool: Arc<Pool>) {
let connection = pool.get().await;
let query = format!("task {task} running a query");
let message = connection.query(&query).await;
println!("{message}");
}
#[tokio::main]
async fn main() {
const POOL_SIZE: u32 = 3;
const NUM_TASKS: u32 = 10;
let connection_pool = Arc::new(Pool::new(POOL_SIZE));
let mut tasks = JoinSet::new();
for task in 0..NUM_TASKS {
let pool_clone = Arc::clone(&connection_pool);
tasks.spawn(simulate_database_query(task, pool_clone));
}
tasks.join_all().await;
}
#[cfg(test)]
mod pool_tests {
use super::Pool;
use std::{sync::Arc, time::Duration};
use tokio::time::timeout;
#[tokio::test]
async fn returns_up_to_capacity_without_blocking() {
const POOL_SIZE: u32 = 3;
let pool = Arc::new(Pool::new(POOL_SIZE));
// Check out POOL_SIZE connections
let _c1 = pool.get().await;
let _c2 = pool.get().await;
let _c3 = pool.get().await;
// Next checkout should block
let result = timeout(Duration::from_millis(10), pool.get()).await;
assert!(result.is_err(), "get() should have blocked");
}
#[tokio::test]
async fn unblocks_when_connection_returned() {
const POOL_SIZE: u32 = 1;
let pool = Arc::new(Pool::new(POOL_SIZE));
let c1 = pool.get().await;
// Pool exhausted - should block
let result = timeout(Duration::from_millis(10), pool.get()).await;
assert!(result.is_err(), "get() should have blocked");
// Drop one connection and verify another is returned without blocking
drop(c1);
// Should succeed now
let result = timeout(Duration::from_millis(10), pool.get()).await;
assert!(result.is_ok(), "get() should not have blocked");
}
#[tokio::test]
async fn same_connection_reused() {
const POOL_SIZE: u32 = 1;
let pool = Arc::new(Pool::new(POOL_SIZE));
let c1 = pool.get().await;
let id1 = c1.id;
drop(c1);
let c2 = pool.get().await;
let id2 = c2.id;
drop(c2);
assert_eq!(id1, id2, "same connection should be reused");
}
}
#[cfg(test)]
mod connection_tests {
use super::Connection;
#[tokio::test(start_paused = true)]
async fn test_query() {
let connection = Connection::new(0);
let msg = connection.query("NAME=test").await;
let mut iter = msg.split_whitespace();
assert_eq!(Some("connection"), iter.next());
assert_eq!(Some("0:"), iter.next());
assert_eq!(Some("query"), iter.next());
assert_eq!(Some("\"NAME=test\""), iter.next());
assert_eq!(Some("completed"), iter.next());
assert_eq!(Some("in"), iter.next());
assert!(iter.next().expect("bad test expectation").ends_with("ms"));
assert_eq!(None, iter.next());
}
}