-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DTP-1024] GC tombstoned map entries for LiveMap and objects in the global pool #1937
base: DTP-986/handle-tombstone-and-object-delete
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export const DEFAULTS = { | ||
gcInterval: 1000 * 60 * 5, // 5 minutes | ||
/** | ||
* Must be > 2 minutes to ensure we keep tombstones long enough to avoid the possibility of receiving an operation | ||
* with an earlier origin timeserial that would not have been applied if the tombstone still existed. | ||
* | ||
* Applies both for map entries tombstones and object tombstones. | ||
*/ | ||
gcGracePeriod: 1000 * 60 * 2.5, // 2.5 minutes | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,7 @@ | ||||||||||
import deepEqual from 'deep-equal'; | ||||||||||
|
||||||||||
import type * as API from '../../../ably'; | ||||||||||
import { DEFAULTS } from './defaults'; | ||||||||||
import { LiveObject, LiveObjectData, LiveObjectUpdate, LiveObjectUpdateNoop } from './liveobject'; | ||||||||||
import { LiveObjects } from './liveobjects'; | ||||||||||
import { | ||||||||||
|
@@ -33,6 +34,10 @@ export type StateData = ObjectIdStateData | ValueStateData; | |||||||||
|
||||||||||
export interface MapEntry { | ||||||||||
tombstone: boolean; | ||||||||||
/** | ||||||||||
* Can't use timeserial from the operation that deleted the entry for the same reason as for {@link LiveObject} tombstones, see explanation there. | ||||||||||
*/ | ||||||||||
tombstonedAt: number | undefined; | ||||||||||
timeserial: string | undefined; | ||||||||||
data: StateData | undefined; | ||||||||||
} | ||||||||||
|
@@ -291,6 +296,22 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData, | |||||||||
return this._updateFromDataDiff(previousDataRef, this._dataRef); | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* @internal | ||||||||||
*/ | ||||||||||
onGCInterval(): void { | ||||||||||
// should remove any tombstoned entries from the underlying map data that have exceeded the GC grace period | ||||||||||
|
||||||||||
const keysToDelete: string[] = []; | ||||||||||
for (const [key, value] of this._dataRef.data.entries()) { | ||||||||||
if (value.tombstone === true && Date.now() - value.tombstonedAt! >= DEFAULTS.gcGracePeriod) { | ||||||||||
keysToDelete.push(key); | ||||||||||
Comment on lines
+307
to
+308
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid using non-null assertions on In the Apply this diff to add a null check: -if (value.tombstone === true && Date.now() - value.tombstonedAt! >= DEFAULTS.gcGracePeriod) {
+if (value.tombstone === true && value.tombstonedAt !== undefined && Date.now() - value.tombstonedAt >= DEFAULTS.gcGracePeriod) { 📝 Committable suggestion
Suggested change
|
||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
keysToDelete.forEach((x) => this._dataRef.data.delete(x)); | ||||||||||
} | ||||||||||
|
||||||||||
protected _getZeroValueData(): LiveMapData { | ||||||||||
return { data: new Map<string, MapEntry>() }; | ||||||||||
} | ||||||||||
|
@@ -455,11 +476,13 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData, | |||||||||
|
||||||||||
if (existingEntry) { | ||||||||||
existingEntry.tombstone = false; | ||||||||||
existingEntry.tombstonedAt = undefined; | ||||||||||
existingEntry.timeserial = opOriginTimeserial; | ||||||||||
existingEntry.data = liveData; | ||||||||||
} else { | ||||||||||
const newEntry: MapEntry = { | ||||||||||
tombstone: false, | ||||||||||
tombstonedAt: undefined, | ||||||||||
timeserial: opOriginTimeserial, | ||||||||||
data: liveData, | ||||||||||
}; | ||||||||||
|
@@ -486,11 +509,13 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData, | |||||||||
|
||||||||||
if (existingEntry) { | ||||||||||
existingEntry.tombstone = true; | ||||||||||
existingEntry.tombstonedAt = Date.now(); | ||||||||||
existingEntry.timeserial = opOriginTimeserial; | ||||||||||
existingEntry.data = undefined; | ||||||||||
} else { | ||||||||||
const newEntry: MapEntry = { | ||||||||||
tombstone: true, | ||||||||||
tombstonedAt: Date.now(), | ||||||||||
timeserial: opOriginTimeserial, | ||||||||||
data: undefined, | ||||||||||
}; | ||||||||||
|
@@ -544,9 +569,10 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData, | |||||||||
|
||||||||||
const liveDataEntry: MapEntry = { | ||||||||||
timeserial: entry.timeserial, | ||||||||||
// true only if we received explicit true. otherwise always false | ||||||||||
tombstone: entry.tombstone === true, | ||||||||||
data: liveData, | ||||||||||
// consider object as tombstoned only if we received an explicit flag stating that. otherwise it exists | ||||||||||
tombstone: entry.tombstone === true, | ||||||||||
tombstonedAt: entry.tombstone === true ? Date.now() : undefined, | ||||||||||
}; | ||||||||||
|
||||||||||
liveMapData.data.set(key, liveDataEntry); | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,14 @@ export abstract class LiveObject< | |
protected _siteTimeserials: Record<string, string>; | ||
protected _createOperationIsMerged: boolean; | ||
private _tombstone: boolean; | ||
/** | ||
* Even though the `timeserial` from the operation that deleted the object contains the timestamp value, | ||
* the `timeserial` should be treated as an opaque string on the client, meaning we should not attempt to parse it. | ||
* | ||
* Therefore, we need to set our own timestamp when the object is deleted client-side. Strictly speaking, this is | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does make an assumption about the client clock not being too heavily skewed behind the server. I think we have two options for a proper fix:
Given the grace period, the likelihood of encountering a race here is pretty low. So, I think we should create a ticket to do this properly but for now this is fine. |
||
* slightly less precise, as we will GC the object later than the server, but it is an acceptable compromise. | ||
*/ | ||
private _tombstonedAt: number | undefined; | ||
|
||
protected constructor( | ||
protected _liveObjects: LiveObjects, | ||
|
@@ -108,6 +116,7 @@ export abstract class LiveObject< | |
*/ | ||
tombstone(): void { | ||
this._tombstone = true; | ||
this._tombstonedAt = Date.now(); | ||
this._dataRef = this._getZeroValueData(); | ||
// TODO: emit "deleted" event so that end users get notified about this object getting deleted | ||
} | ||
|
@@ -119,6 +128,13 @@ export abstract class LiveObject< | |
return this._tombstone; | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
tombstonedAt(): number | undefined { | ||
return this._tombstonedAt; | ||
} | ||
|
||
/** | ||
* Returns true if the given origin timeserial indicates that the operation to which it belongs should be applied to the object. | ||
* | ||
|
@@ -168,6 +184,11 @@ export abstract class LiveObject< | |
* @internal | ||
*/ | ||
abstract overrideWithStateObject(stateObject: StateObject): TUpdate | LiveObjectUpdateNoop; | ||
/** | ||
* @internal | ||
*/ | ||
abstract onGCInterval(): void; | ||
|
||
protected abstract _getZeroValueData(): TData; | ||
/** | ||
* Calculate the update object based on the current Live Object data and incoming new data. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this actually needs to be 24 hours, which is how long realtime will keep tombstones for