Expose per-account session counts and cooldowns in account listing
The status endpoint now reports, per account, how many sessions are bound with traffic inside the routing load window, plus the account's cooldown deadline. The coderouter CLI uses both to make sticky routing and rate-limit cooldowns visible. The session-count read is display only and fails to zero so it can never take the status endpoint down.
This commit is contained in:
@@ -151,22 +151,56 @@ export async function deleteAccount(input: {
|
||||
export async function listAccounts(
|
||||
teamId: string,
|
||||
): Promise<readonly CodeRouterAccountSummary[]> {
|
||||
return await cloudDb()
|
||||
.select({
|
||||
id: coderouterAccounts.id,
|
||||
provider: coderouterAccounts.provider,
|
||||
providerAccountId: coderouterAccounts.providerAccountId,
|
||||
label: coderouterAccounts.label,
|
||||
state: coderouterAccounts.state,
|
||||
credentialExpiresAt: coderouterAccounts.credentialExpiresAt,
|
||||
lastFailureCode: coderouterAccounts.lastFailureCode,
|
||||
})
|
||||
.from(coderouterAccounts)
|
||||
.where(eq(coderouterAccounts.teamId, teamId))
|
||||
.then((rows) => rows.map((row) => ({
|
||||
...row,
|
||||
credentialExpiresAt: row.credentialExpiresAt?.toISOString() ?? null,
|
||||
})));
|
||||
const [rows, sessionCounts] = await Promise.all([
|
||||
cloudDb()
|
||||
.select({
|
||||
id: coderouterAccounts.id,
|
||||
provider: coderouterAccounts.provider,
|
||||
providerAccountId: coderouterAccounts.providerAccountId,
|
||||
label: coderouterAccounts.label,
|
||||
state: coderouterAccounts.state,
|
||||
credentialExpiresAt: coderouterAccounts.credentialExpiresAt,
|
||||
lastFailureCode: coderouterAccounts.lastFailureCode,
|
||||
cooldownUntil: coderouterAccounts.cooldownUntil,
|
||||
})
|
||||
.from(coderouterAccounts)
|
||||
.where(eq(coderouterAccounts.teamId, teamId)),
|
||||
countActiveSessionsByAccount(teamId),
|
||||
]);
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
credentialExpiresAt: row.credentialExpiresAt?.toISOString() ?? null,
|
||||
cooldownUntil: row.cooldownUntil?.toISOString() ?? null,
|
||||
activeSessions: sessionCounts.get(row.id) ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sessions bound per account with traffic inside the load window. Display
|
||||
* only: a failure here must never take the status endpoint down.
|
||||
*/
|
||||
async function countActiveSessionsByAccount(
|
||||
teamId: string,
|
||||
): Promise<ReadonlyMap<string, number>> {
|
||||
try {
|
||||
const rows = await cloudDb()
|
||||
.select({
|
||||
accountId: coderouterSessionAccounts.accountId,
|
||||
sessions: sql<number>`count(*)::int`,
|
||||
})
|
||||
.from(coderouterSessionAccounts)
|
||||
.where(and(
|
||||
eq(coderouterSessionAccounts.teamId, teamId),
|
||||
gt(
|
||||
coderouterSessionAccounts.lastSeenAt,
|
||||
sql`now() - interval '${sql.raw(SESSION_BINDING_LOAD_WINDOW)}'`,
|
||||
),
|
||||
))
|
||||
.groupBy(coderouterSessionAccounts.accountId);
|
||||
return new Map(rows.map((row) => [row.accountId, Number(row.sessions)]));
|
||||
} catch {
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
export async function listCoderouterTeamIds(): Promise<readonly string[]> {
|
||||
|
||||
@@ -41,4 +41,7 @@ export type CodeRouterAccountSummary = {
|
||||
readonly state: "active" | "refreshing" | "expired" | "broken";
|
||||
readonly credentialExpiresAt: string | null;
|
||||
readonly lastFailureCode: string | null;
|
||||
readonly cooldownUntil: string | null;
|
||||
/** Sessions bound to this account with traffic in the recent window. */
|
||||
readonly activeSessions: number;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
||||
import { randomUUID } from "node:crypto";
|
||||
import postgres, { type Sql } from "postgres";
|
||||
import { closeCloudDbForTests } from "../db/client";
|
||||
import { listAccounts } from "../services/coderouter/repository";
|
||||
import {
|
||||
bindSessionAccount,
|
||||
claimAccountForPlacement,
|
||||
@@ -213,6 +214,27 @@ describe("coderouter routing db behavior", () => {
|
||||
expect(during?.sticky).toBe(true);
|
||||
});
|
||||
|
||||
dbTest("account listing reports active session counts and cooldowns", async () => {
|
||||
if (!sql) throw new Error("no sql client");
|
||||
const accounts = await insertAccounts(2);
|
||||
await bindSessionAccount(TEAM, "codex", "fresh-session-1", accounts[0] ?? "");
|
||||
await bindSessionAccount(TEAM, "codex", "fresh-session-2", accounts[0] ?? "");
|
||||
await bindSessionAccount(TEAM, "codex", "stale-session", accounts[1] ?? "");
|
||||
await sql`
|
||||
update coderouter_session_accounts
|
||||
set last_seen_at = now() - interval '7 hours'
|
||||
where session_key = 'stale-session'
|
||||
`;
|
||||
await markAccountCooldown(accounts[1] ?? "", 60_000);
|
||||
const listed = await listAccounts(TEAM);
|
||||
const byId = new Map(listed.map((account) => [account.id, account]));
|
||||
expect(byId.get(accounts[0] ?? "")?.activeSessions).toBe(2);
|
||||
expect(byId.get(accounts[0] ?? "")?.cooldownUntil).toBeNull();
|
||||
// A binding idle beyond the load window no longer counts.
|
||||
expect(byId.get(accounts[1] ?? "")?.activeSessions).toBe(0);
|
||||
expect(byId.get(accounts[1] ?? "")?.cooldownUntil).not.toBeNull();
|
||||
});
|
||||
|
||||
dbTest("routes without stickiness while the session table is missing", async () => {
|
||||
if (!sql) throw new Error("no sql client");
|
||||
await insertAccounts(2);
|
||||
|
||||
Reference in New Issue
Block a user