Merge pull request 'perf: gateway + DB + frontend optimizations (P8-003)' (#211) from feat/p8-003-performance into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Reviewed-on: mosaic/mosaic-stack#211
This commit was merged in pull request #211.
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import { eq, type Db, conversations, messages } from '@mosaic/db';
|
||||
import { eq, asc, desc, type Db, conversations, messages } from '@mosaic/db';
|
||||
|
||||
/** Maximum number of conversations returned per list query. */
|
||||
const MAX_CONVERSATIONS = 200;
|
||||
/** Maximum number of messages returned per conversation history query. */
|
||||
const MAX_MESSAGES = 500;
|
||||
|
||||
export type Conversation = typeof conversations.$inferSelect;
|
||||
export type NewConversation = typeof conversations.$inferInsert;
|
||||
@@ -8,7 +13,12 @@ export type NewMessage = typeof messages.$inferInsert;
|
||||
export function createConversationsRepo(db: Db) {
|
||||
return {
|
||||
async findAll(userId: string): Promise<Conversation[]> {
|
||||
return db.select().from(conversations).where(eq(conversations.userId, userId));
|
||||
return db
|
||||
.select()
|
||||
.from(conversations)
|
||||
.where(eq(conversations.userId, userId))
|
||||
.orderBy(desc(conversations.updatedAt))
|
||||
.limit(MAX_CONVERSATIONS);
|
||||
},
|
||||
|
||||
async findById(id: string): Promise<Conversation | undefined> {
|
||||
@@ -36,7 +46,12 @@ export function createConversationsRepo(db: Db) {
|
||||
},
|
||||
|
||||
async findMessages(conversationId: string): Promise<Message[]> {
|
||||
return db.select().from(messages).where(eq(messages.conversationId, conversationId));
|
||||
return db
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(eq(messages.conversationId, conversationId))
|
||||
.orderBy(asc(messages.createdAt))
|
||||
.limit(MAX_MESSAGES);
|
||||
},
|
||||
|
||||
async addMessage(data: NewMessage): Promise<Message> {
|
||||
|
||||
14
packages/db/drizzle/0003_p8003_perf_indexes.sql
Normal file
14
packages/db/drizzle/0003_p8003_perf_indexes.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
DROP INDEX "agent_logs_session_id_idx";--> statement-breakpoint
|
||||
DROP INDEX "agent_logs_tier_idx";--> statement-breakpoint
|
||||
DROP INDEX "agent_logs_created_at_idx";--> statement-breakpoint
|
||||
DROP INDEX "conversations_user_id_idx";--> statement-breakpoint
|
||||
DROP INDEX "conversations_archived_idx";--> statement-breakpoint
|
||||
DROP INDEX "preferences_user_key_idx";--> statement-breakpoint
|
||||
CREATE INDEX "accounts_provider_account_idx" ON "accounts" USING btree ("provider_id","account_id");--> statement-breakpoint
|
||||
CREATE INDEX "accounts_user_id_idx" ON "accounts" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "agent_logs_session_tier_idx" ON "agent_logs" USING btree ("session_id","tier");--> statement-breakpoint
|
||||
CREATE INDEX "agent_logs_tier_created_at_idx" ON "agent_logs" USING btree ("tier","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "conversations_user_archived_idx" ON "conversations" USING btree ("user_id","archived");--> statement-breakpoint
|
||||
CREATE INDEX "sessions_user_id_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "sessions_expires_at_idx" ON "sessions" USING btree ("expires_at");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "preferences_user_key_idx" ON "preferences" USING btree ("user_id","key");
|
||||
2491
packages/db/drizzle/meta/0003_snapshot.json
Normal file
2491
packages/db/drizzle/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,13 @@
|
||||
"when": 1773625181629,
|
||||
"tag": "0002_nebulous_mimic",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1773887085247,
|
||||
"tag": "0003_p8003_perf_indexes",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,15 @@ export interface DbHandle {
|
||||
|
||||
export function createDb(url?: string): DbHandle {
|
||||
const connectionString = url ?? process.env['DATABASE_URL'] ?? DEFAULT_DATABASE_URL;
|
||||
const sql = postgres(connectionString);
|
||||
const sql = postgres(connectionString, {
|
||||
// Pool sizing: allow up to 20 concurrent connections per gateway instance.
|
||||
// Each NestJS module (brain, preferences, memory, coord) shares this pool.
|
||||
max: Number(process.env['DB_POOL_MAX'] ?? 20),
|
||||
// Recycle idle connections after 30 s to avoid stale TCP state.
|
||||
idle_timeout: Number(process.env['DB_IDLE_TIMEOUT'] ?? 30),
|
||||
// Fail fast (5 s) on connection problems rather than hanging indefinitely.
|
||||
connect_timeout: Number(process.env['DB_CONNECT_TIMEOUT'] ?? 5),
|
||||
});
|
||||
const db = drizzle(sql, { schema });
|
||||
return { db, close: () => sql.end() };
|
||||
}
|
||||
|
||||
@@ -33,36 +33,54 @@ export const users = pgTable('users', {
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const sessions = pgTable('sessions', {
|
||||
id: text('id').primaryKey(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
export const sessions = pgTable(
|
||||
'sessions',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
// Auth hot path: look up all sessions for a user (BetterAuth session list).
|
||||
index('sessions_user_id_idx').on(t.userId),
|
||||
// Session expiry cleanup queries.
|
||||
index('sessions_expires_at_idx').on(t.expiresAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const accounts = pgTable('accounts', {
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
|
||||
scope: text('scope'),
|
||||
password: text('password'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
export const accounts = pgTable(
|
||||
'accounts',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
|
||||
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
|
||||
scope: text('scope'),
|
||||
password: text('password'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
// BetterAuth looks up accounts by (provider_id, account_id) on OAuth callback.
|
||||
index('accounts_provider_account_idx').on(t.providerId, t.accountId),
|
||||
// Also used in session validation to find linked accounts for a user.
|
||||
index('accounts_user_id_idx').on(t.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export const verifications = pgTable('verifications', {
|
||||
id: text('id').primaryKey(),
|
||||
@@ -306,10 +324,10 @@ export const conversations = pgTable(
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
index('conversations_user_id_idx').on(t.userId),
|
||||
// Compound index for the most common query: conversations for a user filtered by archived.
|
||||
index('conversations_user_archived_idx').on(t.userId, t.archived),
|
||||
index('conversations_project_id_idx').on(t.projectId),
|
||||
index('conversations_agent_id_idx').on(t.agentId),
|
||||
index('conversations_archived_idx').on(t.archived),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -369,7 +387,8 @@ export const preferences = pgTable(
|
||||
},
|
||||
(t) => [
|
||||
index('preferences_user_id_idx').on(t.userId),
|
||||
index('preferences_user_key_idx').on(t.userId, t.key),
|
||||
// Unique constraint enables single-round-trip INSERT … ON CONFLICT DO UPDATE.
|
||||
uniqueIndex('preferences_user_key_idx').on(t.userId, t.key),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -431,10 +450,11 @@ export const agentLogs = pgTable(
|
||||
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [
|
||||
index('agent_logs_session_id_idx').on(t.sessionId),
|
||||
// Compound index for session log queries (most common: session + tier filter).
|
||||
index('agent_logs_session_tier_idx').on(t.sessionId, t.tier),
|
||||
index('agent_logs_user_id_idx').on(t.userId),
|
||||
index('agent_logs_tier_idx').on(t.tier),
|
||||
index('agent_logs_created_at_idx').on(t.createdAt),
|
||||
// Used by summarization cron to find hot logs older than a cutoff.
|
||||
index('agent_logs_tier_created_at_idx').on(t.tier, t.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user