Skip to content

Support multi-destination CRR with V2 replication configuration#2627

Open
maeldonn wants to merge 2 commits into
development/8.4from
improvement/ARSN-571/crr-multi
Open

Support multi-destination CRR with V2 replication configuration#2627
maeldonn wants to merge 2 commits into
development/8.4from
improvement/ARSN-571/crr-multi

Conversation

@maeldonn
Copy link
Copy Markdown
Contributor

Add V2 replication configuration support (Filter, Priority, per-rule Account) with multi-destination CRR. V1 remains fully supported and round-trips via a persisted format hint. Per-rule destination/role move into the Backend; legacy top-level ReplicationInfo fields kept as optional for backward-compatible reads.

Issue: ARSN-571

@maeldonn maeldonn requested review from a team, DarkIsDude and delthas May 13, 2026 17:14
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 13, 2026

Hello maeldonn,

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.

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 13, 2026

Incorrect fix version

The Fix Version/s in issue ARSN-571 contains:

  • None

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 8.4.2

Please check the Fix Version/s of ARSN-571, or the target
branch of this pull request.

Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts
@claude
Copy link
Copy Markdown

claude Bot commented May 13, 2026

  • _parsePriority validates isNaN(priority) || priority < 0 but the error message says "non-negative integer" — floats like 1.5 pass through. Add !Number.isInteger(priority) to the check.
    • See inline suggestion at lib/models/ReplicationConfiguration.ts:419
  • _parseAccount accepts any non-empty string. An Account value containing colons (e.g. foo:bar) will corrupt the ARN produced by _deriveDestinationRole, since it splices the account into position 4 of a colon-delimited ARN and re-joins. At minimum reject colons; ideally validate the 12-digit AWS account format.
    • See inline suggestion at lib/models/ReplicationConfiguration.ts:616

Review by Claude Code

@maeldonn maeldonn force-pushed the improvement/ARSN-571/crr-multi branch from a4421cd to a7bbbbc Compare May 13, 2026 17:47
Comment thread lib/models/ReplicationConfiguration.ts Outdated
_derivePerRuleRoles() {
const { Rule } = this._parsedXML.ReplicationConfiguration;
this._rules!.forEach((rule, i) => {
const account = Rule[i].Destination[0].Account?.[0];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_derivePerRuleRoles couples the _rules array ordering to the raw parsed XML Rule array by index. This works because _parseEachRule builds rules sequentially, but it is fragile — if rules are ever filtered, skipped, or reordered during parsing, the indices will silently mismatch and assign the wrong Account/role to rules.

Consider extracting the Account in _buildRuleObject (which already receives the XMLRule) and deriving the role there, so each rule carries its own Account context without depending on array-position alignment.

— Claude Code

Comment thread lib/models/ReplicationConfiguration.ts Outdated
'</ReplicationConfiguration>'
`<?xml version="1.0" encoding="UTF-8"?>` +
`<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">` +
`<Role>${escapeForXml(role)}</Role>` +
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getConfigXML now emits Role before Rule elements. The old V1 code emitted them after (rulesXML + Role). While XML parsers are order-independent, this changes the byte-level output for all existing V1 configs. If any downstream consumer (e.g. checksums, string-matching assertions, ETag comparisons) depends on the exact XML string, this could be a subtle regression.

— Claude Code

})).not.toThrow();
});
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No round-trip test for V2: the tests verify parsing and XML generation separately, but there is no test that parses a V2 XML, extracts the config via getReplicationConfiguration(), feeds it to getConfigXML(), and re-parses the output to verify data fidelity. A round-trip test would catch any discrepancy between the two paths (e.g. lost priorities, wrong Account, prefix differences).

— Claude Code

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.

round-trip test for both v1 and v2 would be nice... (including the "unknown" case)

@claude
Copy link
Copy Markdown

