blob: 64bf2e63b0e249c157f67d23ded25870d0239318 [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SessionPool } from "../../src/client/SessionPool";
/**
* Test pool that hands out lightweight fake sessions (no real IoTDB
* connection) so getSession()/releaseSession() lifecycle bookkeeping can be
* exercised deterministically. `createGate`, when set, lets a test hold a
* createSession() call open to interleave a concurrent release.
*/
class TestPool extends SessionPool {
public createGate: Promise<void> | null = null;
private counter = 0;
protected async createPoolSession(): Promise<any> {
if (this.createGate) {
await this.createGate;
}
const id = ++this.counter;
const session: any = {
id,
_closed: false,
// When set to a promise, close() parks on it while isOpen() keeps
// returning true — lets a test hold a close open to probe TOCTOU.
closeGate: null as Promise<void> | null,
isOpen: () => !session._closed,
close: async () => {
if (session.closeGate) {
await session.closeGate;
}
session._closed = true;
},
};
return session;
}
pooledFor(session: any): any {
return (this as any).pool.find((ps: any) => ps.session === session);
}
runCleanup(): Promise<void> {
return (this as any).cleanupIdleSessions();
}
idleSessionObjects(): any[] {
return (this as any).idleSessions.toArray().map((ps: any) => ps.session);
}
activeSessionObjects(): any[] {
return Array.from((this as any).activeSessions as Set<any>).map(
(ps: any) => ps.session,
);
}
}
function newPool(overrides: Record<string, unknown>): TestPool {
return new TestPool({
host: "localhost",
port: 6667,
minPoolSize: 0,
...overrides,
} as any);
}
describe("BaseSessionPool lifecycle", () => {
it("does not leak a released session to a timed-out waiter (no starvation)", async () => {
const pool = newPool({ maxPoolSize: 1, waitTimeout: 50 });
const s1 = await pool.getSession(); // creates S1; pool is now full (1/1)
// Pool is full, so this acquisition waits and then times out.
await expect(pool.getSession()).rejects.toThrow(/Timeout/);
// Releasing S1 must return it to the pool, not hand it to the dead waiter
// (which would mark S1 active-but-held-by-nobody and starve the pool).
pool.releaseSession(s1);
// S1 must be acquirable again. On the buggy code this getSession() starves
// and rejects with a timeout.
const s2 = await pool.getSession();
expect(s2).toBe(s1);
await pool.close();
});
it("create-branch removes the new session, not a concurrently-released idle one", async () => {
const pool = newPool({ maxPoolSize: 3, waitTimeout: 1000 });
const s1 = await pool.getSession(); // create S1; active, idle=[]
// Hold the next createSession() open so we can release S1 mid-flight.
let openGate!: () => void;
pool.createGate = new Promise<void>((resolve) => {
openGate = resolve;
});
const acquireA = pool.getSession(); // enters create-branch, awaits the gate
// One yield is enough: getSession() runs synchronously up to the first
// await (the createSession call), so after this tick acquireA is parked on
// the gate and the release below interleaves before it resumes.
await new Promise((r) => setImmediate(r));
// Concurrent release pushes S1 to the FRONT of idle while A is awaiting.
pool.releaseSession(s1); // idle=[S1]
openGate(); // A's createSession resolves -> pushes S2 -> idle=[S1,S2]
const s2 = await acquireA;
expect(s2).not.toBe(s1);
// S1 must remain the idle session; S2 was handed to A and must not also
// linger in idle (the blind shift() bug evicted S1 and left S2 in idle).
expect(pool.idleSessionObjects()).toContain(s1);
expect(pool.idleSessionObjects()).not.toContain(s2);
expect(pool.activeSessionObjects()).toContain(s2);
await pool.close();
});
it("marks a reused idle session as inUse", async () => {
const pool = newPool({ maxPoolSize: 2 });
const s1 = await pool.getSession();
pool.releaseSession(s1); // back to idle, inUse=false
const s2 = await pool.getSession(); // idle-reuse branch
expect(s2).toBe(s1);
// The reused session is handed to a caller, so it must be inUse; the
// idle-reuse branch used to skip this, leaving it false while active.
expect(pool.pooledFor(s2).inUse).toBe(true);
await pool.close();
});
it("cleanupIdleSessions never shrinks the pool below minPoolSize", async () => {
const pool = newPool({ maxPoolSize: 5, minPoolSize: 1, maxIdleTime: 1 });
const a = await pool.getSession();
const b = await pool.getSession();
const c = await pool.getSession();
pool.releaseSession(a);
pool.releaseSession(b);
pool.releaseSession(c);
// Make every session look long-idle so all three qualify for cleanup.
for (const ps of (pool as any).pool) {
ps.lastUsed = 0;
}
await pool.runCleanup();
// Must retain minPoolSize; the buggy guard (constant pre-cleanup size)
// removed all three and collapsed the pool to 0.
expect(pool.getPoolSize()).toBe(1);
await pool.close();
});
it("cleanupIdleSessions does not hand out a session that is being closed", async () => {
// minPoolSize=1 (0 would coerce to 1 anyway), 2 idle sessions so cleanup
// removes exactly one (the first-queued, s1) and keeps one warm.
const pool = newPool({ maxPoolSize: 3, minPoolSize: 1, maxIdleTime: 1 });
const s1 = await pool.getSession();
const s2 = await pool.getSession();
pool.releaseSession(s1); // idle=[s1]
pool.releaseSession(s2); // idle=[s1, s2]
for (const ps of (pool as any).pool) {
ps.lastUsed = 0; // both qualify as long-idle
}
// Gate s1's close so it stays "closing" (isOpen()===true) across an await.
let openClose!: () => void;
(s1 as any).closeGate = new Promise<void>((r) => {
openClose = r;
});
const cleanup = pool.runCleanup(); // removes s1 (down to minSize=1), gated close
await new Promise((r) => setImmediate(r)); // let cleanup reach the close await
// A concurrent acquire must NOT receive the session being closed. On the
// buggy close-then-splice, s1 stays in idle during close() and shift()
// hands it out.
const acquired = await pool.getSession();
expect(acquired).not.toBe(s1);
openClose(); // let the close finish
await cleanup;
await pool.close();
});
});