Skip to content

S3C-11599 crrExistingObjects: write the multi-destination format for every bucket rule format - #418

Closed
nicolas2bert wants to merge 1 commit into
development/1.19from
bugfix/S3C-11599/crrExistingObjects-metadata-format
Closed

nicolas2bert wants to merge 1 commit into
development/1.19from
bugfix/S3C-11599/crrExistingObjects-metadata-format

Conversation

@nicolas2bert

@nicolas2bert nicolas2bert commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

problem
On S3C 10.1, crrExistingObjects marks objects for replication, but backbeat never replicates them. The objects stay PENDING.

The script chose how to write the object metadata from the format of the bucket replication rules. Both ways were wrong on S3C 10.1:

  • V1 bucket replication rules (Prefix, no Filter): we added the backend in the S3C 9.5 format, with no bucket and no role. Backbeat failed with BadRole, so the object stayed PENDING (S3C-11599).
  • V2 bucket replication rules (Filter): we set the top-level status to PROCESSING. The backbeat queue populator only queues objects that are PENDING, so backbeat never saw the object (S3C-11596).

Since S3C 10.1, cloudserver writes the same format for every bucket, whatever the rule format: one backend per destination, each with its own bucket and role. The rule format only changes where the prefix is (Prefix or Filter.Prefix).

Example used below:

bucket Role: '...:role/crr-src,...:role/crr-dst'
bucket replication rules:
  r1  Prefix ''       site sf,  bucket backup-sf
  r2  Prefix 'docs/'  site dr2, bucket backup-dr2
objects:
  'docs/a.pdf': already replicated to site sf, bucket backup-sf (COMPLETED)
  'docs/b.pdf': never replicated

fix
We now write the object metadata like cloudserver does on a PUT (arsenal resolveBackends), whatever the rule format:

  1. We take the rules that match the key. They give the destinations: site, bucket and role.
    Example: 'docs/a.pdf' matches r1 and r2, so we get site sf, bucket backup-sf, and site dr2, bucket backup-dr2.
  2. For each destination, we find the object backend with the same site, bucket and role. Its status tells TARGET_REPLICATION_STATUS what to pick.
    Example: site sf, bucket backup-sf is COMPLETED. Site dr2, bucket backup-dr2 has no backend, so NEW picks it.
  3. We write the picked destinations PENDING, with the bucket and role of the rule. We leave the other backends as they are.
    Example: we add site dr2, bucket backup-dr2, role crr-dst, PENDING. The site sf backend stays COMPLETED.
  4. We set the top-level status to PENDING, so backbeat queues the object. We compute the content when it's empty: the object was put before the rule existed.

We also fix two cases:

  • V2 bucket replication rules without StorageClass: we use SITE_NAME as their site (the S3C default site), as we already did for V1 bucket replication rules.
    Example: say r2 is a V2 bucket replication rule with no StorageClass, and we run with SITE_NAME=dr2 and NEW. 'docs/b.pdf' now gets site dr2, bucket backup-dr2. Before, we skipped it.
  • one site with several buckets (one rule per bucket, new in S3C 10.1): each bucket has its own backend and status.
    Example: say r2 sends to site sf, bucket backup-sf-2. 'docs/a.pdf' has site sf, bucket backup-sf, COMPLETED, and site sf, bucket backup-sf-2, FAILED. A FAILED run now retries site sf, bucket backup-sf-2 only. Before, we matched on the site only, so we saw one status for site sf, and could retry the wrong bucket, or none.

