Skip to content

Add Crr Cascade capabilities to backbeat crr replication#2747

Merged
bert-e merged 2 commits into
development/9.5from
improvement/BB-767
Jul 16, 2026
Merged

Add Crr Cascade capabilities to backbeat crr replication#2747
bert-e merged 2 commits into
development/9.5from
improvement/BB-767

Conversation

@SylvainSenechal

@SylvainSenechal SylvainSenechal commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Issue: BB-767

Related PRs :
Arsenal : scality/Arsenal#2628
Cloudserver : scality/cloudserver#6179
CloudserverClient : scality/cloudserverclient#24
S3utils : scality/s3utils#395

@bert-e

bert-e commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Hello sylvainsenechal,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

Comment thread package.json Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
@claude

claude Bot commented Jun 3, 2026

Copy link
Copy Markdown
  • package.json:54 — @scality/cloudserverclient uses a local file path (file:../cloudserverclient/...). Must be changed to a proper registry or git-pinned reference before merge.
    - ReplicateObject.js:6 — checkCrrCascadeEvent and getMicroVersionId() do not appear to exist in arsenal 8.3.9. Arsenal version bump likely needed.
    - ReplicateObject.js:743 — Any 409 from destination putMetadata is assumed to be cascade-stale and marked COMPLETED. Consider using a more specific signal to avoid silently skipping replication if 409 is returned for other reasons.

    Review by Claude Code

@SylvainSenechal
SylvainSenechal marked this pull request as ready for review June 3, 2026 16:15
Comment thread extensions/replication/tasks/ReplicateObject.js

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can functional tests instead of just these,
But waiting for Arsenal/cloudserver to be merged, as it will be easier to make these tests (functional tests in backbeat rely on an image of cloudserver)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keeping unit test is good, functional test should just be an addition?

Comment thread package.json Outdated
@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.50%. Comparing base (f28a791) to head (d7f3303).
⚠️ Report is 2 commits behind head on development/9.5.

Files with missing lines Patch % Lines
extensions/replication/tasks/ReplicateObject.js 94.00% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

Files with missing lines Coverage Δ
extensions/replication/tasks/ReplicateObject.js 92.36% <94.00%> (+0.07%) ⬆️

... and 4 files with indirect coverage changes

Components Coverage Δ
Bucket Notification 80.22% <ø> (ø)
Core Library 81.60% <ø> (+0.01%) ⬆️
Ingestion 70.75% <ø> (+0.62%) ⬆️
Lifecycle 79.71% <ø> (ø)
Oplog Populator 85.83% <ø> (ø)
Replication 61.91% <94.00%> (+0.35%) ⬆️
Bucket Scanner 85.76% <ø> (ø)
@@                 Coverage Diff                 @@
##           development/9.5    #2747      +/-   ##
===================================================
+ Coverage            75.40%   75.50%   +0.09%     
===================================================
  Files                  201      201              
  Lines                13868    13898      +30     
===================================================
+ Hits                 10457    10493      +36     
+ Misses                3401     3395       -6     
  Partials                10       10              
