gristlabs_grist-core/test/gen-server/lib/Housekeeper.ts

208 lines
7.5 KiB
TypeScript
Raw Normal View History

import { TelemetryEvent, TelemetryMetadataByLevel } from 'app/common/Telemetry';
import { Document } from 'app/gen-server/entity/Document';
import { Workspace } from 'app/gen-server/entity/Workspace';
import { Housekeeper } from 'app/gen-server/lib/Housekeeper';
import { Telemetry } from 'app/server/lib/Telemetry';
import { assert } from 'chai';
import * as fse from 'fs-extra';
import moment from 'moment';
import * as sinon from 'sinon';
import { TestServer } from 'test/gen-server/apiUtils';
import { openClient } from 'test/server/gristClient';
import * as testUtils from 'test/server/testUtils';
describe('Housekeeper', function() {
testUtils.setTmpLogLevel('error');
this.timeout(60000);
const org: string = 'testy';
const sandbox = sinon.createSandbox();
let home: TestServer;
let keeper: Housekeeper;
before(async function() {
home = new TestServer(this);
await home.start(['home', 'docs']);
const api = await home.createHomeApi('chimpy', 'docs');
await api.newOrg({name: org, domain: org});
keeper = home.server.housekeeper;
await keeper.stop();
});
after(async function() {
await home.stop();
sandbox.restore();
});
async function getDoc(docId: string) {
const manager = home.dbManager.connection.manager;
return manager.findOneOrFail(Document, {where: {id: docId}});
}
async function getWorkspace(wsId: number) {
const manager = home.dbManager.connection.manager;
return manager.findOneOrFail(Workspace, {where: {id: wsId}});
}
function daysAgo(days: number): Date {
return moment().subtract(days, 'days').toDate();
}
async function ageDoc(docId: string, days: number) {
const dbDoc = await getDoc(docId);
dbDoc.removedAt = daysAgo(days);
await dbDoc.save();
}
async function ageWorkspace(wsId: number, days: number) {
const dbWorkspace = await getWorkspace(wsId);
dbWorkspace.removedAt = daysAgo(days);
await dbWorkspace.save();
}
async function ageFork(forkId: string, days: number) {
const dbFork = await getDoc(forkId);
dbFork.updatedAt = daysAgo(days);
await dbFork.save();
}
it('can delete old soft-deleted docs and workspaces', async function() {
// Make four docs in one workspace, two in another.
const api = await home.createHomeApi('chimpy', org);
const ws1 = await api.newWorkspace({name: 'ws1'}, 'current');
const ws2 = await api.newWorkspace({name: 'ws2'}, 'current');
const doc11 = await api.newDoc({name: 'doc11'}, ws1);
const doc12 = await api.newDoc({name: 'doc12'}, ws1);
const doc13 = await api.newDoc({name: 'doc13'}, ws1);
const doc14 = await api.newDoc({name: 'doc14'}, ws1);
const doc21 = await api.newDoc({name: 'doc21'}, ws2);
const doc22 = await api.newDoc({name: 'doc22'}, ws2);
// Soft-delete some of the docs, and one workspace.
await api.softDeleteDoc(doc11);
await api.softDeleteDoc(doc12);
await api.softDeleteDoc(doc13);
await api.softDeleteWorkspace(ws2);
// Check that nothing is deleted by housekeeper.
await keeper.deleteTrash();
await assert.isFulfilled(getDoc(doc11));
await assert.isFulfilled(getDoc(doc12));
await assert.isFulfilled(getDoc(doc13));
await assert.isFulfilled(getDoc(doc14));
await assert.isFulfilled(getDoc(doc21));
await assert.isFulfilled(getDoc(doc22));
await assert.isFulfilled(getWorkspace(ws1));
await assert.isFulfilled(getWorkspace(ws2));
// Age a doc and workspace somewhat, but not enough to trigger hard-deletion.
await ageDoc(doc11, 10);
await ageWorkspace(ws2, 20);
await keeper.deleteTrash();
await assert.isFulfilled(getDoc(doc11));
await assert.isFulfilled(getWorkspace(ws2));
// Prematurely age two of the soft-deleted docs, and the soft-deleted workspace.
await ageDoc(doc11, 40);
await ageDoc(doc12, 40);
await ageWorkspace(ws2, 40);
// Make sure that exactly those docs are deleted by housekeeper.
await keeper.deleteTrash();
await assert.isRejected(getDoc(doc11));
await assert.isRejected(getDoc(doc12));
await assert.isFulfilled(getDoc(doc13));
await assert.isFulfilled(getDoc(doc14));
await assert.isRejected(getDoc(doc21));
await assert.isRejected(getDoc(doc22));
await assert.isFulfilled(getWorkspace(ws1));
await assert.isRejected(getWorkspace(ws2));
});
it('enforces exclusivity of housekeeping', async function() {
const first = keeper.deleteTrashExclusively();
const second = keeper.deleteTrashExclusively();
assert.equal(await first, true);
assert.equal(await second, false);
assert.equal(await keeper.deleteTrashExclusively(), false);
await keeper.testClearExclusivity();
assert.equal(await keeper.deleteTrashExclusively(), true);
});
it('can delete old forks', async function() {
// Make a document with some forks.
const api = await home.createHomeApi('chimpy', org);
const ws3 = await api.newWorkspace({name: 'ws3'}, 'current');
const trunk = await api.newDoc({name: 'trunk'}, ws3);
const session = await api.getSessionActive();
const client = await openClient(home.server, session.user.email, session.org?.domain || 'docs');
await client.openDocOnConnect(trunk);
const forkResponse1 = await client.send('fork', 0);
const forkResponse2 = await client.send('fork', 0);
const forkPath1 = home.server.getStorageManager().getPath(forkResponse1.data.docId);
const forkPath2 = home.server.getStorageManager().getPath(forkResponse2.data.docId);
const forkId1 = forkResponse1.data.forkId;
const forkId2 = forkResponse2.data.forkId;
// Age the forks somewhat, but not enough to trigger hard-deletion.
await ageFork(forkId1, 10);
await ageFork(forkId2, 20);
await keeper.deleteTrash();
await assert.isFulfilled(getDoc(forkId1));
await assert.isFulfilled(getDoc(forkId2));
assert.equal(await fse.pathExists(forkPath1), true);
assert.equal(await fse.pathExists(forkPath2), true);
// Age one of the forks beyond the cleanup threshold.
await ageFork(forkId2, 40);
// Make sure that only that fork is deleted by housekeeper.
await keeper.deleteTrash();
await assert.isFulfilled(getDoc(forkId1));
await assert.isRejected(getDoc(forkId2));
assert.equal(await fse.pathExists(forkPath1), true);
assert.equal(await fse.pathExists(forkPath2), false);
});
it('can log metrics about sites', async function() {
const logMessages: [TelemetryEvent, TelemetryMetadataByLevel?][] = [];
sandbox.stub(Telemetry.prototype, 'shouldLogEvent').callsFake((name) => true);
sandbox.stub(Telemetry.prototype, 'logEvent').callsFake((_, name, meta) => {
// Skip document usage events that could be arriving in the
// middle of this test.
if (name !== 'documentUsage') {
logMessages.push([name, meta]);
}
return Promise.resolve();
});
await keeper.logMetrics();
assert.isNotEmpty(logMessages);
let [event, meta] = logMessages[0];
assert.equal(event, 'siteUsage');
assert.hasAllKeys(meta?.limited, [
'siteId',
'siteType',
'inGoodStanding',
'numDocs',
'numWorkspaces',
'numMembers',
'lastActivity',
'earliestDocCreatedAt',
]);
assert.hasAllKeys(meta?.full, [
'stripePlanId',
]);
[event, meta] = logMessages[logMessages.length - 1];
assert.equal(event, 'siteMembership');
assert.hasAllKeys(meta?.limited, [
'siteId',
'siteType',
'numOwners',
'numEditors',
'numViewers',
]);
assert.isUndefined(meta?.full);
});
});