behavior changes
They are all in the README, with examples.

  • s3utils 1.19 runs on S3C 10.1 and later only.
    Before: the format we wrote depended on the rule format. With V1 bucket replication rules, we wrote the S3C 9.5 format.
    Why: since S3C 10.1, backbeat expects each backend to carry its own bucket and role, and S3C 9.5 doesn't understand that. Keeping both formats means keeping the write path that broke (S3C-11599).
  • SITE_NAME not set: we requeue every site of the rules that match the key.
    Before: with V1 bucket replication rules, we only requeued the site of the first rule, with a warning. With V2 bucket replication rules, nothing changes: we already requeued every site.
    Example: we run with NEW. 'docs/b.pdf' matches r1 and r2, so we now add site sf, bucket backup-sf, and site dr2, bucket backup-dr2. Before, with V1 bucket replication rules, we only added site sf.
    Why: the rule format only changes where the prefix is, so it must not change which sites we requeue. The old V1 code took one site per bucket as input, so without SITE_NAME it used the site of the first rule. Now one function handles both formats with resolveBackends, like cloudserver on a PUT.
  • a rule has no StorageClass: SITE_NAME is required. Without it, the run fails with "missing SITE_NAME".
    Before: with V1 bucket replication rules, we fell back to the site of the first rule, and only failed when the first rule had no StorageClass. With V2 bucket replication rules, we didn't fail: we ignored the rule.
    Example: say r2 has no StorageClass, and SITE_NAME is not set. The run now fails. Before, with V1 bucket replication rules, we used site sf from r1 for every key, so 'docs/b.pdf' never went to the S3C default site.
    Why: a rule without StorageClass goes to the S3C default site. That site is in the federation config, which the script can't read, so only SITE_NAME gives it. Falling back to the site of the first rule sent objects to the wrong site: we fail instead of guessing.
  • SITE_NAME is not a site of the rules, and every rule names its site: the run doesn't stop. We skip the objects with no backend for SITE_NAME, and warn once per bucket. We still requeue the objects that have one.
    Before: with V1 bucket replication rules, the run stopped at the first object with no backend for SITE_NAME. With V2 bucket replication rules, we skipped the objects without a warning.
    Example: we run with SITE_NAME=dr3 and NEW. We skip 'docs/a.pdf' and 'docs/b.pdf', and log "SITE_NAME is not a site of the bucket rules". Before, with V1 bucket replication rules, the run stopped at 'docs/a.pdf'.
    Why: no rule gives a backend for SITE_NAME, so we have nothing to write. The stop protects objects we are about to write a wrong site to (S3UTILS-250): here there are none. It also stopped runs for nothing: a FAILED run stopped on objects with nothing to retry. The warning says why few or no objects are updated.
  • keys outside every rule prefix: we skip them.
    Before: with V1 bucket replication rules, we marked them PENDING, and backbeat refused them ("replication disabled for object"). With V2 bucket replication rules, we already skipped them.
    Example: say r1 has Prefix 'photos/' instead of ''. 'notes.txt' matches no rule, so we skip it. Before, with V1 bucket replication rules, we marked it PENDING on site sf, and it stayed PENDING.
    Why: bug. Backbeat refuses these objects, so they stayed PENDING forever. Cloudserver and the old V2 code already skipped them.
  • FORCE_USING_CONFIGURATION: we ignore it and warn. We always use the current bucket replication config (bucket and role).
    Before: we kept the role the object was sent with, unless FORCE_USING_CONFIGURATION was set.
    Example: say the bucket Role is now '...:role/crr-src,...:role/crr-dst-new'. We run with COMPLETED. 'docs/a.pdf' gets site sf, bucket backup-sf, role crr-dst-new, PENDING. Before, with V1 bucket replication rules and without FORCE_USING_CONFIGURATION, backbeat sent it again with the old role crr-dst.
    Why: keeping the old bucket and role made backbeat fail with BadRole after a config change (S3UTILS-208). Each backend now gets its bucket and role from the current rule, so the flag has nothing left to do.
  • a rule moved to a new bucket: use TARGET_REPLICATION_STATUS=NEW to copy the existing objects there. We keep the backend for the old bucket. COMPLETED only requeues the buckets the objects were already sent to.
    Before: we matched backends on the site only. NEW skipped the object, because the site already had a backend. COMPLETED with FORCE_USING_CONFIGURATION=true sent it to the new bucket, but replaced the backend for the old bucket.
    Example: say r1 now uses site sf, bucket backup-sf-new. We run with NEW. 'docs/a.pdf' gets site sf, bucket backup-sf-new, PENDING, and keeps site sf, bucket backup-sf, COMPLETED. Before, NEW skipped it.
    Why: since S3C 10.1, one site can have several buckets, so we match backends on site, bucket and role, not on the site only. A new bucket is a new destination, so NEW picks it. Before, overwriting the old backend lost the record of the copy in the old bucket.
  • STORAGE_TYPE: we ignore it. Backbeat reads the location type from the location config of the site.
    Before: with V1 bucket replication rules, we wrote it to the top-level storageType of the object.
    Why: the S3C 10.1 format has no top-level storageType: there is nowhere to write it.