Flag Coverage Δ
api:retry 9.07% <0.00%> (-0.03%) ⬇️
api:routes 8.85% <0.00%> (-0.02%) ⬇️
bucket-scanner 85.76% <ø> (ø)
ft_test:queuepopulator 10.97% <0.00%> (+0.57%) ⬆️
ingestion 12.33% <0.00%> (+0.03%) ⬆️
lib 7.77% <0.00%> (-0.04%) ⬇️
lifecycle 19.16% <0.00%> (-0.06%) ⬇️
notification 1.00% <0.00%> (-0.01%) ⬇️
oplogPopulator 0.13% <0.00%> (-0.01%) ⬇️
replication 18.79% <64.00%> (+0.05%) ⬆️
unit 54.41% <68.00%> (+0.65%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
@claude

claude Bot commented Jun 3, 2026

Copy link
Copy Markdown
  • package.json:54 — @scality/cloudserverclient uses a local file: path that will break CI and other developers. Must be changed to a proper npm or git-tag reference before merge.
    - extensions/replication/tasks/ReplicateObject.js:33-35 — Cascade sentinel objects (cascadeLoopDetected, cascadeDataComplete, partAlreadyAtDest) are plain {} singletons that pass through BackbeatTask.retry(), which mutates errors. Consider Object.freeze() to prevent accidental mutation.

    The cascade logic itself (putData/putMetadata detection, allPartsAlreadyAtDest propagation through the waterfall, _handleReplicationOutcome treating cascade signals as COMPLETED) is sound. The retry wrapper correctly passes cascade signals through without retrying (no retryable property). Tests cover the key scenarios well.

    Review by Claude Code

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread package.json Outdated
Comment thread package.json Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
@claude

claude Bot commented Jun 5, 2026

Copy link
Copy Markdown
  • ReplicateObject.js:602 — Cascade sentinel objects (cascadeLoopDetected, cascadeDataComplete) are passed as err to callbacks. Verify that BackbeatTask._retry won't retry on these truthy-but-non-error sentinels, which would defeat the loop/stale detection.
    - ReplicateObject.js:433 — On error path in _getAndPutData, destLocations may contain partial results. The sentinel objects ({}) pass the filter and could reach _deleteOrphans with undefined keys. Likely harmless due to downstream filtering, but worth a defensive check.
    - ReplicateObject.js:1014 — _processQueueEntryRetryFull now passes allPartsAlreadyAtDest as mdOnly to _putMetadata, changing retry-full semantics from always-full to conditionally-metadata-only. Confirm this is intentional.
    - package.json:57 — Arsenal pinned to a raw commit hash instead of a tag. Should be updated to a tag once the Arsenal PR merges.
    - package.json:54 — scality-cloudserverclient-v1.0.9.tgz vendored as a binary blob in git. Prefer a registry or git-tag reference.
    - ReplicateObject.js:573 — Trailing whitespace.

    Review by Claude Code

Comment thread package.json Outdated
Comment thread package.json Outdated
@SylvainSenechal
SylvainSenechal requested review from a team, benzekrimaha and maeldonn June 8, 2026 15:47
@scality scality deleted a comment from bert-e Jun 9, 2026
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
@SylvainSenechal
SylvainSenechal force-pushed the improvement/BB-767 branch 2 times, most recently from 38d87c8 to 29d498b Compare July 9, 2026 10:22
Comment on lines +732 to +735
if (err instanceof MicroVersionIdAlreadyStoredException ||
err instanceof StaleMicroVersionIdException) {
return cbOnce(err);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the condition here? Can't we just log the error (like all other errors) and pass it up in these case as well?
(the main difference is that we would get a ERROR log instead of INFO... and log here instead of in the continuation callback)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its nicer to keep, these arent unexpected errors, and they are logged already in the replication outcome.

I moved them with the if (err.ObjNotFound) above though, as its a similar situation : expected error

entry: destEntry.getLogInfo(),
error: destMvIdRaw.message,
});
return doneOnce(destMvIdRaw);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should still be a collision : even if we can't parse the microVersionID, we are still in the case "data already at destination", so we shoud still try to write the metadata anyway ?

