Path: blob/master/src/packages/backend/conat/test/persist/backup-archive.test.ts
1712 views
/*1Testing automatic tiered storage and backup persistence functionality.2*/34import {5before,6after,7delay,8client,9wait,10} from "@cocalc/backend/conat/test/setup";11import { stream } from "@cocalc/conat/persist/client";12import { syncFiles } from "@cocalc/conat/persist/context";13import { pathExists } from "fs-extra";14import { join } from "path";15import * as fs from "fs/promises";16import { messageData } from "@cocalc/conat/core/client";17import sqlite from "better-sqlite3";18import { openPaths } from "@cocalc/conat/persist/storage";1920beforeAll(async () => {21await before({ archive: "archive", backup: "backup", archiveInterval: 250 });22});2324describe("create persist server that also saves data to an archive folder and a backup folder", () => {25it("verify that archive, backup and archiveInterval are all configured", async () => {26expect(syncFiles.archive).toContain("archive");27expect(syncFiles.archiveInterval).toBeGreaterThan(0);28expect(syncFiles.backup).toContain("backup");29});3031async function waitUntilClosed() {32await wait({33until: () => {34return !openPaths.has(join(syncFiles.local, "hub/foo"));35},36});37}3839let s1;40it("create a new stream", async () => {41s1 = stream({42client,43user: { hub_id: "x" },44storage: { path: "hub/foo" },45});46await s1.set({47key: "my-key-1",48messageData: messageData("one"),49});50});5152let local, archive, backup;53it(`wait, then there is an updated archive file too`, async () => {54((local = join(syncFiles.local, "hub/foo.db")),55(archive = join(syncFiles.archive, "hub/foo.db")),56(backup = join(syncFiles.backup, "hub/foo.db")),57expect(await pathExists(local)).toBe(true));58// gets created initially59expect(await pathExists(archive)).toBe(true);60// backup should only exist when stream is closed61expect(await pathExists(backup)).toBe(false);6263// timestamp before another write64const stats = await fs.stat(archive);6566await s1.set({67key: "my-key-2",68messageData: messageData("two"),69});70// now wait to ensure archive gets written7172await delay(syncFiles.archiveInterval + 100);73expect(await pathExists(archive)).toBe(true);74const stats2 = await fs.stat(archive);75expect(stats2.mtimeMs).not.toEqual(stats.mtimeMs);76});7778it("close the stream and see that the backup and archive are both written, even though we didn't wait the full archive interval", async () => {79s1.close();80const t = Date.now();81await wait({82until: async () => await pathExists(backup),83});84expect(Date.now() - t).toBeLessThan(syncFiles.archiveInterval);85expect(await pathExists(backup)).toBe(true);86// at this point the actual sqlite3 database should be closed87});8889it("the backup, archive, and local files should all be identical as sqlite database", async () => {90// they are not the same as files though so we need some care to compare them.91expect(await serialize(local)).toEqual(await serialize(backup));92expect(await serialize(archive)).toEqual(await serialize(backup));93});9495it("delete the local copy and open stream, the data is still available", async () => {96await fs.unlink(local);9798s1 = stream({99client,100user: { hub_id: "x" },101storage: { path: "hub/foo" },102});103const mesg = await s1.get({ key: "my-key-1" });104expect(mesg.data).toBe("one");105106await s1.set({107key: "my-key-3",108messageData: messageData("three"),109});110111s1.close();112await waitUntilClosed();113});114115it("delete the archive copy and open stream, the data is still available because local is used", async () => {116await fs.unlink(archive);117118s1 = stream({119client,120user: { hub_id: "x" },121storage: { path: "hub/foo" },122});123const mesg = await s1.get({ key: "my-key-3" });124expect(mesg.data).toBe("three");125126s1.close();127await waitUntilClosed();128});129130it("all should identical again sqlite database", async () => {131// they are not the same as files though so we need some care to compare them.132expect(await serialize(local)).toEqual(await serialize(backup));133expect(await serialize(archive)).toEqual(await serialize(backup));134});135136it("if both archive and local exist and local is newer, it is used", async () => {137// grab copy of local138const copy = local + ".copy";139await fs.copyFile(local, copy);140141s1 = stream({142client,143user: { hub_id: "x" },144storage: { path: "hub/foo" },145});146await s1.set({147key: "my-key-4",148messageData: messageData("four"),149});150fs.unlink(backup);151s1.close();152await wait({153until: async () => await pathExists(backup),154});155156// ensure the old copy of local is the newer one by making archive old157await fs.copyFile(copy, local);158await fs.utimes(159archive,160Date.now() / 1000 - 100_000,161Date.now() / 1000 - 100_000,162);163s1 = stream({164client,165user: { hub_id: "x" },166storage: { path: "hub/foo" },167});168expect((await s1.get({ key: "my-key-4" }))?.data).toEqual(undefined);169170s1.close();171await waitUntilClosed();172});173174it("if both archive and local exist and archive is newer, then archive is used", async () => {175// grab copy of archive176const copy = archive + ".copy";177await fs.copyFile(archive, copy);178179s1 = stream({180client,181user: { hub_id: "x" },182storage: { path: "hub/foo" },183});184await s1.set({185key: "my-key-5",186messageData: messageData("five"),187});188s1.close();189await waitUntilClosed();190191// ensure the old copy of archive is the newer one by making local old192await fs.copyFile(copy, archive);193await fs.utimes(194local,195Date.now() / 1000 - 100_000,196Date.now() / 1000 - 100_000,197);198s1 = stream({199client,200user: { hub_id: "x" },201storage: { path: "hub/foo" },202});203expect((await s1.get({ key: "my-key-5" }))?.data).toEqual(undefined);204205s1.close();206await waitUntilClosed();207});208209it("another check all are equal now", async () => {210//console.log("checking equality");211expect(await serialize(local)).toEqual(await serialize(backup));212expect(await serialize(archive)).toEqual(await serialize(backup));213});214215it("deletes local and archive but not backup -- data is NOT available", async () => {216await fs.unlink(local);217await fs.unlink(archive);218s1 = stream({219client,220user: { hub_id: "x" },221storage: { path: "hub/foo" },222});223expect((await s1.get({ key: "my-key-1" }))?.data).toEqual(undefined);224});225});226227async function serialize(path: string): Promise<string> {228while (true) {229const db = new sqlite(path);230try {231const x = JSON.stringify({232messages: db.prepare("select * from messages").all(),233config: db.prepare("select * from config").all(),234});235db.close();236return x;237} catch (err) {238console.log(err);239}240await delay(50);241}242}243244afterAll(async () => {245after();246});247248249