1
use super::*;
2
use sqlx::postgres::PgPoolOptions;
3

            
4
/// A pool handle that never connects — enough to make a slot "ready" without a
5
/// database behind it.
6
26
fn lazy_pool() -> Pool<Postgres> {
7
26
    PgPoolOptions::new()
8
26
        .max_connections(TENANT_MAX_CONNECTIONS)
9
26
        .connect_lazy("postgres://unused@localhost/unused")
10
26
        .expect("a syntactically valid DSN never fails to build a lazy pool")
11
26
}
12

            
13
/// A registry that gives up on a residency permit quickly, so the exhaustion
14
/// path is testable without a ten-second wait.
15
20
fn impatient_registry() -> PoolRegistry {
16
20
    PoolRegistry {
17
20
        reserve_deadline: Duration::from_millis(50),
18
20
        ..PoolRegistry::new(Duration::from_secs(300))
19
20
    }
20
20
}
21

            
22
/// Installs a user and drives its slot to ready, as a successful build would.
23
///
24
/// The slot handle is dropped, as `acquire_connection` drops its own when it
25
/// returns: holding one means "a caller is mid-acquire", which is a state the
26
/// registry deliberately refuses to evict under.
27
26
async fn ready_user(registry: &PoolRegistry) -> Uuid {
28
26
    let user = Uuid::new_v4();
29
26
    let slot = registry.install(user).expect("registry lock");
30
26
    let permit = registry.reserve().await.expect("under the ceiling");
31
26
    slot.set(Arc::new(TenantPool::new(lazy_pool(), permit)))
32
26
        .map_err(|_| "slot was already set")
33
26
        .expect("slot is freshly installed");
34
26
    user
35
26
}
36

            
37
/// The live pool handle for a resident tenant, as a connection would hold it.
38
10
fn pool_of(registry: &PoolRegistry, user: Uuid) -> Arc<TenantPool> {
39
10
    registry
40
10
        .pools
41
10
        .lock()
42
10
        .expect("registry lock")
43
10
        .get(&user)
44
10
        .and_then(|entry| entry.cell.get().cloned())
45
10
        .expect("tenant is resident and ready")
46
10
}
47

            
48
6
fn age(registry: &PoolRegistry, user: Uuid, by: Duration) {
49
6
    let mut pools = registry.pools.lock().expect("registry lock");
50
6
    let entry = pools.get_mut(&user).expect("user is resident");
51
6
    entry.last_used = Instant::now() - by;
52
6
}
53

            
54
2
fn age_check(registry: &PoolRegistry, user: Uuid, by: Duration) {
55
2
    let mut pools = registry.pools.lock().expect("registry lock");
56
2
    let entry = pools.get_mut(&user).expect("user is resident");
57
2
    entry.checked_at = Instant::now() - by;
58
2
}
59

            
60
6
fn resident(registry: &PoolRegistry) -> usize {
61
6
    registry.pools.lock().expect("registry lock").len()
62
6
}
63

            
64
#[tokio::test]
65
1
async fn touch_reports_a_resident_pool_and_refreshes_its_idle_clock() {
66
1
    let registry = impatient_registry();
67
1
    let user = ready_user(&registry).await;
68
1
    age(&registry, user, Duration::from_secs(120));
69

            
70
1
    assert!(registry.touch(user).expect("registry lock").is_some());
71

            
72
1
    let refreshed = registry
73
1
        .pools
74
1
        .lock()
75
1
        .expect("registry lock")
76
1
        .get(&user)
77
1
        .expect("still resident")
78
1
        .last_used;
79
1
    assert!(
80
1
        refreshed.elapsed() < Duration::from_secs(1),
81
1
        "touch must reset the idle clock"
82
1
    );
83
1
}
84

            
85
#[tokio::test]
86
1
async fn touch_reports_nothing_for_a_user_with_no_pool() {
87
1
    let registry = impatient_registry();
88

            
89
1
    assert!(
90
1
        registry
91
1
            .touch(Uuid::new_v4())
92
1
            .expect("registry lock")
93
1
            .is_none()
94
1
    );
95
1
}
96

            
97
#[tokio::test]
98
1
async fn touch_sweeps_pools_that_have_gone_idle() {
99
1
    let registry = impatient_registry();
100
1
    assert_eq!(registry.idle_timeout(), Duration::from_secs(300));
101

            
102
1
    let idle = ready_user(&registry).await;
103
1
    age(&registry, idle, Duration::from_secs(600));
104

            
105
1
    registry.touch(Uuid::new_v4()).expect("registry lock");
106

            
107
1
    assert_eq!(
108
1
        resident(&registry),
109
        0,
110
        "an expired pool must be swept, not left holding its permit"
111
    );
112
1
    assert_eq!(
113
1
        registry.residency.available_permits(),
114
1
        MAX_RESIDENT_POOLS,
115
1
        "sweeping must return the permit with the pool"
116
1
    );
117
1
}
118

            
119
#[tokio::test]
120
1
async fn install_hands_back_the_existing_slot_when_two_callers_race() {
121
1
    let registry = impatient_registry();
122
1
    let user = Uuid::new_v4();
123

            
124
1
    let first = registry.install(user).expect("registry lock");
125
1
    let second = registry.install(user).expect("registry lock");
126

            
127
1
    assert!(
128
1
        Arc::ptr_eq(&first, &second),
129
        "the loser of the race must reuse the winner's slot, not overwrite it"
130
    );
131
1
    assert_eq!(resident(&registry), 1, "a race must not leave two entries");
132
1
}
133

            
134
#[tokio::test]
135
1
async fn evict_lru_takes_the_oldest_ready_pool() {
136
1
    let registry = impatient_registry();
137
1
    let oldest = ready_user(&registry).await;
138
1
    let newest = ready_user(&registry).await;
139
1
    age(&registry, oldest, Duration::from_secs(60));
140

            
141
1
    assert!(evict_lru(
142
1
        &mut registry.pools.lock().expect("registry lock")
143
    ));
144

            
145
1
    let pools = registry.pools.lock().expect("registry lock");
146
1
    assert!(!pools.contains_key(&oldest));
147
1
    assert!(pools.contains_key(&newest));
148
1
}
149

            
150
/// The permit comes back only when a pool's last connection drops, so evicting
151
/// a pool somebody is using frees nothing: the newcomer waits out the whole
152
/// deadline and fails anyway, and the victim builds a SECOND pool beside its
153
/// live one. The oldest pool is passed over while it is in use.
154
#[tokio::test]
155
1
async fn evict_lru_passes_over_a_pool_that_is_still_in_use() {
156
1
    let registry = impatient_registry();
157
1
    let oldest = ready_user(&registry).await;
158
1
    let newest = ready_user(&registry).await;
159
1
    age(&registry, oldest, Duration::from_secs(60));
160

            
161
    // Stand in for a live `TenantConnection`, which anchors its pool by Arc.
162
1
    let in_use = pool_of(&registry, oldest);
163

            
164
1
    assert!(evict_lru(
165
1
        &mut registry.pools.lock().expect("registry lock")
166
    ));
167
    {
168
1
        let pools = registry.pools.lock().expect("registry lock");
169
1
        assert!(pools.contains_key(&oldest), "the busy pool must survive");
170
1
        assert!(!pools.contains_key(&newest), "the idle one goes instead");
171
    }
172

            
173
    // Once the connection lets go it becomes the obvious victim again.
174
1
    drop(in_use);
175
1
    assert!(evict_lru(
176
1
        &mut registry.pools.lock().expect("registry lock")
177
    ));
178
1
    assert!(
179
1
        !registry
180
1
            .pools
181
1
            .lock()
182
1
            .expect("registry lock")
183
1
            .contains_key(&oldest)
184
1
    );
185
1
}
186

            
187
#[tokio::test]
188
1
async fn evict_lru_skips_a_slot_that_is_still_building() {
189
1
    let registry = impatient_registry();
190
1
    let building = Uuid::new_v4();
191
1
    registry.install(building).expect("registry lock");
192

            
193
1
    assert!(!evict_lru(
194
1
        &mut registry.pools.lock().expect("registry lock")
195
1
    ));
196
1
    assert!(
197
1
        registry
198
1
            .pools
199
1
            .lock()
200
1
            .expect("registry lock")
201
1
            .contains_key(&building)
202
1
    );
203
1
}
204

            
205
#[tokio::test]
206
1
async fn a_dropped_pool_returns_its_permit() {
207
1
    let registry = impatient_registry();
208
1
    let user = ready_user(&registry).await;
209
1
    assert_eq!(
210
1
        registry.residency.available_permits(),
211
1
        MAX_RESIDENT_POOLS - 1
212
    );
213

            
214
1
    registry.pools.lock().expect("registry lock").remove(&user);
215

            
216
1
    assert_eq!(
217
1
        registry.residency.available_permits(),
218
1
        MAX_RESIDENT_POOLS,
219
1
        "the permit rides with the pool, so discarding one returns the other"
220
1
    );
221
1
}
222

            
223
#[tokio::test]
224
1
async fn an_evicted_pool_stays_usable_for_whoever_still_holds_it() {
225
1
    let registry = impatient_registry();
226
1
    let user = ready_user(&registry).await;
227
1
    let in_use = pool_of(&registry, user);
228

            
229
1
    registry.pools.lock().expect("registry lock").remove(&user);
230

            
231
1
    assert!(
232
1
        !in_use.pool().is_closed(),
233
        "eviction must un-register a pool, never close one out from under a request"
234
    );
235
1
    assert_eq!(
236
1
        registry.residency.available_permits(),
237
1
        MAX_RESIDENT_POOLS - 1,
238
1
        "the permit stays spent while the pool is still in use"
239
1
    );
240
1
}
241

            
242
#[tokio::test]
243
1
async fn forget_drops_only_the_slot_it_was_given() {
244
1
    let registry = impatient_registry();
245
1
    let user = Uuid::new_v4();
246
1
    let failed = registry.install(user).expect("registry lock");
247

            
248
    // The failing caller's slot is replaced by a fresh one before it cleans up.
249
1
    registry.forget(user, &failed).expect("registry lock");
250
1
    let replacement = registry.install(user).expect("registry lock");
251

            
252
1
    registry.forget(user, &failed).expect("registry lock");
253

            
254
1
    let pools = registry.pools.lock().expect("registry lock");
255
1
    let current = pools.get(&user).expect("replacement must survive");
256
1
    assert!(
257
1
        Arc::ptr_eq(&current.cell, &replacement),
258
1
        "a stale caller must not evict the generation that replaced it"
259
1
    );
260
1
}
261

            
262
#[tokio::test]
263
1
async fn residency_is_capped_and_reserving_past_it_evicts() {
264
1
    let registry = impatient_registry();
265
1
    let mut held = Vec::new();
266
1
    for _ in 0..MAX_RESIDENT_POOLS {
267
8
        held.push(ready_user(&registry).await);
268
    }
269

            
270
1
    assert_eq!(registry.residency.available_permits(), 0);
271

            
272
    // Release the local handles so an evicted pool can actually be dropped.
273
1
    held.clear();
274

            
275
1
    let permit = registry.reserve().await.expect("eviction frees a permit");
276

            
277
1
    assert_eq!(
278
1
        resident(&registry),
279
1
        MAX_RESIDENT_POOLS - 1,
280
        "reserving past the ceiling evicts exactly one pool"
281
    );
282
1
    drop(permit);
283
1
}
284

            
285
#[tokio::test]
286
1
async fn reserve_gives_up_rather_than_hanging_when_nothing_is_evictable() {
287
1
    let registry = impatient_registry();
288

            
289
    // Every slot is still building, so `evict_lru` refuses all of them.
290
1
    let mut permits = Vec::new();
291
1
    for _ in 0..MAX_RESIDENT_POOLS {
292
8
        registry.install(Uuid::new_v4()).expect("registry lock");
293
8
        permits.push(registry.reserve().await.expect("under the ceiling"));
294
1
    }
295
1

            
296
1
    assert!(
297
1
        matches!(registry.reserve().await, Err(DBError::PoolBudgetExhausted)),
298
1
        "a newcomer must fail fast, not park forever on a permit nobody will release"
299
1
    );
300
1
}
301

            
302
#[tokio::test]
303
1
async fn a_reservation_never_evicts_a_pool_that_is_in_use() {
304
1
    let registry = impatient_registry();
305
1
    let mut held = Vec::new();
306
1
    for _ in 0..MAX_RESIDENT_POOLS {
307
8
        let user = ready_user(&registry).await;
308
        // Anchoring the pool is what a live `TenantConnection` does.
309
8
        held.push(pool_of(&registry, user));
310
    }
311

            
312
    // Every pool is still checked out, so no eviction could return a permit
313
    // anyway: the reservation must time out having destroyed nothing. Taking a
314
    // victim here used to cost the tenant its pool AND leave the newcomer to
315
    // fail regardless, because the permit rides the pool until its last
316
    // connection drops.
317
1
    assert!(matches!(
318
1
        registry.reserve().await,
319
        Err(DBError::PoolBudgetExhausted)
320
    ));
321

            
322
1
    assert_eq!(
323
1
        resident(&registry),
324
1
        MAX_RESIDENT_POOLS,
325
1
        "a waiting newcomer must not close pools that are still in use"
326
1
    );
327
1
}
328

            
329
#[tokio::test]
330
1
async fn a_freshly_installed_tenant_is_not_immediately_rechecked() {
331
1
    let registry = impatient_registry();
332
1
    let user = Uuid::new_v4();
333
1
    registry.install(user).expect("registry lock");
334

            
335
1
    assert!(
336
1
        !registry
337
1
            .claim_recheck(user, Duration::from_secs(30))
338
1
            .expect("registry lock"),
339
1
        "install reads the directory already; re-reading at once is waste"
340
1
    );
341
1
}
342

            
343
#[tokio::test]
344
1
async fn only_one_caller_per_interval_gets_to_recheck() {
345
1
    let registry = impatient_registry();
346
1
    let user = ready_user(&registry).await;
347
1
    age_check(&registry, user, Duration::from_secs(60));
348

            
349
1
    assert!(
350
1
        registry
351
1
            .claim_recheck(user, Duration::from_secs(30))
352
1
            .expect("registry lock"),
353
        "a stale status must be re-read by someone"
354
    );
355

            
356
1
    for _ in 0..5 {
357
5
        assert!(
358
5
            !registry
359
5
                .claim_recheck(user, Duration::from_secs(30))
360
5
                .expect("registry lock"),
361
1
            "concurrent acquires must not each query the admin pool"
362
1
        );
363
1
    }
364
1
}
365

            
366
#[tokio::test]
367
1
async fn a_failed_recheck_is_given_back_so_the_next_caller_retries() {
368
1
    let registry = impatient_registry();
369
1
    let user = ready_user(&registry).await;
370
1
    age_check(&registry, user, Duration::from_secs(60));
371

            
372
1
    assert!(
373
1
        registry
374
1
            .claim_recheck(user, Duration::from_secs(30))
375
1
            .expect("registry lock")
376
    );
377
1
    registry
378
1
        .release_recheck(user, Duration::from_secs(30))
379
1
        .expect("registry lock");
380

            
381
1
    assert!(
382
1
        registry
383
1
            .claim_recheck(user, Duration::from_secs(30))
384
1
            .expect("registry lock"),
385
1
        "a read that failed must not buy silence for the rest of the interval"
386
1
    );
387
1
}
388

            
389
#[tokio::test]
390
1
async fn an_unknown_tenant_is_always_due() {
391
1
    let registry = impatient_registry();
392

            
393
1
    assert!(
394
1
        registry
395
1
            .claim_recheck(Uuid::new_v4(), Duration::from_secs(30))
396
1
            .expect("registry lock"),
397
1
        "a tenant with no pool is about to build one, which reads the directory"
398
1
    );
399
1
}
400

            
401
#[tokio::test]
402
1
async fn releasing_an_unknown_tenant_is_harmless() {
403
1
    let registry = impatient_registry();
404

            
405
1
    registry
406
1
        .release_recheck(Uuid::new_v4(), Duration::from_secs(30))
407
1
        .expect("a tenant evicted mid-check must not fail the release");
408
1
}
409

            
410
/// A slot whose build was cancelled leaves an empty cell nobody holds. It has
411
/// no permit to give back, so eviction rightly passes over it — but the sweep
412
/// must still reclaim it, or the registry grows by one entry per cancelled
413
/// request and never shrinks.
414
#[tokio::test]
415
1
async fn the_sweep_reclaims_a_slot_whose_build_was_abandoned() {
416
1
    let registry = impatient_registry();
417
1
    let abandoned = Uuid::new_v4();
418
1
    registry.install(abandoned).expect("registry lock");
419
1
    age(&registry, abandoned, Duration::from_secs(600));
420

            
421
1
    assert!(
422
1
        !evict_lru(&mut registry.pools.lock().expect("registry lock")),
423
        "an empty slot holds no permit, so evicting it frees nothing"
424
    );
425

            
426
1
    registry.touch(Uuid::new_v4()).expect("registry lock");
427
1
    assert_eq!(
428
1
        resident(&registry),
429
1
        0,
430
1
        "an abandoned, unheld slot must not outlive the idle timeout"
431
1
    );
432
1
}
433

            
434
/// The other half: a slot somebody is still building must survive the sweep.
435
#[tokio::test]
436
1
async fn the_sweep_leaves_a_build_in_flight_alone() {
437
1
    let registry = impatient_registry();
438
1
    let building = Uuid::new_v4();
439
1
    let _held = registry.install(building).expect("registry lock");
440
1
    age(&registry, building, Duration::from_secs(600));
441

            
442
1
    registry.touch(Uuid::new_v4()).expect("registry lock");
443
1
    assert_eq!(
444
1
        resident(&registry),
445
1
        1,
446
1
        "the caller still building holds the slot"
447
1
    );
448
1
}