(core) revamp snapshot inventory

Summary:
Deliberate changes:
 * save snapshots to s3 prior to migrations.
 * label migration snapshots in s3 metadata.
 * avoid pruning migration snapshots for a month.

Opportunistic changes:
 * Associate document timezone with snapshots, so pruning can respect timezones.
 * Associate actionHash/Num with snapshots.
 * Record time of last change in snapshots (rather than just s3 upload time, which could be a while later).

This ended up being a biggish change, because there was nowhere ideal to put tags (list of possibilities in diff).

Test Plan: added tests

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2646
This commit is contained in:
Paul Fitzpatrick
2020-10-30 12:53:23 -04:00
parent ce824aad34
commit 71519d9e5c
20 changed files with 699 additions and 246 deletions

37
app/common/DocSnapshot.ts Normal file
View File

@@ -0,0 +1,37 @@
/**
* Core metadata about a single document version.
*/
export interface ObjSnapshot {
lastModified: string;
snapshotId: string;
}
/**
* Extended Grist metadata about a single document version. Names of fields are kept
* short since there is a tight limit on total metadata size in S3.
*/
export interface ObjMetadata {
t?: string; // timestamp
tz?: string; // timezone
h?: string; // actionHash
n?: number; // actionNum
label?: string;
}
export interface ObjSnapshotWithMetadata extends ObjSnapshot {
metadata?: ObjMetadata;
}
/**
* Information about a single document snapshot in S3, including a Grist docId.
*/
export interface DocSnapshot extends ObjSnapshotWithMetadata {
docId: string;
}
/**
* A collection of document snapshots. Most recent snapshots first.
*/
export interface DocSnapshots {
snapshots: DocSnapshot[];
}

46
app/common/KeyedMutex.ts Normal file
View File

@@ -0,0 +1,46 @@
import { Mutex, MutexInterface } from 'async-mutex';
/**
* A per-key mutex. It has the same interface as Mutex, but with an extra key supplied.
* Maintains an independent mutex for each key on need.
*/
export class KeyedMutex {
private _mutexes = new Map<string, Mutex>();
public async acquire(key: string): Promise<MutexInterface.Releaser> {
// Create a new mutex if we need one.
if (!this._mutexes.has(key)) {
this._mutexes.set(key, new Mutex());
}
const mutex = this._mutexes.get(key)!
const unlock = await mutex.acquire();
return () => {
unlock();
// After unlocking, clean-up the mutex if it is no longer needed.
// unlock() leaves the mutex locked if anyone has been waiting for it.
if (!mutex.isLocked()) {
this._mutexes.delete(key);
}
};
}
public async runExclusive<T>(key: string, callback: MutexInterface.Worker<T>): Promise<T> {
const unlock = await this.acquire(key);
try {
return callback();
} finally {
unlock();
}
}
public isLocked(key: string): boolean {
const mutex = this._mutexes.get(key);
if (!mutex) { return false; }
return mutex.isLocked();
}
// Check how many mutexes are in use.
public get size(): number {
return this._mutexes.size;
}
}