ref: remove transactions

This commit is contained in:
Steve Korshakov 2025-07-26 02:00:25 -07:00
parent ae95f70372
commit 6c0428c9d3
3 changed files with 154 additions and 247 deletions

View File

@ -8,6 +8,9 @@ import * as tweetnacl from "tweetnacl";
import { db } from "@/storage/db"; import { db } from "@/storage/db";
import { Account, Update } from "@prisma/client"; import { Account, Update } from "@prisma/client";
import { onShutdown } from "@/utils/shutdown"; import { onShutdown } from "@/utils/shutdown";
import { allocateSessionSeq, allocateUserSeq } from "@/services/seq";
import { randomKey } from "@/utils/randomKey";
import { randomKeyNaked } from "@/utils/randomKeyNaked";
// Connection metadata types // Connection metadata types
interface SessionScopedConnection { interface SessionScopedConnection {
@ -916,114 +919,57 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
c: message c: message
}; };
// Start transaction to ensure consistency // Resolve seq
const result = await db.$transaction(async (tx) => { const updSeq = await allocateUserSeq(userId);
const msgSeq = await allocateSessionSeq(sid);
// Get user for update (lock account first to prevent deadlocks) // Check if message already exists
const user = await tx.account.findUnique({ if (useLocalId) {
where: { id: userId } const existing = await db.sessionMessage.findFirst({
where: { sessionId: sid, localId: useLocalId }
}); });
if (existing) {
if (!user) { return { msg: existing, update: null };
throw new Error('User not found');
} }
}
// Verify session belongs to user and lock it // Create message
const session = await tx.session.findFirst({ const msg = await db.sessionMessage.create({
where: { data: {
id: sid, sessionId: sid,
accountId: userId seq: msgSeq,
} content: msgContent,
}); localId: useLocalId
if (!session) {
throw new Error('Session not found');
} }
// Get next sequence numbers
const msgSeq = session.seq + 1;
const updSeq = user.seq + 1;
if (useLocalId) {
const existing = await tx.sessionMessage.findFirst({
where: { sessionId: sid, localId: useLocalId }
});
if (existing) {
return { msg: existing, update: null };
}
}
// Create message
const msg = await tx.sessionMessage.create({
data: {
sessionId: sid,
seq: msgSeq,
content: msgContent,
localId: useLocalId
}
});
// Create update
const updContent: PrismaJson.UpdateBody = {
t: 'new-message',
sid: sid,
message: {
id: msg.id,
seq: msg.seq,
content: msgContent,
localId: useLocalId,
createdAt: msg.createdAt.getTime(),
updatedAt: msg.updatedAt.getTime()
}
};
const update = await tx.update.create({
data: {
accountId: userId,
seq: updSeq,
content: updContent
}
});
// Update sequences
await tx.session.update({
where: { id: sid },
data: { seq: msgSeq }
});
await tx.account.update({
where: { id: userId },
data: { seq: updSeq }
});
return { msg, update };
}).catch((error) => {
if (error.message === 'Session not found') {
return null;
}
throw error;
}); });
// If no update, we're done // Create update
if (!result) { const update: PrismaJson.UpdateBody = {
return; t: 'new-message',
} sid: sid,
message: {
id: msg.id,
seq: msg.seq,
content: msgContent,
localId: useLocalId,
createdAt: msg.createdAt.getTime(),
updatedAt: msg.updatedAt.getTime()
}
};
// Emit update to relevant clients // Emit update to relevant clients
if (result.update) { emitUpdateToInterestedClients({
emitUpdateToInterestedClients({ event: 'update',
event: 'update', userId,
userId, sessionId: sid,
sessionId: sid, payload: {
payload: { id: randomKeyNaked(12),
id: result.update.id, seq: updSeq,
seq: result.update.seq, body: update,
body: result.update.content, createdAt: Date.now()
createdAt: result.update.createdAt.getTime() },
}, skipSenderConnection: connection
skipSenderConnection: connection });
});
}
}); });
socket.on('update-metadata', async (data: any, callback: (response: any) => void) => { socket.on('update-metadata', async (data: any, callback: (response: any) => void) => {
@ -1037,88 +983,57 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
return; return;
} }
// Start transaction to ensure consistency // Resolve session
const result = await db.$transaction(async (tx) => { const session = await db.session.findUnique({
// Get user for update (lock account first to prevent deadlocks) where: { id: sid, accountId: userId }
const user = await tx.account.findUnique({
where: { id: userId }
});
if (!user) {
callback({ result: 'error' });
return null;
}
// Verify session belongs to user and lock it
const session = await tx.session.findFirst({
where: {
id: sid,
accountId: userId
}
});
if (!session) {
callback({ result: 'error' });
return null;
}
// Check version
if (session.metadataVersion !== expectedVersion) {
callback({ result: 'version-mismatch', version: session.metadataVersion, metadata: session.metadata });
return null;
}
// Get next sequence number
const updSeq = user.seq + 1;
const newMetadataVersion = session.metadataVersion + 1;
// Update session metadata
await tx.session.update({
where: { id: sid },
data: {
metadata: metadata,
metadataVersion: newMetadataVersion
}
});
// Create update
const updContent: PrismaJson.UpdateBody = {
t: 'update-session',
id: sid,
metadata: {
value: metadata,
version: newMetadataVersion
}
};
const update = await tx.update.create({
data: {
accountId: userId,
seq: updSeq,
content: updContent
}
});
// Update user sequence
await tx.account.update({
where: { id: userId },
data: { seq: updSeq }
});
return { update, newMetadataVersion };
}); });
if (!result) { if (!session) {
return; return;
} }
// Emit update to connected sockets // Check version
if (session.metadataVersion !== expectedVersion) {
callback({ result: 'version-mismatch', version: session.metadataVersion, metadata: session.metadata });
return null;
}
// Update metadata
const { count } = await db.session.updateMany({
where: { id: sid, metadataVersion: expectedVersion },
data: {
metadata: metadata,
metadataVersion: expectedVersion + 1
}
});
if (count === 0) {
callback({ result: 'version-mismatch', version: session.metadataVersion, metadata: session.metadata });
return null;
}
// Generate update
const updSeq = await allocateUserSeq(userId);
const updContent: PrismaJson.UpdateBody = {
t: 'update-session',
id: sid,
metadata: {
value: metadata,
version: expectedVersion + 1
}
};
emitUpdateToInterestedClients({ emitUpdateToInterestedClients({
event: 'update', event: 'update',
userId, userId,
sessionId: sid, sessionId: sid,
payload: result.update payload: {
id: randomKeyNaked(12),
seq: updSeq,
body: updContent,
createdAt: Date.now()
}
}); });
// Send success response with new version via callback // Send success response with new version via callback
callback({ result: 'success', version: result.newMetadataVersion, metadata: metadata }); callback({ result: 'success', version: expectedVersion + 1, metadata: metadata });
}); });
@ -1133,93 +1048,63 @@ export async function startApi(): Promise<{ app: FastifyInstance; io: Server }>
return; return;
} }
// Start transaction to ensure consistency // Resolve session
const result = await db.$transaction(async (tx) => { const session = await db.session.findUnique({
// Get user for update (lock account first to prevent deadlocks) where: {
const user = await tx.account.findUnique({
where: { id: userId }
});
if (!user) {
callback({ result: 'error' });
return null;
}
// Verify session belongs to user and lock it
const session = await tx.session.findFirst({
where: {
id: sid,
accountId: userId
}
});
if (!session) {
callback({ result: 'error' });
return null;
}
// Check version
if (session.agentStateVersion !== expectedVersion) {
callback({ result: 'version-mismatch', version: session.agentStateVersion, agentState: session.agentState });
return null;
}
// Get next sequence number
const updSeq = user.seq + 1;
const newAgentStateVersion = session.agentStateVersion + 1;
// Update session agent state
await tx.session.update({
where: { id: sid },
data: {
agentState: agentState,
agentStateVersion: newAgentStateVersion
}
});
// Create update
const updContent: PrismaJson.UpdateBody = {
t: 'update-session',
id: sid, id: sid,
agentState: { accountId: userId
value: agentState, }
version: newAgentStateVersion
}
};
const update = await tx.update.create({
data: {
accountId: userId,
seq: updSeq,
content: updContent
}
});
// Update user sequence
await tx.account.update({
where: { id: userId },
data: { seq: updSeq }
});
return { update, newAgentStateVersion };
}); });
if (!result) { if (!session) {
return; callback({ result: 'error' });
return null;
} }
// Check version
if (session.agentStateVersion !== expectedVersion) {
callback({ result: 'version-mismatch', version: session.agentStateVersion, agentState: session.agentState });
return null;
}
// Update agent state
const { count } = await db.session.updateMany({
where: { id: sid, agentStateVersion: expectedVersion },
data: {
agentState: agentState,
agentStateVersion: expectedVersion + 1
}
});
if (count === 0) {
callback({ result: 'version-mismatch', version: session.agentStateVersion, agentState: session.agentState });
return null;
}
// Generate update
const updSeq = await allocateUserSeq(userId);
const updContent: PrismaJson.UpdateBody = {
t: 'update-session',
id: sid,
agentState: {
value: agentState,
version: expectedVersion + 1
}
};
// Emit update to connected sockets // Emit update to connected sockets
emitUpdateToInterestedClients({ emitUpdateToInterestedClients({
event: 'update', event: 'update',
userId, userId,
sessionId: sid, sessionId: sid,
payload: { payload: {
id: result.update.id, id: randomKeyNaked(12),
seq: result.update.seq, seq: updSeq,
body: result.update.content, body: updContent,
createdAt: result.update.createdAt.getTime() createdAt: Date.now()
} }
}); });
// Send success response with new version via callback // Send success response with new version via callback
callback({ result: 'success', version: result.newAgentStateVersion, agentState: agentState }); callback({ result: 'success', version: expectedVersion + 1, agentState: agentState });
}); });
// RPC register - Register this socket as a listener for an RPC method // RPC register - Register this socket as a listener for an RPC method

