Skip to content

fix: normalise archive timestamps before comparing them - #408

Open
delthas wants to merge 1 commit into
development/1.17from
bugfix/S3UTILS-246/cold-predicates-string-timestamps
Open

delthas wants to merge 1 commit into
development/1.17from
bugfix/S3UTILS-246/cold-predicates-string-timestamps

Conversation

@delthas

@delthas delthas commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Restore dates are typed Date | string, so, like cloudserver and backbeat do, we should properly handle string dates, by wrapping the var with new Date().


_isObjectCold, _isObjectRestoring and _isObjectRestored compare the archive timestamps directly with Date.now(), which returns a Number. A relational operator coerces both sides with ToNumeric:

  • Date <= Number → epoch milliseconds → compares correctly
  • "2025-01-05T11:42:42.771Z" <= NumberNaN → always false

Arsenal types all three fields Date | string (lib/models/ObjectMDArchive.ts:9-15) and its setters validate the value but store it unchanged, so a caller that round-trips the metadata through JSON writes ISO strings into MongoDB.

Behaviour, measured against the real predicates

case                                     cold   restoring restored   verdict
archived, never restored      (Date)     true   false     false      classified
archived, never restored      (string)   true   false     false      classified
restoring                     (Date)     false  true      false      classified
restoring                     (string)   false  false     false      *** counted as HOT ***
restored                      (Date)     false  false     true       classified
restored                      (string)   false  false     false      *** counted as HOT ***
restore expired               (Date)     true   false     false      classified
restore expired               (string)   false  false     false      *** counted as HOT ***

Only the never-restored case survives, because !restoreRequestedAt short-circuits before any comparison. Every object that has ever had a restore requested is affected.

Impact

:274-284 uses the flags to pick a counter suffix; all three false means no suffix, so the object is accumulated into plain masterCount/masterData, indistinguishable from a hot object. Also skipped: the destination-location attribution at :543 while restoring, and the cold-location attribution at :554 once restored.

Fix

Wrap each timestamp in new Date() before comparing. This is what every equivalent site elsewhere already does — s3utils was the only place comparing the raw value:

where comparison
cloudserver lib/api/apiUtils/object/coldStorage.js:83 new Date(objectMD.archive?.restoreWillExpireAt) < new Date(Date.now())
cloudserver lib/api/objectPut.js:277 Date.now() - new Date(objMD.archive.restoreRequestedAt)
backbeat extensions/lifecycle/tasks/LifecycleRetriggerRestoreTask.js:40 new Date(archive.restoreWillExpireAt) < new Date()
backbeat extensions/lifecycle/LifecycleQueuePopulator.js:477 new Date(md.archive.restoreWillExpireAt) < new Date()

Inlined at each of the five comparisons rather than behind a helper, since no repository defines one and new Date(x) at the call site is the established form. new Date(x) <= Date.now() and new Date(x).getTime() <= Date.now() were checked to agree for Date, ISO string, undefined, null and unparseable input.

The truthiness guards (!restoreRequestedAt, restoreCompletedAt &&) are untouched, which is what keeps behaviour identical for Date values and for objects with no archive.

Scope

Fixed in the consumer rather than in Arsenal, deliberately. The root enabler is that ObjectMDArchive's setters validate that a timestamp parses but store the caller's type as-is, so normalising to Date there would address the class everywhere — but that is a wider behavioural change across consumers which have not been surveyed, and cloudserver and backbeat already coerce defensively at their own call sites. Handling it here keeps the change small and matches the convention already in use across the codebase.

Tests

The existing fixtures were built exclusively with new Date(...), which is why this shipped. Now covered at both layers:

  • the cold-helper tests run over Date and ISO-string forms via describe.each, matching the test.each idiom already used in tests/unit/CountItems/, plus a new expired-restore case
  • two string-typed rows added to the _processEntryData table, which computes the flags through the real predicates and asserts location attribution

All five fail on the parent commit, with exactly the predicted consequences — the restoring row loses us-east-1 (the :543 attribution) and the restored row loses cold-location (the :554 one):

Expected: {"location": {"cold-location": 42, "us-east-1": 42}}
Received: {"location": {"cold-location": 42}}

The four Date variants pass both before and after, confirming the fix changes nothing on that path.

Affected range

Introduced by 0f26e06 (S3UTILS-155, 2024-03-25), first released in 1.14.6. Unchanged through 1.19.1.

Verification

yarn test:unit: 32 suites, 460 tests, all passing. eslint clean.

Issue: S3UTILS-246

@bert-e

bert-e commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Hello delthas,

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.

@scality scality deleted a comment from bert-e Sep 17, 2026
@bert-e

bert-e commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Request integration branches

Waiting for integration branch creation to be requested by the user.

To request integration branches, please comment on this pull request with the following command:

/create_integration_branches

Alternatively, the /approve and /create_pull_requests commands will automatically
create the integration branches.

@delthas
delthas force-pushed the bugfix/S3UTILS-246/cold-predicates-string-timestamps branch from 895e69e to c6d7872 Compare September 17, 2026 13:59
`_isObjectCold`, `_isObjectRestoring` and `_isObjectRestored` compared the
archive timestamps directly with `Date.now()`, which returns a Number. A
relational operator coerces both sides with ToNumeric, so a `Date` resolves
to its epoch milliseconds and compares correctly while an ISO string
resolves to `NaN` and every comparison is false.

Arsenal types `restoreRequestedAt`, `restoreCompletedAt` and
`restoreWillExpireAt` as `Date | string` and its setters validate the value
but store it unchanged, so a caller that round-trips the metadata through
JSON writes strings into MongoDB.

With string timestamps only the never-restored case survived, because
`!restoreRequestedAt` short-circuits before any comparison: every object
which had ever had a restore requested fell through all three predicates
and was accumulated into the plain masterCount/masterData counters as an
ordinary hot object. Its bytes were also left out of the destination
location while restoring, and out of the cold location once restored.

Wrap each timestamp in `new Date()` before comparing, which is what
cloudserver and backbeat already do at every equivalent site. Behaviour is
unchanged for `Date` values and for objects with no `archive`.

The existing tests built their fixtures exclusively with `new Date(...)`,
so they never exercised the string form; they are now run over both.

Issue: S3UTILS-246
@delthas
delthas force-pushed the bugfix/S3UTILS-246/cold-predicates-string-timestamps branch from c6d7872 to e49d7c6 Compare September 17, 2026 14:01
@delthas
delthas marked this pull request as ready for review September 17, 2026 14:03
@delthas
delthas requested review from a team, DarkIsDude and benzekrimaha September 17, 2026 14:04
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 45.30%. Comparing base (c318e92) to head (e49d7c6).

Additional details and impacted files
@@                 Coverage Diff                  @@
##           development/1.17     #408      +/-   ##
====================================================
+ Coverage             45.28%   45.30%   +0.02%     
====================================================
  Files                    88       88              
  Lines                  6486     6489       +3     
  Branches               1360     1363       +3     
====================================================
+ Hits                   2937     2940       +3     
  Misses                 3503     3503              
  Partials                 46       46              

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

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.

3 participants