Skip to content

Commit e031374

Browse files
maeldonnSylvainSenechal
authored andcommitted
Upgrade microVersionId to timestamp-ordered format and add isReplica
Replace the random 8-byte hex microVersionId the timestamp-ordered identifier matching the versionId scheme, and add helpers for the crr cascade feature. Add an optional isReplica flag to ReplicationInfo to distinguish replica writes from user writes, enabling cascaded CRR. Issue: ARSN-578
1 parent 3582287 commit e031374

4 files changed

Lines changed: 147 additions & 32 deletions

File tree

lib/models/ObjectMD.ts

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as crypto from 'crypto';
21
import * as constants from '../constants';
32
import * as VersionIDUtils from '../versioning/VersionID';
43
import { VersioningConstants } from '../versioning/constants';
@@ -49,6 +48,7 @@ export type ReplicationInfo = {
4948
/** @deprecated in favor of per-backend dataStoreVersionId for multi-destination. */
5049
dataStoreVersionId?: string;
5150
isNFS?: boolean;
51+
isReplica?: boolean;
5252
};
5353

5454
export type ObjectMDTag = {
@@ -260,6 +260,7 @@ export default class ObjectMD {
260260
storageType: undefined,
261261
dataStoreVersionId: undefined,
262262
isNFS: undefined,
263+
isReplica: undefined,
263264
},
264265
dataStoreName: '',
265266
originOp: '',
@@ -1168,6 +1169,27 @@ export default class ObjectMD {
11681169
return this;
11691170
}
11701171

1172+
/**
1173+
* Mark this object as the result of a replication write (replica),
1174+
* as opposed to a write originating from a user request.
1175+
*
1176+
* @param isReplica - true if this object was written by replication
1177+
* @return itself
1178+
*/
1179+
setReplicationIsReplica(isReplica: boolean) {
1180+
this._data.replicationInfo.isReplica = isReplica;
1181+
return this;
1182+
}
1183+
1184+
/**
1185+
* Get whether this object was written by replication (replica).
1186+
* @return true if this object is a replica
1187+
*/
1188+
getReplicationIsReplica(): boolean {
1189+
return this._data.replicationInfo.isReplica === true
1190+
|| this._data.replicationInfo.status === 'REPLICA';
1191+
}
1192+
11711193
getReplicationSiteStatus(key: BackendKey): string | undefined {
11721194
return this._findBackend(key)?.status;
11731195
}
@@ -1348,35 +1370,28 @@ export default class ObjectMD {
13481370
}
13491371

13501372
/**
1351-
* Create or update the microVersionId field
1352-
*
1353-
* This field can be used to force an update in MongoDB. This can
1354-
* be needed in the following cases:
1355-
*
1356-
* - in case no other metadata field changes
1357-
*
1358-
* - to detect a change when fields change but object version does
1359-
* not change e.g. when ingesting a putObjectTagging coming from
1360-
* S3C to Zenko
1361-
*
1362-
* - to manage conflicts during concurrent updates, using
1363-
* conditions on the microVersionId field.
1364-
*
1365-
* It's a field of 16 hexadecimal characters randomly generated
1366-
*
1373+
* Update the microVersionId
1374+
*
1375+
* This field can be used to force an update in MongoDB when no other
1376+
* metadata field changes, to detect a change for CRR,
1377+
* and to manage conflicts during concurrent updates using conditions on this field.
1378+
*
1379+
* @param instanceId - instance identifier (e.g. config.instanceId)
1380+
* @param replicationGroupId - replication group ID (e.g. config.replicationGroupId)
13671381
* @return itself
13681382
*/
1369-
updateMicroVersionId() {
1370-
this._data.microVersionId = crypto.randomBytes(8).toString('hex');
1383+
updateMicroVersionId(instanceId: string, replicationGroupId: string) {
1384+
this._data.microVersionId = VersionIDUtils.generateVersionId(instanceId, replicationGroupId);
1385+
return this;
13711386
}
13721387

13731388
/**
1374-
* Get the microVersionId field, or null if not set
1389+
* Get the microVersionId field, or undefined if not set
13751390
*
1376-
* @return the microVersionId field if exists, or {null} if it does not exist
1391+
* @return the microVersionId field if set, or undefined if not
13771392
*/
13781393
getMicroVersionId() {
1379-
return this._data.microVersionId || null;
1394+
return this._data.microVersionId || undefined;
13801395
}
13811396

13821397
/**

lib/versioning/VersionID.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,34 @@ export function decode(str: string): string | Error {
369369
}
370370
return new Error(`cannot decode str ${str.length}`);
371371
}
372+
373+
export enum Ordering {
374+
OLDER = 'older',
375+
YOUNGER = 'younger',
376+
EQUAL = 'equal',
377+
NOT_COMPARABLE = 'notComparable',
378+
}
379+
380+
/**
381+
* Compare two raw (decoded) version IDs.
382+
*
383+
* Returns NOT_COMPARABLE when either value is absent or shorter than
384+
* LEGACY_BASE62_DECODED_LENGTH (rejects the old 16-char random-hex format).
385+
* Callers are responsible for deciding how to handle NOT_COMPARABLE.
386+
*/
387+
export function compare(
388+
v1: string | null | undefined,
389+
v2: string | null | undefined,
390+
): Ordering {
391+
if (
392+
typeof v1 !== 'string' || v1.length < LEGACY_BASE62_DECODED_LENGTH ||
393+
typeof v2 !== 'string' || v2.length < LEGACY_BASE62_DECODED_LENGTH
394+
) {
395+
return Ordering.NOT_COMPARABLE;
396+
}
397+
if (v1 === v2) {
398+
return Ordering.EQUAL;
399+
}
400+
401+
return v1 > v2 ? Ordering.OLDER : Ordering.YOUNGER;
402+
}

tests/unit/models/ObjectMD.spec.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ describe('ObjectMD class setters/getters', () => {
112112
storageType: undefined,
113113
dataStoreVersionId: undefined,
114114
isNFS: undefined,
115+
isReplica: undefined,
115116
},
116117
],
117118
[
@@ -132,6 +133,7 @@ describe('ObjectMD class setters/getters', () => {
132133
storageType: 'aws_s3',
133134
dataStoreVersionId: '',
134135
isNFS: undefined,
136+
isReplica: undefined,
135137
},
136138
],
137139
['DataStoreName', null, ''],
@@ -183,6 +185,26 @@ describe('ObjectMD class setters/getters', () => {
183185
});
184186
});
185187

188+
it('getReplicationIsReplica: returns true when status is REPLICA', () => {
189+
md.setReplicationStatus('REPLICA');
190+
assert.strictEqual(md.getReplicationIsReplica(), true,
191+
'status REPLICA alone can be used to determine isReplica');
192+
});
193+
194+
it('getReplicationIsReplica: survives status transition to PENDING (cascade)', () => {
195+
md.setReplicationInfo({
196+
status: 'REPLICA',
197+
isReplica: true,
198+
backends: [],
199+
content: [],
200+
destination: '',
201+
role: '',
202+
});
203+
md.setReplicationStatus('PENDING');
204+
assert.strictEqual(md.getReplicationIsReplica(), true,
205+
'isReplica must survive status changes');
206+
});
207+
186208
it('ObjectMD::setReplicationSiteStatus', () => {
187209
md.setReplicationInfo({
188210
backends: [
@@ -386,22 +408,22 @@ describe('ObjectMD class setters/getters', () => {
386408
});
387409

388410
it('ObjectMD::microVersionId unset', () => {
389-
assert.strictEqual(md.getMicroVersionId(), null);
411+
assert.strictEqual(md.getMicroVersionId(), undefined);
390412
});
391413

392414
it('ObjectMD::microVersionId set', () => {
393-
const generatedIds = new Set();
415+
const generatedIds = [];
394416
for (let i = 0; i < 100; ++i) {
395-
md.updateMicroVersionId();
396-
generatedIds.add(md.getMicroVersionId());
417+
md.updateMicroVersionId('instance', 'RG001');
418+
generatedIds.push(md.getMicroVersionId());
397419
}
398420
// all generated IDs should be different
399-
assert.strictEqual(generatedIds.size, 100);
400-
generatedIds.forEach(key => {
401-
// length is always 16 in hex because leading 0s are
402-
// also encoded in the 8-byte random buffer.
403-
assert.strictEqual(key.length, 16);
404-
});
421+
assert.strictEqual(new Set(generatedIds).size, 100);
422+
// microVersionIds use the versionId format (reversed time ordered):
423+
// newer values sort before older ones lexicographically.
424+
for (let i = 1; i < generatedIds.length; ++i) {
425+
assert(generatedIds[i] < generatedIds[i - 1]);
426+
}
405427
});
406428

407429
it('ObjectMD::set/getRetentionMode', () => {

tests/unit/versioning/VersionID.spec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const crypto = require('crypto');
12
const VID = require('../../../lib/versioning/VersionID');
23
const VersioningConstants = require('../../../lib/versioning/constants').VersioningConstants;
34
const assert = require('assert');
@@ -188,3 +189,49 @@ describe('test generating versionIds', () => {
188189
});
189190
});
190191
});
192+
193+
describe('compare', () => {
194+
const { compare, Ordering, generateVersionId } = VID;
195+
196+
it('should return NOT_COMPARABLE when existing is null', () => {
197+
assert.strictEqual(compare(generateVersionId('test', 'RG001'), null), Ordering.NOT_COMPARABLE);
198+
});
199+
200+
it('should return NOT_COMPARABLE when existing is undefined', () => {
201+
assert.strictEqual(compare(generateVersionId('test', 'RG001'), undefined), Ordering.NOT_COMPARABLE);
202+
});
203+
204+
it('should return NOT_COMPARABLE when existing is a legacy 16-char hex microVersionId', () => {
205+
const legacyHex = crypto.randomBytes(8).toString('hex');
206+
assert.strictEqual(legacyHex.length, 16);
207+
assert.strictEqual(compare(generateVersionId('test', 'RG001'), legacyHex), Ordering.NOT_COMPARABLE);
208+
});
209+
210+
it('should return NOT_COMPARABLE when incoming is a legacy 16-char hex microVersionId', () => {
211+
const legacyHex = crypto.randomBytes(8).toString('hex');
212+
assert.strictEqual(compare(legacyHex, generateVersionId('test', 'RG001')), Ordering.NOT_COMPARABLE);
213+
});
214+
215+
it('should return NOT_COMPARABLE for a string shorter than 27 chars', () => {
216+
assert.strictEqual(compare('tooshort', generateVersionId('test', 'RG001')), Ordering.NOT_COMPARABLE);
217+
});
218+
219+
it('should return EQUAL when incoming equals existing', () => {
220+
const raw = generateVersionId('test', 'RG001');
221+
assert.strictEqual(compare(raw, raw), Ordering.EQUAL);
222+
});
223+
224+
it('should return OLDER when incoming is older than existing', () => {
225+
const older = generateVersionId('test', 'RG001');
226+
const newer = generateVersionId('test', 'RG001');
227+
assert.ok(older > newer, 'test requires two distinct versionIds in order');
228+
assert.strictEqual(compare(older, newer), Ordering.OLDER);
229+
});
230+
231+
it('should return YOUNGER when incoming is newer than existing', () => {
232+
const older = generateVersionId('test', 'RG001');
233+
const newer = generateVersionId('test', 'RG001');
234+
assert.ok(older > newer, 'test requires two distinct versionIds in order');
235+
assert.strictEqual(compare(newer, older), Ordering.YOUNGER);
236+
});
237+
});

0 commit comments

Comments
 (0)