Skip to content

CLDSRV-936 Lifecycle noncurrent expiration stalls when listing truncates on a bare master#6205

Merged
bert-e merged 3 commits into
development/9.3from
bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster
Jun 29, 2026
Merged

CLDSRV-936 Lifecycle noncurrent expiration stalls when listing truncates on a bare master#6205
bert-e merged 3 commits into
development/9.3from
bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster

Conversation

@nicolas2bert

Copy link
Copy Markdown
Contributor

The bug in one sentence
The lifecycle noncurrent listing returns the marker version-id-marker=null when a scan page stops on a "bare master" (an object with no internal versionId). CloudServer can't decode 'null', so the worker crashes. The bucket hasn't changed in months, so it crashes every cycle and the noncurrent versions are never expired.

How a bare master (null master) gets created

  • bucket created, not versioned yet.

  • PUT foo -> stored as a plain master key "foo", the value has NO versionId field (x-amz-version-id: "null"). <- this is the "bare master"

  • versioning ENABLED later on the bucket.

  • foo is NEVER re-PUT -> the bare master stays as-is, forever.
    So a bare master = an object written before versioning, never re-PUT afterward. If it had been overwritten, the system would have converted it into a normal null version (id 99999...,isNull2:true), which is harmless. To trigger the bug the bare master never has to be overwritten (which is the case).

How the noncurrent listing works in metadata (bucketd)
The bucket-processor asks CloudServer for a DelimiterNonCurrent listing, which hits bucketd.
bucketd scans the keys in order, newest->oldest per key.
For each key:
the first version seen = the current one -> not returned, but its last-modified is kept in memory.
the following versions = noncurrent -> returned, with a staleDate (= the in memory last-modified).

The staleDate is the last-modified of the version that replaced it, ie the moment that version became noncurrent. It's this staleDate that gets compared to NoncurrentDays. A noncurrent is only returned if staleDate < beforeDate, ie old enough.

Two limits bound each page:

  • max-keys (default 1000) -> number of noncurrent versions returned.

  • maxScannedLifecycleListingEntries (default 10000) -> number of entries scanned, so that a bucket full of current objects but with few noncurrents still responds quickly.

When a limit is hit -> IsTruncated=true + a continuation marker (NextKeyMarker + NextVersionIdMarker), and backbeat re-requests the next page with those markers, until IsTruncated=false.

The NextVersionIdMarker is computed as getVersionId() || 'null'. And that's exactly where it goes wrong: on a bare master, getVersionId() is undefined -> the marker becomes the string 'null'.

Example of the issue:

scan# key kind marker becomes
1 bar master (regular) 982... (real id)
2 bar \0 982... current version 982...
3 bar \0 981... non-current -> expirable 981...
... ... ~10k entries, all real ids ...
10000 foo BARE master (no versionId) getVersionId() || 'null' = "null"
>>> STOP (scanned 10000): IsTruncated=true,
NextKeyMarker="foo", NextVersionIdMarker="null" <-- the poison marker
10001 baz ... (next page - never reached)

The noncurrent versions to delete are on the next page, behind that marker.
Condition to trigger the issue: the 10000th scanned entry is a bare master.
If it were a normal version/master, the marker would be a valid id and nothing would break.

NOTE: Only scan-limit truncation can produce 'null', max-keys truncation always stops on a real noncurrent -> real id.

Why it's a bug
The marker round-trip is asymmetric. 'null' is guarded on the way out, not on the way in:
Page 1 version-id-marker= -> returns NextVersionIdMarker:"null" (encode handle 'null' properly-> OK)
Page 2 version-id-marker=null-> decode("null") = Error (decode does NOT handle 'null' properly) -> encode(Error) -> TypeError -> CloudServer worker crash.

bucketd answers page 2 without any problem (HTTP 200). The crash is 100% on the CloudServer side, while re-encoding the version-id-marker=null from the request itself.

Why it stays stuck
The processor gives up after its retries and moves on to something else. But the versions to expire are on page 2, which crashes every time. The bucket doesn't change -> every cycle page 1 re-emits 'null', page 2 re-crashes.

