fix: memory improvements

- SQLite: enable WAL mode and PRAGMA performance settings

- ws.ts (public + private): fix clientConfigVersions memory leak

- internal server: add rate limiting and request timeouts

- audit log: fix flush re-queue feedback loop

- memory: add monitoring instrumentation

- security: remove debug log of full request body
This commit is contained in:
Josh Voyles
2026-05-02 07:37:18 -04:00
parent bb5853827b
commit d6abe83fdc
9 changed files with 112 additions and 10 deletions

View File

@@ -41,7 +41,7 @@ export async function exchangeSession(
res: Response,
next: NextFunction
): Promise<any> {
logger.debug("Exchange session: Badger sent", req.body);
logger.debug("Exchange session: Badger request received");
const parsedBody = exchangeSessionBodySchema.safeParse(req.body);

View File

@@ -84,14 +84,14 @@ async function flushAuditLogs() {
logger.debug(`Flushed ${logsToWrite.length} audit logs to database`);
} catch (error) {
logger.error("Error flushing audit logs:", error);
// On transaction error, put logs back at the front of the buffer to retry
// but only if buffer isn't too large
if (auditLogBuffer.length < MAX_BUFFER_SIZE - logsToWrite.length) {
auditLogBuffer.unshift(...logsToWrite);
logger.info(`Re-queued ${logsToWrite.length} audit logs for retry`);
} else {
logger.error(`Buffer full, dropped ${logsToWrite.length} audit logs`);
}
// On transaction error, drop the logs rather than re-queuing them.
// The previous re-queue approach created a positive feedback loop:
// failed flush → re-queue → larger next flush → longer DB lock →
// higher chance of next failure → repeat. This caused unbounded
// memory growth on SQLite where write contention is common.
// Audit logs are best-effort telemetry — losing a batch on error
// is acceptable; leaking memory until the process crashes is not.
logger.warn(`Dropped ${logsToWrite.length} audit logs after flush failure`);
} finally {
isFlushInProgress = false;
// If buffer filled up while we were flushing, flush again

View File

@@ -80,7 +80,7 @@ export async function verifyResourceSession(
res: Response,
next: NextFunction
): Promise<any> {
logger.debug("Verify session: Badger sent", req.body); // remove when done testing
logger.debug("Verify session: Badger request received");
const parsedBody = verifyResourceSessionSchema.safeParse(req.body);

View File

@@ -80,6 +80,10 @@ const removeClient = async (
const updatedClients = existingClients.filter((client) => client !== ws);
if (updatedClients.length === 0) {
connectedClients.delete(mapKey);
// Clean up config version tracking to prevent unbounded memory
// growth. Without this, every unique clientId that ever connects
// leaves a permanent entry in clientConfigVersions.
clientConfigVersions.delete(clientId);
logger.info(
`All connections removed for ${clientType.toUpperCase()} ID: ${clientId}`
@@ -507,6 +511,12 @@ const disconnectClient = async (clientId: string): Promise<boolean> => {
}
});
// Eagerly clean up tracking maps. The close event handlers will also
// call removeClient, but if the socket is already in CLOSING state
// the close event may never fire, leaving zombie entries.
connectedClients.delete(mapKey);
clientConfigVersions.delete(clientId);
return true;
};