Issue: S3C-11599, S3C-11596

@bert-e

bert-e commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Hello nicolas2bert,

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 ⭐
/bypass_source_branch_lineage Bypass the cross-branch contamination check ⭐
/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.
/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

bert-e commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Incorrect Jira project

The Jira issue S3C-11599 specified in the source
branch name, does not belong to project S3UTILS.

Comment thread CRR/ReplicationStatusUpdater.js Outdated
enabled: r.Status === 'Enabled',
prefix: r.Filter?.Prefix ?? r.Prefix ?? '',
storageClass: r.Destination.StorageClass,
storageClass: r.Destination.StorageClass || this.siteName,

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 mixed rules, this sends objects to the wrong bucket. Example: r1 has no StorageClass (default site dr, bucket dr-bucket), r2 has StorageClass sf (bucket sf-bucket), and SITE_NAME=sf is set to requeue sf. r1 is then mapped to site sf, so resolveBackends returns a candidate {site: sf, destination: dr-bucket}. No existing backend matches it, so NEW picks it and backbeat copies the object to dr-bucket through site sf. Before this PR, r1 had no site and resolveBackends ignored it. Only use SITE_NAME for rules without StorageClass when no other rule names SITE_NAME, e.g. !this._getConfiguredSites(Rules).includes(this.siteName). Please add a unit test for this case.

@codecov

codecov Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 47.23%. Comparing base (ff7bcf6) to head (5ef47db).

Files with missing lines Patch % Lines
CRR/ReplicationStatusUpdater.js 98.63% 1 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                  @@
##           development/1.19     #418      +/-   ##
====================================================
+ Coverage             47.14%   47.23%   +0.09%     
====================================================
  Files                    90       90              
  Lines                  6809     6798      -11     
  Branches               1460     1460              
====================================================
+ Hits                   3210     3211       +1     
+ Misses                 3543     3532      -11     
+ Partials                 56       55       -1     

☔ View full report in Codecov by Harness.
📢 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.

@nicolas2bert
nicolas2bert force-pushed the bugfix/S3C-11599/crrExistingObjects-metadata-format branch from ef8c01a to 5890841 Compare October 28, 2026 13:04
Comment thread CRR/ReplicationStatusUpdater.js Outdated
return !replicationInfo?.destination
|| candidate.destination === replicationInfo.destination;
}
return existing.destination === candidate.destination && existing.role === candidate.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.

Matching on role breaks requeues after the bucket Role changes (rotated role, or a new Destination.Account). The existing backend keeps the old role, so it no longer matches the candidate. Result: TARGET_REPLICATION_STATUS=FAILED/COMPLETED skip the object silently, and NEW adds a second backend for the same site and bucket. That goes against step 2 ("requeue with the current bucket and role", S3UTILS-208). Match on site and destination only. The picked backend gets the candidate's role anyway.

Suggested change
return existing.destination === candidate.destination && existing.role === candidate.role;
return existing.destination === candidate.destination;