Net progress = zero, indefinitely -> "noncurrent versions never removed".

the fix : treat version-id-marker=null as "no marker" instead of blindly decoding it.

@bert-e

bert-e commented Jun 24, 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
/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

bert-e commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Branches have diverged

This pull request's source branch bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster has diverged from
development/9.3 by more than 50 commits.

To avoid any integration risks, please re-synchronize them using one of the
following solutions:

  • Merge origin/development/9.3 into bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster
  • Rebase bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster onto origin/development/9.3

Note: If you choose to rebase, you may have to ask me to rebuild
integration branches using the reset command.

@nicolas2bert nicolas2bert force-pushed the bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster branch from fc1d50e to 1602dc3 Compare June 24, 2026 09:09
@bert-e

bert-e commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Incorrect fix version

The Fix Version/s in issue CLDSRV-936 contains:

  • None

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

  • 9.3.12

  • 9.4.0

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

@nicolas2bert nicolas2bert force-pushed the bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster branch 2 times, most recently from 8b35ba5 to 02aaafa Compare June 24, 2026 09:13
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
8490 3 8487 0
View the full list of 3 ❄️ flaky test(s)
"after each" hook for "should fail if trying to overwrite a delete marker"::MPU with x-scal-s3-version-id header With default signature "after each" hook for "should fail if trying to overwrite a delete marker"

Flake rate in main: 100.00% (Passed 0 times, Failed 92 times)

Stack Traces | 0.014s run time
We encountered an internal error. Please try again.
"after each" hook for "should fail if trying to overwrite a delete marker"::MPU with x-scal-s3-version-id header With v4 signature "after each" hook for "should fail if trying to overwrite a delete marker"

Flake rate in main: 100.00% (Passed 0 times, Failed 84 times)

Stack Traces | 0.014s run time
We encountered an internal error. Please try again.
should create a bunch of objects and their versions::put and head object with versioning With v4 signature on versioning suspended then enabled bucket w/ null version should create a bunch of objects and their versions

Flake rate in main: 13.81% (Passed 668 times, Failed 107 times)

Stack Traces | 600s run time
Timeout of 600000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (.../test/versioning/objectHead.js)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Comment thread tests/unit/api/apiUtils/lifecycle.js Outdated
// a malformed (non-null) marker that decode rejects by returning an Error
const result = decodeVersionIdMarker('@@@bad@@@');
assert(result instanceof Error);
assert.strictEqual(result.is.InvalidArgument, true);

@BourgoisMickael BourgoisMickael Jun 24, 2026

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.

It could be better to compare value instead of boolean to see in failure message the actual vs expected value instread of false !== true

Suggested change
assert.strictEqual(result.is.InvalidArgument, true);
assert.strictEqual(result.message, 'InvalidArgument');

Comment thread tests/unit/api/apiUtils/lifecycle.js Outdated
Comment thread lib/api/apiUtils/object/lifecycle.js Outdated
@nicolas2bert nicolas2bert force-pushed the bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster branch from 02aaafa to 046ca1a Compare June 25, 2026 10:09
Apply Prettier formatting to the four files touched by this branch so the
prettier:diff CI check passes. Pure formatting, no logic changes.
@nicolas2bert nicolas2bert force-pushed the bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster branch from 046ca1a to 73e02ed Compare June 29, 2026 08:03
@nicolas2bert nicolas2bert force-pushed the bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster branch from b5dcbda to 73e02ed Compare June 29, 2026 08:52
@nicolas2bert

Copy link
Copy Markdown
Contributor Author

@bert-e approve

@bert-e

bert-e commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Incorrect fix version

The Fix Version/s in issue CLDSRV-936 contains:

  • None

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

  • 9.3.12

  • 9.4.0

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

The following options are set: approve

@nicolas2bert

Copy link
Copy Markdown
Contributor Author

ping

@bert-e

bert-e commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Conflict

A conflict has been raised during the creation of
integration branch w/9.4/bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster with contents from bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster
and development/9.4.

I have not created the integration branch.