claude Bot commented May 13, 2026

  • _derivePerRuleRoles couples rule and XML arrays by index — fragile if rules are ever filtered or reordered during parsing - Consider moving Account extraction into _buildRuleObject to keep each rule self-contained- getConfigXML changes V1 XML element ordering (Role now before Rule elements instead of after) - Verify no downstream consumer depends on the byte-level XML output- No round-trip test for V2 (parse XML -> getReplicationConfiguration -> getConfigXML -> re-parse) - Add one to catch discrepancies between the parsing and serialization paths

    Review by Claude Code

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 97.02381% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.78%. Comparing base (2312744) to head (21b9bb3).
⚠️ Report is 10 commits behind head on development/8.4.

Files with missing lines Patch % Lines
lib/models/ReplicationConfiguration.ts 96.75% 5 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           development/8.4    #2627      +/-   ##
===================================================
+ Coverage            73.55%   73.78%   +0.22%     
===================================================
  Files                  222      222              
  Lines                18214    18314     +100     
  Branches              3774     3847      +73     
===================================================
+ Hits                 13398    13513     +115     
+ Misses                4811     4796      -15     
  Partials                 5        5              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 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.

@scality scality deleted a comment from bert-e May 13, 2026
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 13, 2026

Waiting for approval

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

  • the author

  • 2 peers

Copy link
Copy Markdown
Contributor

@francoisferrand francoisferrand left a comment

Choose a reason for hiding this comment

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

can be greatly simplified by not being strict in parsing:

  • always parse account
  • always parse priority
  • when parsing prefix, remember if it was in a or directly in the

This will always lots of back-and-forth and coupling with the _v2 variable in parsing, and simplify generation.

(And if we really want or need to be strict in parsing, we can add an extra validation at the end of parsing : after we have parsed everything, fail if format == v1 && account && priority. But IMHO not needed...)

Comment thread lib/models/ObjectMD.ts
Comment thread lib/models/ObjectMD.ts
Comment thread lib/models/ObjectMD.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Copy link
Copy Markdown
Contributor

@DarkIsDude DarkIsDude left a comment

Choose a reason for hiding this comment

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

Not much more than Francois 🙏

Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 18, 2026

Waiting for approval

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

  • the author

  • 2 peers

The following reviewers are expecting changes from the author, or must review again:

@delthas delthas removed their request for review May 18, 2026 14:04
@maeldonn maeldonn force-pushed the improvement/ARSN-571/crr-multi branch from a7bbbbc to acc58f0 Compare May 22, 2026 12:37
* pair; the destination side is used when present. If the rule carries
* an `account` override, its 12-digit ID replaces the account segment.
*/
static resolveDestinationRole(topRole: string, account?: string): string | undefined {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

resolveDestinationRole is a new public static method with non-trivial ARN-splitting and account-substitution logic, but has no direct unit tests. Consider adding tests for: single ARN without account, comma-separated pair, account override replacing the correct segment, and empty/falsy topRole.

— Claude Code

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 22, 2026

Incorrect fix version

The Fix Version/s in issue ARSN-571 contains:

  • 8.4.2

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 8.4.3

Please check the Fix Version/s of ARSN-571, or the target
branch of this pull request.

obj.storageClass = storageClass;
}

if (rule.Priority?.[0] !== undefined) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_parsePriority skips validation when rule.Priority[0] is an empty string (!"" is true), but _buildRuleObject still converts it to a number: +'' → 0. An empty element would silently set priority to 0 without validation.