@nicolas2bert
nicolas2bert force-pushed the bugfix/S3C-11599/crrExistingObjects-metadata-format branch from 5890841 to b7b5bd5 Compare October 28, 2026 16:36
// Example: NEW picks dr2. COMPLETED would pick sf.
const pairs = candidateBackends.map(candidate => {
const existing = existingBackends.find(e => this._isSameBackend(e, candidate, prev));
const selected = this._statusShouldBeProcessed(existing?.status);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TARGET_REPLICATION_STATUS=REPLICA now checks the status of the matched backend, not the object status. In the documented DR failback (README "Re-sync a DR site back to the primary site"), the candidate is the primary site, and the replica has no backend for it (only dr=REPLICA). So existing is undefined, REPLICA never matches, and every replica is skipped without a word. Match REPLICA on the object instead, e.g. filter === 'REPLICA' ? prev?.status === 'REPLICA' && !existing : ..., and add a unit test for the failback case.

@nicolas2bert
nicolas2bert force-pushed the bugfix/S3C-11599/crrExistingObjects-metadata-format branch from b7b5bd5 to 75908d5 Compare October 28, 2026 20:16
// Example: NEW picks dr2. COMPLETED would pick sf.
const pairs = candidateBackends.map(candidate => {
const existing = existingBackends.find(e => this._isSameBackend(e, candidate, prev, true))
|| existingBackends.find(e => this._isSameBackend(e, candidate, prev));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The role-free fallback can take a backend that another candidate already matched by role. Example: rule A (account 111) replicated the object to site sf, bucket d1. Then rule B (account 222, same site and bucket) is added. With NEW, candidate B falls back to A's COMPLETED backend, so it isn't picked and B never gets a backend. In the fallback, skip existing backends whose role matches a candidate's role (or that another pair already claimed), and add a unit test for this case.

@nicolas2bert
nicolas2bert force-pushed the bugfix/S3C-11599/crrExistingObjects-metadata-format branch from 75908d5 to cd61e91 Compare October 28, 2026 20:52
…rmat

crrExistingObjects picked its write path from the bucket rule format. Rules
without Filter (V1) got the old single-destination format: a new backend had
no bucket and no role. Since S3C 10.1, cloudserver writes the multi-destination
format for every bucket, whatever the rule format, so backbeat failed with
BadRole and the object stayed PENDING (S3C-11599). The rule format only
changes where the prefix is. s3utils 1.19 runs on S3C 10.1 and later only.

- one write path for every bucket: bucket and role per backend, from the bucket rules (arsenal resolveBackends, like cloudserver on a PUT)
- top-level status PENDING: the backbeat queue populator only queues PENDING objects. PROCESSING left objects of V2 buckets never replicated (S3C-11596)
- compute content when it is empty (object put before the rule)
- rule without StorageClass: SITE_NAME is the S3C default site. Fails with "missing SITE_NAME" when it's not set
- SITE_NAME unset: every site of the rules, instead of the site of the first rule
- keys outside every rule prefix are skipped: backbeat refuses them anyway
- match object backends on site, bucket and role: one site can have several buckets since S3C 10.1. Backends written by S3C 9.5 match on the site
- don't add a rule site that TARGET_REPLICATION_STATUS doesn't pick, keep the backends this run doesn't pick as they are
- top-level role: source role from the bucket config. Keep the object role and destination while a backend written by S3C 9.5 is kept: backbeat needs them for it
- FORCE_USING_CONFIGURATION is ignored: the bucket config is always used (S3UTILS-208)
- warn when SITE_NAME is not a site of the bucket rules
- keep the unknown-site check (S3UTILS-250), the SITE_NAME requeue of a site the rules don't name anymore, and isReplica
- STORAGE_TYPE is ignored
@nicolas2bert
nicolas2bert force-pushed the bugfix/S3C-11599/crrExistingObjects-metadata-format branch from cd61e91 to 5ef47db Compare October 29, 2026 08:45
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.

2 participants