(the only difference is that we can't really make the optimization of skipping the metadata updated if the microVersionId is already up to date on target)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I updated the code, this will be a situation where we still do a putMetadata.
I also moved the decoding to another place, more centralized

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment on lines +467 to +475
// Another replicant already stored the object data
// Compare microVersionIds to decide what to do with metadata:
// - source is newer : proceed in metadata-only mode
// - same or older : skip putMetadata
const destMvId = collisionResult.destMvIdRaw;
const srcMvId = sourceEntry.getMicroVersionId() || null;
const isLoop = srcMvId === destMvId;
const isStale = destMvId !== null &&
(srcMvId === null || srcMvId > destMvId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is where compareMicroVersionId shines, please use the arsenal function:

  • it makes the intent explicit (just comparing micro version id)
  • the function should handle the corner cases itself ("null" micro version id...)
  • we don't actually need to differentiate loop vs stale here

It would look something like this, i.e. we skip if we receive the destination micro version id (field present and no parse error) AND the micro version id is newer or equal at destination

const skipPutMetadata = destMvId !== undefined && compareMicroVersoinId(sourceEntry.getMicroVersionId(), destMvId) <= 0;
return cb(null, [], true, skipPutMetadata);

if (err instanceof VersionIdCollisionException) {
const destMvIdRaw = err.microVersionId
? decodeMicroVersionId(err.microVersionId) : null;
if (destMvIdRaw instanceof Error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I wonder if it is best to do the decoding here, or just pass the field "verbatim" in the part and let _getAndPutData() decode

  • it would avoid useless decoding of every parts' microVersionId
  • it would increase locally (
  • but the code here still needs to make sure if forwards both the microVersionId "value" AND the information that the field was indeed provided

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
// update location, replication status and put metadata in
// target bucket
(destLocations, next) => {
(destLocations, noNewDataLocations, skipMetadata, next) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these 2 parameters are not independent, there are actually only 3 cases:

  • no conflict (i.e. as before)
  • conflict with metadata already up to date
  • conflict but we have newer metadata which need to be replicated

not sure how best to handle this, maybe an object ("conflict") with the microVersionId : so we also improve locality by performing the microVersionId comparison (optimization) here?
undefined in the usual case (no conflict), don't even need the false like line 972
{ microVersionId: ... } (possibly with the "conflict" field you already have) otherwise ; skip if compareMicroVersionId() <= 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it with a conflict object and move some logic around into a helper function as we still need it in 2 different places

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
destEntry.setLocation(location);
this._putMetadata(destEntry, false, log, next);
(destLocations, noNewDataLocations, skipMetadata, next) => {
if (skipMetadata) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this block is duplicated and not trivial (it handles putData conflict resolution) : should be written just once... does it makes sense do do this in _putMetadata, by adding it another parameter for putData conflict ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna refactor this but I prefer extracting a helper functoin that putting it in putMetadata.
It looks very weird to call a function with an object that's used at the top of the function so that the function itself decides whether it should conitnue or not

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
const replicationContent = (mdOnly ? 'METADATA' : undefined);
// Send x-scal-replication-content so cloudserver know the putMetadata api
// is used in the context of a replication
const replicationContent = (mdOnly ? 'METADATA' : 'DATA,METADATA');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is required as otherwise, in non mdOnly situation, there is no replicationContent header, and cloudserver has no way no know we are calling putMetadata in the context of a replication.

Anyways i think sending data, metadata for non mdOnly should've been added earlier.

// holds this revision or a newer one. Returns false when there is no conflict,
// when the destination microVersionId is absent or can't be parsed (proceed
// conservatively), or when the source holds a newer revision.
_shouldSkipMetadata(sourceMicroVersionId, conflict, log) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discuss : Could be moved into putMetada so that we don't have to call this from 2 places, and instead inline it.

But imo it's a weird pattern to add an extra param to a function, then call that function and use that extra param at the top to decide to potentially not run this function

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either are fine with me.

  • while I agree on the param (especially the skip(bool skip) anti-pattern), in this case it seems fine to me : the parameter is not just an indication to skip, but really an input allowing to make the decision ("details of the conflict").
  • in my mind, putting it is putMetadata makes it a further from the anti-pattern : to me it is not skipping the metadata write, but just raising the abstraction level. PutMetadata's goal is to ensure the metadata is in the system, and it would gain the ability to reach the goal without actually making a write (kind of magic!) - similar in a way to how a getter could either read from the actual DB or quickly return the data from a cache.

(but either way may be better to keep _shouldSkipMetadata separate -just decide if it is called from within _putMetadata or before it- so each path can be demonstrated and tested easily)

@SylvainSenechal

Copy link
Copy Markdown
Contributor Author

The code is getting smaller after each review 😆
Retested in codespace after last push

}
}
const comparison = compareMicroVersionId(sourceMicroVersionId, destMvId);
return destMvId !== null &&

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May completely remove the null check if I end up update the compareMicroVersionId function.
Still reviewing cloudserver, haven't done it/decided yet

continue;
}
if (result.isCollision) {
collisionResult = collisionResult || result;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
collisionResult = collisionResult || result;
collisionResult ||= result;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually i dont think its supoprted currently :

Run yarn run --silent lint

/home/runner/work/backbeat/backbeat/extensions/replication/tasks/ReplicateObject.js
471:39 error Parsing error: Unexpected token =

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
// holds this revision or a newer one. Returns false when there is no conflict,
// when the destination microVersionId is absent or can't be parsed (proceed
// conservatively), or when the source holds a newer revision.
_shouldSkipMetadata(sourceMicroVersionId, conflict, log) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

either are fine with me.

  • while I agree on the param (especially the skip(bool skip) anti-pattern), in this case it seems fine to me : the parameter is not just an indication to skip, but really an input allowing to make the decision ("details of the conflict").
  • in my mind, putting it is putMetadata makes it a further from the anti-pattern : to me it is not skipping the metadata write, but just raising the abstraction level. PutMetadata's goal is to ensure the metadata is in the system, and it would gain the ability to reach the goal without actually making a write (kind of magic!) - similar in a way to how a getter could either read from the actual DB or quickly return the data from a cache.

(but either way may be better to keep _shouldSkipMetadata separate -just decide if it is called from within _putMetadata or before it- so each path can be demonstrated and tested easily)

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
});
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should add tests to verify _shouldSkipMetadata behavior directly in the different cases

  • no conflict
  • conflict with "invalid" microVersionId
  • conflict with missing microVersionId
  • conflict with newer/equal/older microVersionId
  • conflict with null microVersionId (either in conflict or in destination)

@bert-e

bert-e commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

@maeldonn maeldonn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor issues to fix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid callbacks on new isolated functions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I'm gonna do this in a separate pr where i will remove all callbacks from this file, ticket already opened and some work done on it

Comment thread extensions/replication/tasks/ReplicateObject.js Outdated
Comment thread extensions/replication/tasks/ReplicateObject.js Outdated

@benzekrimaha benzekrimaha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing more to add after mael and françois's comments. Approving

Comment thread extensions/replication/tasks/ReplicateObject.js
@SylvainSenechal
SylvainSenechal force-pushed the improvement/BB-767 branch 2 times, most recently from 08697b0 to dc8766f Compare July 15, 2026 14:10
@SylvainSenechal

Copy link
Copy Markdown
Contributor Author

/approve

@bert-e

bert-e commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

I have successfully merged the changeset of this pull request
into targetted development branches:

  • ✔️ development/9.5

The following branches have NOT changed:

  • development/7.10
  • development/7.4
  • development/7.70
  • development/8.6
  • development/9.0
  • development/9.1
  • development/9.2
  • development/9.3
  • development/9.4

This pull request did not target the following hotfix branch(es) so they
were left untouched:

  • hotfix/7.4.3
  • hotfix/7.6.0
  • hotfix/8.2.12
  • hotfix/9.0.4
  • hotfix/7.4.7
  • hotfix/7.70.1
  • hotfix/7.10.0
  • hotfix/7.2.0
  • hotfix/7.4.4
  • hotfix/7.8.0
  • hotfix/7.4.8
  • hotfix/7.70.15
  • hotfix/7.4.0
  • hotfix/7.4.1
  • hotfix/7.10.17
  • hotfix/7.9.0
  • hotfix/7.7.0
  • hotfix/9.0.7
  • hotfix/7.4.9
  • hotfix/7.4.5
  • hotfix/7.10.12
  • hotfix/7.4.10
  • hotfix/7.10.8
  • hotfix/7.10.1
  • hotfix/7.10.2
  • hotfix/7.10.3
  • hotfix/7.4.6
  • hotfix/7.70.12
  • hotfix/7.10.4
  • hotfix/7.4.2

Please check the status of the associated issue BB-767.

Goodbye sylvainsenechal.

The following options are set: approve

@bert-e
bert-e merged commit d7f3303 into development/9.5 Jul 16, 2026
24 checks passed
@bert-e
bert-e deleted the improvement/BB-767 branch July 16, 2026 16:48
@eve-ci-cd
eve-ci-cd Bot had a problem deploying to zenko/improvement/ZENKO-5263@2.16 July 16, 2026 17:49 Failure
@eve-ci-cd
eve-ci-cd Bot had a problem deploying to zenko/improvement/ZENKO-5263@2.16 July 16, 2026 18:04 Failure
@eve-ci-cd
eve-ci-cd Bot had a problem deploying to zenko/improvement/ZENKO-5253/multi-crr@2.16 July 17, 2026 09:06 Failure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants