Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions packages/backend/server/src/core/doc/adapters/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,22 +276,16 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
return false;
}

try {
await this.models.history.create(
{
spaceId: snapshot.spaceId,
docId: snapshot.docId,
timestamp: snapshot.timestamp,
blob: Buffer.from(snapshot.bin),
editorId: snapshot.editor,
},
historyMaxAge
);
} catch (e) {
// safe to ignore
// only happens when duplicated history record created in multi processes
this.logger.error('Failed to create history record', e);
}
await this.models.history.create(
{
spaceId: snapshot.spaceId,
docId: snapshot.docId,
timestamp: snapshot.timestamp,
blob: Buffer.from(snapshot.bin),
editorId: snapshot.editor,
},
historyMaxAge
);

metrics.doc
.counter('history_created_counter', {
Expand Down
21 changes: 21 additions & 0 deletions packages/backend/server/src/models/__tests__/history.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ test('should create a history record', async t => {
});
});

test('should not fail on duplicated history record', async t => {
const snapshot = {
spaceId: workspace.id,
docId: randomUUID(),
blob: Uint8Array.from([1, 2, 3]),
timestamp: Date.now(),
editorId: user.id,
};

const created1 = await t.context.history.create(snapshot, 1000);
const created2 = await t.context.history.create(snapshot, 1000);
t.deepEqual(created1.timestamp, snapshot.timestamp);
t.deepEqual(created2.timestamp, snapshot.timestamp);

const histories = await t.context.history.findMany(
snapshot.spaceId,
snapshot.docId
);
t.is(histories.length, 1);
});

test('should return null when history timestamp not match', async t => {
const snapshot = {
spaceId: workspace.id,
Expand Down
27 changes: 19 additions & 8 deletions packages/backend/server/src/models/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,33 @@ export class HistoryModel extends BaseModel {
* Create a doc history with a max age.
*/
async create(snapshot: Doc, maxAge: number): Promise<DocHistorySimple> {
const row = await this.db.snapshotHistory.create({
select: {
timestamp: true,
createdByUser: { select: publicUserSelect },
const timestamp = new Date(snapshot.timestamp);
const expiredAt = new Date(Date.now() + maxAge);

// This method may be called concurrently by multiple processes for the same
// (workspaceId, docId, timestamp). Using upsert avoids duplicate key errors
// that would otherwise abort the surrounding transaction.
const row = await this.db.snapshotHistory.upsert({
where: {
workspaceId_id_timestamp: {
workspaceId: snapshot.spaceId,
id: snapshot.docId,
timestamp,
},
},
data: {
select: { timestamp: true, createdByUser: { select: publicUserSelect } },
create: {
workspaceId: snapshot.spaceId,
id: snapshot.docId,
timestamp: new Date(snapshot.timestamp),
timestamp,
blob: snapshot.blob,
createdBy: snapshot.editorId,
expiredAt: new Date(Date.now() + maxAge),
expiredAt,
},
update: { expiredAt },
});
this.logger.debug(
`Created history ${row.timestamp} for ${snapshot.docId} in ${snapshot.spaceId}`
`Upserted history ${row.timestamp} for ${snapshot.docId} in ${snapshot.spaceId}`
);
return {
timestamp: row.timestamp.getTime(),
Expand Down
Loading