Here are the steps to resolve this conflict:

 git fetch
 git checkout -B w/9.4/bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster origin/development/9.4
 git merge origin/bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster
 # <intense conflict resolution>
 git commit
 git push -u origin w/9.4/bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster

The following options are set: approve

@bert-e

bert-e commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Build failed

The build for commit did not succeed in branch w/9.4/bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster

The following options are set: approve

@bert-e

bert-e commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

In the queue

The changeset has received all authorizations and has been added to the
relevant queue(s). The queue(s) will be merged in the target development
branch(es) as soon as builds have passed.

The changeset will be merged in:

  • ✔️ development/9.3

  • ✔️ development/9.4

The following branches will NOT be impacted:

  • development/7.10
  • development/7.4
  • development/7.70
  • development/8.8
  • development/9.0
  • development/9.1
  • development/9.2

This pull request does not target the following hotfix branch(es) so they
will be left untouched:

  • hotfix/7.10.8
  • hotfix/7.4.8
  • hotfix/7.10.1
  • hotfix/7.10.3
  • hotfix/7.4.1
  • hotfix/7.4.6
  • hotfix/7.70.73
  • hotfix/9.0.7
  • hotfix/6.4.7
  • hotfix/7.2.0
  • hotfix/7.70.11
  • hotfix/7.10.15
  • hotfix/7.10.49
  • hotfix/7.70.21
  • hotfix/7.9.0
  • hotfix/7.4.9
  • hotfix/8.8.45
  • hotfix/9.0.32
  • hotfix/7.10.27
  • hotfix/7.4.3
  • hotfix/7.7.0
  • hotfix/7.4.10
  • hotfix/7.8.0
  • hotfix/9.2.24
  • hotfix/7.4.2
  • hotfix/7.6.0
  • hotfix/7.10.0
  • hotfix/7.10.2
  • hotfix/7.4.5
  • hotfix/7.4.0
  • hotfix/7.70.51
  • hotfix/7.10.28
  • hotfix/7.4.7
  • hotfix/7.10.30
  • hotfix/7.10.4
  • hotfix/7.4.4
  • hotfix/7.70.45

There is no action required on your side. You will be notified here once
the changeset has been merged. In the unlikely event that the changeset
fails permanently on the queue, a member of the admin team will
contact you to help resolve the matter.

IMPORTANT

Please do not attempt to modify this pull request.

  • Any commit you add on the source branch will trigger a new cycle after the
    current queue is merged.
  • Any commit you add on one of the integration branches will be lost.

If you need this pull request to be removed from the queue, please contact a
member of the admin team now.

The following options are set: approve

@bert-e

bert-e commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Queue build failed

The corresponding build for the queue failed:

  • Checkout the status page.
  • Identify the failing build and review the logs.
  • If no issue is found, re-run the build.
  • If an issue is identified, checkout the steps below to remove
    the pull request from the queue for further analysis and maybe rebase/merge.
Remove the pull request from the queue
  • Add a /wait comment on this pull request.
  • Click on login on the status page.
  • Go into the manage page.
  • Find the option called Rebuild the queue and click on it.
    Bert-E will loop again on all pull requests to put the valid ones
    in the queue again, while skipping the one with the /wait comment.
  • Wait for the new queue to merge, then merge/rebase your pull request
    with the latest changes to then work on a proper fix.
  • Once the issue is fixed, delete the /wait comment and
    follow the usual process to merge the pull request.

@bert-e

bert-e commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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

  • ✔️ development/9.3

  • ✔️ development/9.4

The following branches have NOT changed:

  • development/7.10
  • development/7.4
  • development/7.70
  • development/8.8
  • development/9.0
  • development/9.1
  • development/9.2

Please check the status of the associated issue CLDSRV-936.

Goodbye nicolas2bert.

@bert-e bert-e merged commit 73e02ed into development/9.3 Jun 29, 2026
162 of 169 checks passed
@bert-e bert-e deleted the bugfix/CLDSRV-936/listLifecycleNonCurrents-baremaster branch June 29, 2026 13:14
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