From 099477a4bc501d047580b34d927c8f5c51a936e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Fri, 11 Sep 2026 14:20:36 +0200 Subject: [PATCH 1/2] Bump arsenal for the ObjectMD copy constructor fix clone() no longer shares replicationInfo with the source entry. toCompletedEntry and toPendingEntry computed the global status from the source, which only worked through that sharing: compute it from the clone. Issue: ARSN-651 --- lib/models/ObjectQueueEntry.js | 25 +++++++++-------------- package.json | 2 +- tests/unit/replication/QueueEntry.spec.js | 13 ++++++------ yarn.lock | 14 ++++++------- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/lib/models/ObjectQueueEntry.js b/lib/models/ObjectQueueEntry.js index 0a565825a..d6c8676bc 100644 --- a/lib/models/ObjectQueueEntry.js +++ b/lib/models/ObjectQueueEntry.js @@ -212,22 +212,17 @@ class ObjectQueueEntry extends ObjectMD { }; } - _getGlobalReplicationStatus() { - const data = this.getValue(); - // Check the global status relative to the other backends - if (Array.isArray(data.replicationInfo.backends)) { - const statuses = data.replicationInfo.backends.map( - backend => backend.status); - // If any site replication failed, set the global status - // to FAILED. - if (statuses.includes('FAILED')) { - return 'FAILED'; + _setGlobalReplicationStatus() { + let status = 'COMPLETED'; + for (const backend of this.getReplicationBackends() ?? []) { + if (backend.status === 'FAILED') { + return this.setReplicationStatus('FAILED'); } - if (statuses.includes('PENDING')) { - return 'PROCESSING'; + if (backend.status === 'PENDING') { + status = 'PROCESSING'; } } - return 'COMPLETED'; + return this.setReplicationStatus(status); } toReplicaEntry(backend) { @@ -244,7 +239,7 @@ class ObjectQueueEntry extends ObjectMD { return this.clone() .setAccountId(this.getAccountId()) .setReplicationSiteStatus(backend, 'COMPLETED') - .setReplicationStatus(this._getGlobalReplicationStatus()) + ._setGlobalReplicationStatus() .setOriginOp('s3:Replication:OperationCompletedReplication'); } @@ -260,7 +255,7 @@ class ObjectQueueEntry extends ObjectMD { return this.clone() .setAccountId(this.getAccountId()) .setReplicationSiteStatus(backend, 'PENDING') - .setReplicationStatus(this._getGlobalReplicationStatus()) + ._setGlobalReplicationStatus() .setOriginOp('s3:Replication:OperationPendingReplication'); } diff --git a/package.json b/package.json index 496d3badd..f08c80f5d 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@scality/cloudserverclient": "^1.0.12", "@smithy/node-http-handler": "^3.3.3", "JSONStream": "^1.3.5", - "arsenal": "git+https://github.com/scality/arsenal#8.5.6", + "arsenal": "git+https://github.com/scality/arsenal#8.5.18", "async": "^2.3.0", "backo": "^1.1.0", "breakbeat": "scality/breakbeat#v1.0.3", diff --git a/tests/unit/replication/QueueEntry.spec.js b/tests/unit/replication/QueueEntry.spec.js index 601c658f6..38072f72c 100644 --- a/tests/unit/replication/QueueEntry.spec.js +++ b/tests/unit/replication/QueueEntry.spec.js @@ -62,14 +62,15 @@ describe('QueueEntry helper class', () => { assert.strictEqual(completed.getReplicationStatus(), 'PROCESSING'); // If all sites are COMPLETED, the global status should be COMPLETED - const completed1 = entry.toCompletedEntry({ site: 'sf' }); - const completed2 = entry.toCompletedEntry({ site: 'replicationaws' }); - assert.strictEqual(completed2 - .getReplicationSiteStatus({ site: 'replicationaws' }), + const allCompleted = entry.toCompletedEntry({ site: 'sf' }) + .toCompletedEntry({ site: 'replicationaws' }); + assert.strictEqual(allCompleted.getReplicationSiteStatus({ site: 'sf' }), 'COMPLETED'); - assert.strictEqual(completed1.getReplicationSiteStatus({ site: 'sf' }), + assert.strictEqual(allCompleted + .getReplicationSiteStatus({ site: 'replicationaws' }), 'COMPLETED'); - assert.strictEqual(completed1.getReplicationStatus(), 'COMPLETED'); + assert.strictEqual(allCompleted.getReplicationStatus(), 'COMPLETED'); + assert.strictEqual(entry.getReplicationSiteStatus({ site: 'sf' }), 'PENDING'); }); }); }); diff --git a/yarn.lock b/yarn.lock index 0e73e2c49..44d493873 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5500,9 +5500,9 @@ arraybuffer.prototype.slice@^1.0.4: optionalDependencies: ioctl "^2.0.2" -"arsenal@git+https://github.com/scality/arsenal#8.5.6": - version "8.5.6" - resolved "git+https://github.com/scality/arsenal#0db557930c7d13204167188a7503e6e00d154df5" +"arsenal@git+https://github.com/scality/arsenal#8.5.18": + version "8.5.18" + resolved "git+https://github.com/scality/arsenal#e0d5315a1bde8c94e24e1dad9129074333eddbcd" dependencies: "@aws-sdk/client-kms" "^3.975.0" "@aws-sdk/client-s3" "^3.975.0" @@ -5541,7 +5541,7 @@ arraybuffer.prototype.slice@^1.0.4: simple-glob "^0.2.0" socket.io "^4.8.0" socket.io-client "^4.8.0" - sproxydclient "github:scality/sproxydclient#8.2.1" + sproxydclient "github:scality/sproxydclient#8.2.2" utf8 "^3.0.0" uuid "^10.0.0" werelogs scality/werelogs#8.2.4 @@ -10554,9 +10554,9 @@ sprintf-js@~1.0.2: httpagent "github:scality/httpagent#1.1.0" werelogs scality/werelogs#8.2.0 -"sproxydclient@github:scality/sproxydclient#8.2.1": - version "8.2.1" - resolved "https://codeload.github.com/scality/sproxydclient/tar.gz/501829f5521787e7e946de6792f5806ffa6ec437" +"sproxydclient@github:scality/sproxydclient#8.2.2": + version "8.2.2" + resolved "https://codeload.github.com/scality/sproxydclient/tar.gz/62d3aac1c844beda2286f33a1a7c7f2a31937579" dependencies: async "^3.2.6" httpagent "github:scality/httpagent#1.1.0" From c0829c87652c2096291fd4eab7f3f5f68b184926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20DONNART?= Date: Fri, 11 Sep 2026 14:20:36 +0200 Subject: [PATCH 2/2] Keep only the targeted backend in the replica metadata toReplicaEntry cloned the source entry and only updated the targeted backend, so the destination metadata inherited the statuses of the source's other destinations, frozen at queue time and never refreshed. Keep the targeted backend alone. Issue: BB-875 --- lib/models/ObjectQueueEntry.js | 4 ++-- tests/functional/replication/queueProcessor.js | 8 -------- tests/unit/lib/models/ObjectQueueEntry.spec.js | 15 +++++++++++++++ tests/unit/replication/QueueEntry.spec.js | 4 ++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/models/ObjectQueueEntry.js b/lib/models/ObjectQueueEntry.js index d6c8676bc..68ec0d6f2 100644 --- a/lib/models/ObjectQueueEntry.js +++ b/lib/models/ObjectQueueEntry.js @@ -227,12 +227,12 @@ class ObjectQueueEntry extends ObjectMD { toReplicaEntry(backend) { const newEntry = this.clone(); - newEntry + return newEntry .setAccountId(this.getAccountId()) .setBucket(this.getReplicationTargetBucket(backend)) + .setReplicationBackends([newEntry._findBackend(backend)]) .setReplicationSiteStatus(backend, 'REPLICA') .setReplicationStatus('REPLICA'); - return newEntry; } toCompletedEntry(backend) { diff --git a/tests/functional/replication/queueProcessor.js b/tests/functional/replication/queueProcessor.js index b1ad76b5c..4f32154ef 100644 --- a/tests/functional/replication/queueProcessor.js +++ b/tests/functional/replication/queueProcessor.js @@ -599,14 +599,6 @@ class S3Mock extends TestConfigurator { site: 'sf', status: 'REPLICA', dataStoreVersionId: '', - }, { - site: 'replicationaws', - status: 'PENDING', - dataStoreVersionId: '', - }, { - site: 'toazure', - status: 'PENDING', - dataStoreVersionId: '', }], content: replicatedContent, destination: this.getParam('source.md.replicationInfo.destination'), diff --git a/tests/unit/lib/models/ObjectQueueEntry.spec.js b/tests/unit/lib/models/ObjectQueueEntry.spec.js index d9cfb96dc..02cdfc46d 100644 --- a/tests/unit/lib/models/ObjectQueueEntry.spec.js +++ b/tests/unit/lib/models/ObjectQueueEntry.spec.js @@ -144,6 +144,21 @@ describe('ObjectQueueEntry', () => { assert.strictEqual(replicaA.getBucket(), 'bucket-a'); assert.strictEqual(replicaB.getBucket(), 'bucket-b'); }); + + it('toReplicaEntry drops the other destinations statuses', () => { + const entry = _makeEntryWithBackends([ + { site: 'siteA', status: 'PENDING', dataStoreVersionId: '' }, + { site: 'siteB', status: 'PENDING', dataStoreVersionId: '' }, + ]); + + const replica = entry.toReplicaEntry({ site: 'siteB' }); + assert.deepStrictEqual( + replica.getReplicationBackends().map(b => b.site), ['siteB']); + assert.strictEqual(replica.getReplicationSiteStatus({ site: 'siteB' }), 'REPLICA'); + assert.strictEqual(replica.getReplicationStatus(), 'REPLICA'); + assert.strictEqual(entry.getReplicationBackends().length, 2); + assert.strictEqual(entry.getReplicationSiteStatus({ site: 'siteB' }), 'PENDING'); + }); }); describe('same-site backend disambiguation', () => { diff --git a/tests/unit/replication/QueueEntry.spec.js b/tests/unit/replication/QueueEntry.spec.js index 38072f72c..8dd992b8d 100644 --- a/tests/unit/replication/QueueEntry.spec.js +++ b/tests/unit/replication/QueueEntry.spec.js @@ -39,7 +39,7 @@ describe('QueueEntry helper class', () => { 'REPLICA'); assert.strictEqual( replica.getReplicationSiteStatus({ site: 'replicationaws' }), - 'PENDING'); + undefined); assert.strictEqual(replica.getReplicationStatus(), 'REPLICA'); // If one site is FAILED, the global status should be FAILED @@ -47,7 +47,7 @@ describe('QueueEntry helper class', () => { assert.strictEqual(failed.getReplicationSiteStatus({ site: 'sf' }), 'FAILED'); assert.strictEqual( - replica.getReplicationSiteStatus({ site: 'replicationaws' }), + failed.getReplicationSiteStatus({ site: 'replicationaws' }), 'PENDING'); assert.strictEqual(failed.getReplicationStatus(), 'FAILED');