Merge pull request #10424 from manaflow-ai/feat/coderouter-status-session-counts
Expose per-account session counts and cooldowns in account listing
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