View File

@ -1,11 +1,12 @@
import { pubsub } from "@/services/pubsub"; import { pubsub } from "@/services/pubsub";
import { db } from "@/storage/db"; import { db } from "@/storage/db";
import { backoff, delay } from "@/utils/delay"; import { delay } from "@/utils/delay";
import { forever } from "@/utils/forever";
import { shutdownSignal } from "@/utils/shutdown";
export function startTimeout() { export function startTimeout() {
backoff(async () => { forever('session-timeout', async () => {
while (true) { while (true) {
// Find timed out sessions // Find timed out sessions
const sessions = await db.session.findMany({ const sessions = await db.session.findMany({
where: { where: {
@ -30,7 +31,7 @@ export function startTimeout() {
} }
// Wait for 1 minute // Wait for 1 minute
await delay(1000 * 60); await delay(1000 * 60, shutdownSignal);
} }
}); });
} }

21
sources/services/seq.ts Normal file
View File

@ -0,0 +1,21 @@
import { db } from "@/storage/db";
export async function allocateUserSeq(accountId: string) {
const user = await db.account.update({
where: { id: accountId },
select: { seq: true },
data: { seq: { increment: 1 } }
});
const seq = user.seq;
return seq;
}
export async function allocateSessionSeq(sessionId: string) {
const session = await db.session.update({
where: { id: sessionId },
select: { seq: true },
data: { seq: { increment: 1 } }
});
const seq = session.seq;
return seq;
}