Skip to content

Commit 8dfda2e

Browse files
authored
Merge pull request #2053 from ably/PUB-1702/objects-server-gc
[PUB-1702] Use server-provided GC grace period for tombstone removal
2 parents 38e5804 + d910262 commit 8dfda2e

6 files changed

Lines changed: 118 additions & 10 deletions

File tree

src/plugins/objects/defaults.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
export const DEFAULTS = {
22
gcInterval: 1000 * 60 * 5, // 5 minutes
33
/**
4+
* The SDK will attempt to use the `objectsGCGracePeriod` value provided by the server in the `connectionDetails` object of the `CONNECTED` event.
5+
* If the server does not provide this value, the SDK will fall back to this default value.
6+
*
47
* Must be > 2 minutes to ensure we keep tombstones long enough to avoid the possibility of receiving an operation
58
* with an earlier serial that would not have been applied if the tombstone still existed.
69
*

src/plugins/objects/livemap.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { dequal } from 'dequal';
22

33
import type { Bufferlike } from 'common/platform';
44
import type * as API from '../../../ably';
5-
import { DEFAULTS } from './defaults';
65
import { LiveObject, LiveObjectData, LiveObjectUpdate, LiveObjectUpdateNoop } from './liveobject';
76
import { ObjectId } from './objectid';
87
import {
@@ -556,7 +555,7 @@ export class LiveMap<T extends API.LiveMapType> extends LiveObject<LiveMapData,
556555

557556
const keysToDelete: string[] = [];
558557
for (const [key, value] of this._dataRef.data.entries()) {
559-
if (value.tombstone === true && Date.now() - value.tombstonedAt! >= DEFAULTS.gcGracePeriod) {
558+
if (value.tombstone === true && Date.now() - value.tombstonedAt! >= this._objects.gcGracePeriod) {
560559
keysToDelete.push(key);
561560
}
562561
}

src/plugins/objects/objects.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export interface OnObjectsEventResponse {
3737
export type BatchCallback = (batchContext: BatchContext) => void;
3838

3939
export class Objects {
40+
gcGracePeriod: number;
41+
4042
private _client: BaseClient;
4143
private _channel: RealtimeChannel;
4244
private _state: ObjectsState;
@@ -63,6 +65,12 @@ export class Objects {
6365
this._objectsPool = new ObjectsPool(this);
6466
this._syncObjectsDataPool = new SyncObjectsDataPool(this);
6567
this._bufferedObjectOperations = [];
68+
// use server-provided objectsGCGracePeriod if available, and subscribe to new connectionDetails that can be emitted as part of the RTN24
69+
this.gcGracePeriod =
70+
this._channel.connectionManager.connectionDetails?.objectsGCGracePeriod ?? DEFAULTS.gcGracePeriod;
71+
this._channel.connectionManager.on('connectiondetails', (details: Record<string, any>) => {
72+
this.gcGracePeriod = details.objectsGCGracePeriod ?? DEFAULTS.gcGracePeriod;
73+
});
6674
}
6775

6876
/**

src/plugins/objects/objectspool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export class ObjectsPool {
109109
// tombstoned objects should be removed from the pool if they have been tombstoned for longer than grace period.
110110
// by removing them from the local pool, Objects plugin no longer keeps a reference to those objects, allowing JS's
111111
// Garbage Collection to eventually free the memory for those objects, provided the user no longer references them either.
112-
if (obj.isTombstoned() && Date.now() - obj.tombstonedAt()! >= DEFAULTS.gcGracePeriod) {
112+
if (obj.isTombstoned() && Date.now() - obj.tombstonedAt()! >= this._objects.gcGracePeriod) {
113113
toDelete.push(objectId);
114114
continue;
115115
}

test/common/modules/private_api_recorder.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ define(['test/support/output_directory_paths'], function (outputDirectoryPaths)
8080
'read.Defaults.version',
8181
'read.LiveMap._dataRef.data',
8282
'read.EventEmitter.events',
83+
'read.Objects._DEFAULTS.gcGracePeriod',
84+
'read.Objects.gcGracePeriod',
8385
'read.Platform.Config.push',
8486
'read.ProtocolMessage.channelSerial',
8587
'read.Realtime._transports',
@@ -141,8 +143,8 @@ define(['test/support/output_directory_paths'], function (outputDirectoryPaths)
141143
'write.Defaults.ENDPOINT',
142144
'write.Defaults.ENVIRONMENT',
143145
'write.Defaults.wsConnectivityCheckUrl',
144-
'write.Objects._DEFAULTS.gcGracePeriod',
145146
'write.Objects._DEFAULTS.gcInterval',
147+
'write.Objects.gcGracePeriod',
146148
'write.Platform.Config.push', // This implies using a mock implementation of the internal IPlatformPushConfig interface. Our mock (in push_channel_transport.js) then interacts with internal objects and private APIs of public objects to implement this interface; I haven’t added annotations for that private API usage, since there wasn’t an easy way to pass test context information into the mock. I think that for now we can just say that if we wanted to get rid of this private API usage, then we’d need to remove this mock entirely.
147149
'write.auth.authOptions.requestHeaders',
148150
'write.auth.key',

test/realtime/objects.test.js

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ define(['ably', 'shared_helper', 'chai', 'objects', 'objects_helper'], function
1515
const objectsFixturesChannel = 'objects_fixtures';
1616
const nextTick = Ably.Realtime.Platform.Config.nextTick;
1717
const gcIntervalOriginal = ObjectsPlugin.Objects._DEFAULTS.gcInterval;
18-
const gcGracePeriodOriginal = ObjectsPlugin.Objects._DEFAULTS.gcGracePeriod;
1918

2019
function RealtimeWithObjects(helper, options) {
2120
return helper.AblyRealtime({ ...options, plugins: { Objects: ObjectsPlugin } });
@@ -4525,6 +4524,99 @@ define(['ably', 'shared_helper', 'chai', 'objects', 'objects_helper'], function
45254524
}, client);
45264525
});
45274526

4527+
it('gcGracePeriod is set from connectionDetails.objectsGCGracePeriod', async function () {
4528+
const helper = this.test.helper;
4529+
const client = RealtimeWithObjects(helper);
4530+
4531+
await helper.monitorConnectionThenCloseAndFinishAsync(async () => {
4532+
await client.connection.once('connected');
4533+
4534+
const channel = client.channels.get('channel', channelOptionsWithObjects());
4535+
const objects = channel.objects;
4536+
const connectionManager = client.connection.connectionManager;
4537+
const connectionDetails = connectionManager.connectionDetails;
4538+
4539+
// gcGracePeriod should be set after the initial connection
4540+
helper.recordPrivateApi('read.Objects.gcGracePeriod');
4541+
expect(
4542+
objects.gcGracePeriod,
4543+
'Check gcGracePeriod is set after initial connection from connectionDetails.objectsGCGracePeriod',
4544+
).to.exist;
4545+
helper.recordPrivateApi('read.Objects.gcGracePeriod');
4546+
expect(objects.gcGracePeriod).to.equal(
4547+
connectionDetails.objectsGCGracePeriod,
4548+
'Check gcGracePeriod is set to equal connectionDetails.objectsGCGracePeriod',
4549+
);
4550+
4551+
const connectionDetailsPromise = connectionManager.once('connectiondetails');
4552+
4553+
helper.recordPrivateApi('call.connectionManager.activeProtocol.getTransport');
4554+
helper.recordPrivateApi('call.transport.onProtocolMessage');
4555+
helper.recordPrivateApi('call.makeProtocolMessageFromDeserialized');
4556+
connectionManager.activeProtocol.getTransport().onProtocolMessage(
4557+
createPM({
4558+
action: 4, // CONNECTED
4559+
connectionDetails: {
4560+
...connectionDetails,
4561+
objectsGCGracePeriod: 999,
4562+
},
4563+
}),
4564+
);
4565+
4566+
helper.recordPrivateApi('listen.connectionManager.connectiondetails');
4567+
await connectionDetailsPromise;
4568+
// wait for next tick to ensure the connectionDetails event was processed by Objects plugin
4569+
await new Promise((res) => nextTick(res));
4570+
4571+
helper.recordPrivateApi('read.Objects.gcGracePeriod');
4572+
expect(objects.gcGracePeriod).to.equal(999, 'Check gcGracePeriod is updated on new CONNECTED event');
4573+
}, client);
4574+
});
4575+
4576+
it('gcGracePeriod has a default value if connectionDetails.objectsGCGracePeriod is missing', async function () {
4577+
const helper = this.test.helper;
4578+
const client = RealtimeWithObjects(helper);
4579+
4580+
await helper.monitorConnectionThenCloseAndFinishAsync(async () => {
4581+
await client.connection.once('connected');
4582+
4583+
const channel = client.channels.get('channel', channelOptionsWithObjects());
4584+
const objects = channel.objects;
4585+
const connectionManager = client.connection.connectionManager;
4586+
const connectionDetails = connectionManager.connectionDetails;
4587+
4588+
helper.recordPrivateApi('read.Objects._DEFAULTS.gcGracePeriod');
4589+
helper.recordPrivateApi('write.Objects.gcGracePeriod');
4590+
// set gcGracePeriod to a value different from the default
4591+
objects.gcGracePeriod = ObjectsPlugin.Objects._DEFAULTS.gcGracePeriod + 1;
4592+
4593+
const connectionDetailsPromise = connectionManager.once('connectiondetails');
4594+
4595+
// send a CONNECTED event without objectsGCGracePeriod, it should use the default value instead
4596+
helper.recordPrivateApi('call.connectionManager.activeProtocol.getTransport');
4597+
helper.recordPrivateApi('call.transport.onProtocolMessage');
4598+
helper.recordPrivateApi('call.makeProtocolMessageFromDeserialized');
4599+
connectionManager.activeProtocol.getTransport().onProtocolMessage(
4600+
createPM({
4601+
action: 4, // CONNECTED
4602+
connectionDetails,
4603+
}),
4604+
);
4605+
4606+
helper.recordPrivateApi('listen.connectionManager.connectiondetails');
4607+
await connectionDetailsPromise;
4608+
// wait for next tick to ensure the connectionDetails event was processed by Objects plugin
4609+
await new Promise((res) => nextTick(res));
4610+
4611+
helper.recordPrivateApi('read.Objects._DEFAULTS.gcGracePeriod');
4612+
helper.recordPrivateApi('read.Objects.gcGracePeriod');
4613+
expect(objects.gcGracePeriod).to.equal(
4614+
ObjectsPlugin.Objects._DEFAULTS.gcGracePeriod,
4615+
'Check gcGracePeriod is set to a default value if connectionDetails.objectsGCGracePeriod is missing',
4616+
);
4617+
}, client);
4618+
});
4619+
45284620
const tombstonesGCScenarios = [
45294621
// for the next tests we need to access the private API of Objects plugin in order to verify that tombstoned entities were indeed deleted after the GC grace period.
45304622
// public API hides that kind of information from the user and returns undefined for tombstoned entities even if realtime client still keeps a reference to them.
@@ -4631,8 +4723,6 @@ define(['ably', 'shared_helper', 'chai', 'objects', 'objects_helper'], function
46314723
try {
46324724
helper.recordPrivateApi('write.Objects._DEFAULTS.gcInterval');
46334725
ObjectsPlugin.Objects._DEFAULTS.gcInterval = 500;
4634-
helper.recordPrivateApi('write.Objects._DEFAULTS.gcGracePeriod');
4635-
ObjectsPlugin.Objects._DEFAULTS.gcGracePeriod = 250;
46364726

46374727
const objectsHelper = new ObjectsHelper(helper);
46384728
const client = RealtimeWithObjects(helper, clientOptions);
@@ -4644,6 +4734,11 @@ define(['ably', 'shared_helper', 'chai', 'objects', 'objects_helper'], function
46444734
await channel.attach();
46454735
const root = await objects.getRoot();
46464736

4737+
helper.recordPrivateApi('read.Objects.gcGracePeriod');
4738+
const gcGracePeriodOriginal = objects.gcGracePeriod;
4739+
helper.recordPrivateApi('write.Objects.gcGracePeriod');
4740+
objects.gcGracePeriod = 250;
4741+
46474742
// helper function to spy on the GC interval callback and wait for a specific number of GC cycles.
46484743
// returns a promise which will resolve when required number of cycles have happened.
46494744
const waitForGCCycles = (cycles) => {
@@ -4674,12 +4769,13 @@ define(['ably', 'shared_helper', 'chai', 'objects', 'objects_helper'], function
46744769
helper,
46754770
waitForGCCycles,
46764771
});
4772+
4773+
helper.recordPrivateApi('write.Objects.gcGracePeriod');
4774+
objects.gcGracePeriod = gcGracePeriodOriginal;
46774775
}, client);
46784776
} finally {
46794777
helper.recordPrivateApi('write.Objects._DEFAULTS.gcInterval');
46804778
ObjectsPlugin.Objects._DEFAULTS.gcInterval = gcIntervalOriginal;
4681-
helper.recordPrivateApi('write.Objects._DEFAULTS.gcGracePeriod');
4682-
ObjectsPlugin.Objects._DEFAULTS.gcGracePeriod = gcGracePeriodOriginal;
46834779
}
46844780
});
46854781

@@ -4920,7 +5016,7 @@ define(['ably', 'shared_helper', 'chai', 'objects', 'objects_helper'], function
49205016
*/
49215017
it('object message publish respects connectionDetails.maxMessageSize', async function () {
49225018
const helper = this.test.helper;
4923-
const client = RealtimeWithObjects(helper, { clientId: 'test' });
5019+
const client = RealtimeWithObjects(helper);
49245020

49255021
await helper.monitorConnectionThenCloseAndFinishAsync(async () => {
49265022
await client.connection.once('connected');

0 commit comments

Comments
 (0)