```suggestion
if (rule.Priority?.[0] !== undefined && rule.Priority[0] !== '') {

@@ -144,6 +175,7 @@ export default class ReplicationConfiguration {
destination: this.getDestination(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getDestination() returns string | null (always null now since _destination is never set), but ReplicationConfigurationMetadata.destination is typed as string | undefined. Under strict: true, assigning null to string | undefined is a type error. Consider changing _destination to string | undefined and initializing to undefined, or adjusting the return type here.

— Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

  • Empty <Priority></Priority> silently becomes priority 0: _parsePriority skips empty strings but _buildRuleObject still parses them via +''
    • Guard with rule.Priority?.[0] !== undefined && rule.Priority[0] !== ''
  • getDestination() returns null but ReplicationConfigurationMetadata.destination is typed string | undefined — type mismatch under strict: true
    • Change _destination init to undefined instead of null, or adjust the metadata type
  • resolveDestinationRole is a new public static method with no direct test coverage
    • Add tests for single ARN, comma pair, account override, and empty input

Review by Claude Code

Copy link
Copy Markdown
Contributor

@DarkIsDude DarkIsDude left a comment

Choose a reason for hiding this comment

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

LGTM 🙏

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 27, 2026

Waiting for approval

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

  • the author

  • 2 peers

Comment thread lib/models/ObjectMD.ts Outdated
}

if (rule.Priority?.[0] !== undefined) {
obj.priority = +rule.Priority[0];
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.

would be more explicit with Number or parseInt

Suggested change
obj.priority = +rule.Priority[0];
obj.priority = Number(rule.Priority[0]);

const rules = instance.getRules();
expect(rules.length).toEqual(1);
expect(rules[0].prefix).toEqual('docs/');
expect(rules[0].destination).toEqual('arn:aws:s3:::crr-dest');
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.

        expect(instance.getFormat()).toEqual('v1');

rulesArr.push(this._buildRuleObject(rules[i]));
}

const hasAnyPriority = rulesArr.some(r => r.priority !== undefined);
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.

the rule applies in any case : when 2 rules have overlapping prefix, they MUST have either a different destination (i.e. region for AWS, storageClass for us) or different priority

i.e. algorithm may be to create one "bucket" for each destination/priority, and compare prefixes in the bucket (or do a NxN comparison: check each rule either does not have the same destination, priority or "startsWith" all other rules)

})).not.toThrow();
});
});
});
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.

round-trip test for both v1 and v2 would be nice... (including the "unknown" case)

Comment thread lib/models/ReplicationConfiguration.ts Outdated
const Status = `<Status>${enabled ? 'Enabled' : 'Disabled'}</Status>`;

const prefixContent = prefix === '' ? '<Prefix/>' : `<Prefix>${escapeForXml(prefix)}</Prefix>`;
const prefixXML = isV2 ? `<Filter>${prefixContent}</Filter>` : prefixContent;
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 HAVE TO specify a prefix (i.e. empty ) ?

if the user sends no prefix at all (neither v1 nor v2), this XML serialization would end up adding an empty filter/prefix : <Filer><Prefix/></Filter> instead of "nothing" ?

@maeldonn maeldonn force-pushed the improvement/ARSN-571/crr-multi branch from acc58f0 to 21b9bb3 Compare May 28, 2026 15:17
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 28, 2026

Incorrect fix version

The Fix Version/s in issue ARSN-571 contains:

  • 8.4.3

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 8.4.4

Please check the Fix Version/s of ARSN-571, or the target
branch of this pull request.

@maeldonn maeldonn force-pushed the improvement/ARSN-571/crr-multi branch 2 times, most recently from f682065 to fc0600f Compare May 29, 2026 16:14

const backends: Backend[] = [];
for (const item of byKey.values()) {
const existing = existingBackends?.find(b => b.site === item.site);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With multi-destination CRR, byKey can hold multiple entries for the same site with different destinations (tested in "keeps two CRR backends for same site, different destinations"). This find matches by site alone, so both iterations pick up the same existing backend and carry forward the wrong dataStoreVersionId.

Match on destination as well for CRR sites:

```suggestion
const existing = existingBackends?.find(b => {
if (b.site !== item.site) return false;
if (!isCloud(item.site)) {
return b.destination === item.destination;
}
return true;
});

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

  • resolveBackends: existingBackends lookup uses site only, but after dedup CRR backends can share a site with different destinations — the wrong dataStoreVersionId gets carried forward.
    • Also match on destination for non-cloud sites.

Review by Claude Code

@maeldonn maeldonn force-pushed the improvement/ARSN-571/crr-multi branch from fc0600f to c4207e1 Compare May 29, 2026 16:24
Comment thread lib/models/ObjectMD.ts
return this;
}

