From f2c67b902e55b475b74f8ea159e8d4fdd1089f88 Mon Sep 17 00:00:00 2001 From: Juha Paananen Date: Sun, 28 Jan 2024 21:38:05 +0200 Subject: [PATCH] Save board snapshot every 5000 events to avoid excessively long board load time due to snapshot being late --- backend/src/board-state.ts | 30 ++++++++++++++++++++++++++---- backend/src/board-store.ts | 9 +++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/backend/src/board-state.ts b/backend/src/board-state.ts index 0b2c39b84..7a10d6a48 100644 --- a/backend/src/board-state.ts +++ b/backend/src/board-state.ts @@ -1,14 +1,23 @@ import { boardReducer } from "../../common/src/board-reducer" import { Board, BoardCursorPositions, BoardHistoryEntry, Id, ItemLocks, Serial } from "../../common/src/domain" import { Locks } from "./locker" -import { createAccessToken, createBoard, fetchBoard, saveRecentEvents } from "./board-store" +import { + createAccessToken, + createBoard, + fetchBoard, + mkSnapshot, + saveBoardSnapshot, + saveRecentEvents, +} from "./board-store" import { broadcastItemLocks, getBoardSessionCount, getSessionCount } from "./websocket-sessions" import { compactBoardHistory, quickCompactBoardHistory } from "./compact-history" import { sleep } from "../../common/src/sleep" import { UserSession } from "./websocket-sessions" +import { inTransaction } from "./db" // A mutable state object for server side state export type ServerSideBoardState = { ready: true + snapshotSerial: Serial board: Board recentEvents: BoardHistoryEntry[] storingEvents: BoardHistoryEntry[] @@ -35,7 +44,7 @@ export async function getBoard(id: Id): Promise { const fetchState = async () => { const boardData = await fetchBoard(id) if (!boardData) return null - const { board, accessTokens } = boardData + const { board, accessTokens, snapshotSerial } = boardData return { ready: true, board, @@ -46,6 +55,7 @@ export async function getBoard(id: Id): Promise { cursorsMoved: false, cursorPositions: {}, sessions: [], + snapshotSerial, } as ServerSideBoardState } const fetch = fetchState() @@ -100,10 +110,10 @@ export function updateBoards(boardState: ServerSideBoardState, appEvent: BoardHi export async function addBoard(board: Board, createToken?: boolean): Promise { await createBoard(board) const accessTokens = createToken ? [await createAccessToken(board)] : [] - const boardState = { + const boardState: ServerSideBoardState = { ready: true as const, board, - serial: 0, + snapshotSerial: 0, recentEvents: [], storingEvents: [], locks: Locks((changedLocks) => broadcastItemLocks(board.id, changedLocks)), @@ -145,6 +155,18 @@ async function saveBoardChanges(state: ServerSideBoardState) { ) try { await saveRecentEvents(state.board.id, state.storingEvents) + if (state.board.serial - state.snapshotSerial > 5000) { + console.log( + `Saving snapshot for board ${state.board.id} with ${ + state.board.serial - state.snapshotSerial + } new events`, + ) + await inTransaction(async (client) => { + await saveBoardSnapshot(mkSnapshot(state.board, state.board.serial), client) + }) + state.snapshotSerial = state.board.serial + } + // TODO: also update snapshot if more than 1000 events since snapshot } catch (e) { // Push event back to the head of save list for retrying later state.recentEvents = [...state.storingEvents, ...state.recentEvents] diff --git a/backend/src/board-store.ts b/backend/src/board-store.ts index b460dafe8..b4accc3e9 100644 --- a/backend/src/board-store.ts +++ b/backend/src/board-store.ts @@ -6,9 +6,10 @@ import { Board, BoardAccessPolicy, BoardHistoryEntry, Id, isBoardEmpty, Serial } import { migrateBoard, migrateEvent, mkBootStrapEvent } from "../../common/src/migration" import { inTransaction, withDBClient } from "./db" -export type BoardAndAccessTokens = { +export type BoardFetchResult = { board: Board accessTokens: string[] + snapshotSerial: Serial } export type BoardInfo = { @@ -37,7 +38,7 @@ select id, where id=$1 ` -export async function fetchBoard(id: Id): Promise { +export async function fetchBoard(id: Id): Promise { return await inTransaction(async (client) => { const started = new Date().getTime() const result = await client.query(selectBoardQuery, [id]) @@ -100,7 +101,7 @@ export async function fetchBoard(id: Id): Promise { await client.query("SELECT token FROM board_api_token WHERE board_id=$1", [id]) ).rows.map((row) => row.token) - return { board: { ...board, serial }, accessTokens } + return { board: { ...board, serial }, accessTokens, snapshotSerial: snapshot.serial } } }) } @@ -180,7 +181,7 @@ export async function createAccessToken(board: Board): Promise { } export async function saveRecentEvents(id: Id, recentEvents: BoardHistoryEntry[]) { - await inTransaction(async (client) => storeEventHistoryBundle(id, recentEvents, client)) + await inTransaction(async (client) => await storeEventHistoryBundle(id, recentEvents, client)) } type StreamingBoardEventCallback = (chunk: BoardHistoryEntry[]) => void