/** @deprecated in favor of per-backend role for multi-destination CRR. */
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Four public methods were removed here (setReplicationStorageType, setReplicationStorageClass, setReplicationTargetBucket, getReplicationDataStoreVersionId). As a shared library consumed by CloudServer and Backbeat, deleting public methods is a breaking API change that will fail downstream callers at runtime. Consider marking them @deprecated (like setReplicationRoles above) and keeping stubs that delegate or no-op, so consumers can migrate at their own pace.

— Claude Code


const backends: Backend[] = [];
for (const item of byKey.values()) {
const existing = existingBackends?.find(b => b.site === item.site);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In multi-destination CRR, two backends can share the same site but target different destinations (the dedup key is site|destination|role). This find matches on site alone, so the first existing backend for that site would be reused for all new backends of the same site — carrying forward the wrong dataStoreVersionId.

— Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

  • ObjectMD.ts:1192 — Four public methods removed (setReplicationStorageType, setReplicationStorageClass, setReplicationTargetBucket, getReplicationDataStoreVersionId). This is a breaking API change for downstream consumers. Deprecate and keep stubs instead of deleting.
    - ReplicationConfiguration.ts:308resolveBackends matches existing backends by site alone, but the dedup key for CRR backends includes destination and role. Two backends for the same site with different destinations would get the wrong dataStoreVersionId.

    Review by Claude Code

maeldonn added 2 commits May 29, 2026 18:50
Run prettier on the files modified by the upcoming
multi-destination CRR commit, so the logical changes land on a
pre-formatted base and the next commit's diff stays focused on
behavior rather than whitespace.

Issue: ARSN-571
Add V2 replication configuration format with Filter, Priority, and
per-rule Account/Destination fields. V1 format remains fully supported.

Issue: ARSN-571
@maeldonn maeldonn force-pushed the improvement/ARSN-571/crr-multi branch from c4207e1 to 644d6dd Compare May 29, 2026 16:56

const backends: Backend[] = [];
for (const item of byKey.values()) {
const existing = existingBackends?.find(b => b.site === item.site);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

existingBackends lookup matches on site only, but multi-destination CRR (which this PR enables) can produce multiple backends for the same site with different destinations. find(b => b.site === item.site) returns the first match, so the wrong dataStoreVersionId is carried forward when two existing backends share a site but differ in destination.

Suggested change
const existing = existingBackends?.find(b => b.site === item.site);
const existing = existingBackends?.find(b =>
b.site === item.site &&
(!item.destination || b.destination === item.destination)
);


— Claude Code

Comment thread lib/models/ObjectMD.ts
dataStoreVersionId: '',
destination: undefined,
storageClass: undefined,
role: undefined,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changing these defaults from '' to undefined is a breaking change for downstream callers. getReplicationRoles() (no args) previously always returned a string; now it returns undefined for new/default ObjectMD instances. Any code doing getReplicationRoles().split(',') will throw a TypeError. Same applies to getReplicationStorageType() and getReplicationStorageClass().

Consider keeping '' as the default here and only making the type optional, or updating all deprecated getters to fall back to '' (e.g. return this._data.replicationInfo.role ?? '').

— Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

  • Bug in resolveBackends existing-backend carry-forward: existingBackends?.find(b => b.site === item.site) matches on site only, but multi-destination CRR can have multiple backends for the same site with different destinations. The first match is always used, so the wrong dataStoreVersionId may be carried forward.
    • Also match on destination in the find predicate.
  • Breaking default change in ObjectMD._initMd: replication info fields (role, destination, storageClass, storageType, dataStoreVersionId) changed from '' to undefined. Deprecated getters like getReplicationRoles() now return undefined instead of '' for new ObjectMD instances, which will crash callers that call string methods on the result.
    • Either keep '' as the default, or add ?? '' fallbacks in the deprecated getters.

Review by Claude Code

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.

4 participants