Skip to content

Handle retention locks on SM side#4834

Merged
Michal-Leszczynski merged 11 commits into
masterfrom
ml/1915-retention-lock-sm-side
May 21, 2026
Merged

Handle retention locks on SM side#4834
Michal-Leszczynski merged 11 commits into
masterfrom
ml/1915-retention-lock-sm-side

Conversation

@Michal-Leszczynski

@Michal-Leszczynski Michal-Leszczynski commented May 21, 2026

Copy link
Copy Markdown
Collaborator

This PR adds new StageRetentionLock to SM backup procedure.
This stage is configured according to RetentionLockMode, OverrideRetentionLock, RetentionDays backup Target fields (not yet exposed via sctool flags).

Most things are straight forward and described in commit messages, but here are a few considerations that could be discussed:

  • on SM agent rclone side, we do best effort approach to object locking, on SM server side we do fail fast approach - is it a problem?
  • no progress resume for StageRetentionLock - it always starts from scratch
  • no sctool progress display for StageRetentionLock - progress can be tracked only via metrics
  • StageRetentionLock operates for up to 12 nodes in parallel to limit the amount of manifests stored in memory - can be fixed in the future, but is this a problem?
  • no clean handling of permission and lock configuration errors - UX is going to be improved in next PRs
  • google SDK for GCS (big addition to vendor) added just because of test code - is this a problem?

Fixes https://scylladb.atlassian.net/browse/CLOUD-1915

Parallel manifest upload is limited to 12 to avoid memory
issues caused by storing all manifests in memory at the same
time. This is a sane guardrail that should be exported and
used whenever we are worried about going OOM on SM side.

Note that this limitation is not really respected in other
places in the code (we store snapshotDirs for all nodes in
memory, restore loads all manifests to memory), but we should
aim not to increase the potential risk of running OOM if we
can prevent it, and we don't expect big performance penalty.

One such case could be creating retention locks on backed
up files, where we expect this operation to be way shorter
than the upload of backed up files, so it shouldn't introduce
noticeable performance regression.
As discovered in:
#4659 (comment),
rclone server reads the whole req body into memory before
calling specific handlers. Because of that, there is a limit
to a single req body size, so that agent does not go OOM.

This commit adds a guardrail describing max safe sstable
component file cnt that can be sent in a single req body,
so that it doesn't hit the rclone side limit.

It should be used in all places which call rclone endpoints
accepting sstable component list of potentially any size.
One of such places will be the new clone/operations/retention-lock
endpoint used for locking backup files in batches.

Refs #4659
This commit adds a separate backup stage that will be
responsible for applying retention locks on all files
of and already finalized backup.
This commit adds new backup metric responsible for tracking
progress of already locked backup files.
@Michal-Leszczynski Michal-Leszczynski changed the title Ml/1915 retention lock sm side Handle retention locks on SM side May 21, 2026
@Michal-Leszczynski
Michal-Leszczynski force-pushed the ml/1915-retention-lock-sm-side branch from 2a13aff to 0fae07d Compare May 21, 2026 10:56
This commit introduces retention lock configuration
to backup properties and target. It also defines
their validation and default values.
This commit adds StageRetentionLock, which is responsible
for creating retention locks on finalized backup files.

To make it resumable, it can't rely on existing snapshotDirs,
as they might be empty or incomplete after resume, as they
only represent the files that were uploaded in the current
task run. This means it needs to fetch the already uploaded
manifests from backup location and discover the files that
needs to be locked from them.

The SM side implementation is works on a fail fast basis,
so it works differently than the SM agent side implementation
of retention lock endpoint which works on best effort basis.
We lack a mechanism for resuming the retention lock progress
on resume, so the best effort approach does not give us that
many benefits for now. It doesn't seem important to align
them on SM and SM agent side for now, but it's something to
take into consideration.

SM memory consideration:

Retention locks operate on per-file basis,
so at some point we need store file names in memory.
The current most memory safe way of iterating over
manifest files is by downloading manifest Index into
a tmp file and iterating over FilesMeta one by one.
FilesMeta contains all files from a single node from
a single table, so it's potentially still in millions.

That's why the cross node parallelism is limited to 12
(as when uploading the manifests), so that we don't end
up storing all file names from all nodes in memory at
the same time, which could lead into memory issues for
bigger clusters.

This can be improved by refactoring ForEachIndexIterWithError
(or just adding new iter method), so that it would allow to
call callback on a batch of read files, but this requires
additional work and should be done as a separate effort if needed.
Then, we could either remove the limitation or greatly increase
it.

SM agent memory considerations:

Since rclone server reads the whole req body into memory
before processing the req, it limits the max body size
to 1MB. We need to be aware of this on SM side, as sending
all file names of given table in a single req body could
easily go over such limit and fail, as it was shown in #4659.

Because of that, retention lock jobs are batched to send
up to 10k files in a single req. If ForEachIndexIterWithError
is ever changed to work per batch basis, we should use the
same batch size here as well.

Fixes https://scylladb.atlassian.net/browse/CLOUD-1915
Since we lack rclone endpoint for checking file retention lock
configuration, we will need to check it directly from fake GCS
server with GCS SDK. This commit adds it to our vendored deps.
The hidden import is temporarily added so that 'go mod tidy'
does not remove this dep from our go.mod.

As it turned out, our *.dev .gitignore rule prevented us from
vendoring the new deps referring to packages like cel.dev/expr.
To fix that, .gitignore has been updated to always include
entire vendor dir.
@Michal-Leszczynski
Michal-Leszczynski force-pushed the ml/1915-retention-lock-sm-side branch from 0fae07d to bc398f0 Compare May 21, 2026 11:24
This commit adds the following tests validating
retention lock backup:
- TestBackupRetentionLockIntegration - checking that
consecutive backups succeeds and apply correct locks
- TestBackupResumeOnRetentionLockStageIntegration -
checking that backup interrupted when applying locks
can be safely resumed and succeed
- TestRetentionLockUntil - simple unit test checking
that the retention period is calculated correctly
This commit modifies smoke tables and schema restore
tests so that they operate on backup with retention
locks to verify that they don't impact restore procedure
in any way.
@Michal-Leszczynski
Michal-Leszczynski force-pushed the ml/1915-retention-lock-sm-side branch from bc398f0 to 5b09a7c Compare May 21, 2026 11:52
@Michal-Leszczynski
Michal-Leszczynski marked this pull request as ready for review May 21, 2026 13:25
@Michal-Leszczynski

Copy link
Copy Markdown
Collaborator Author

@copilot review this PR

Copilot AI commented May 21, 2026

Copy link
Copy Markdown
Contributor

@copilot review this PR

Here's my review of the PR:

Overall: The implementation is well-structured and follows the existing patterns in the codebase. The new StageRetentionLock stage fits naturally into the backup pipeline, and the code is clear and readable.

Positive aspects:

  • Good separation of concerns: retentionLockConfig, batching logic, and per-host/per-table locking are cleanly separated
  • Proper error handling with errors.Wrap/Wrapf context at each level
  • Memory-conscious design with MaxManifestInMemory limiting parallelism
  • The batchLock function correctly handles large file lists with MaxSStableComponentCountInReqBody
  • Solid unit test for retentionLockUntil with edge cases
  • Comprehensive integration tests covering multi-backup scenarios and resume behavior
  • Validation in taskProperties.validate() is thorough (provider check, retention days check, mutual exclusion with count-based retention)

Potential issues / suggestions:

  1. StageRetentionLock marked as Resumable() but doesn't track progress (line 62 in stage.go): The stage is marked resumable, which means on resume it will restart from the beginning. This is noted in the PR description as intentional, but the lockAndAwait call with override=false could fail on already-locked files during retry if the mode is Locked. This seems safe only because the agent does best-effort, but worth a comment in the code.

  2. waitRetentionLockJob metric update (line 227-229 in worker_retention_lock.go): The diff can be negative if job.Uploaded resets (e.g., on agent restart or job recreation). Since this calls Add(float64(locked)), a negative diff would decrease the gauge. This is unlikely but could produce confusing metrics.

  3. Missing StageDeduplicate from stageDescription map (line 27-38 in stage.go): StageDeduplicate is in StageOrder() and Resumable() but not in stageDescription. This appears to be pre-existing but might be worth noting.

  4. lockSchemaFiles iterates hosts sequentially (lines 67-85 in worker_retention_lock.go): Schema files are locked one location at a time. For clusters with many locations this could be slow, though in practice the number of distinct locations is small.

  5. Integration test listSnapshotFiles uses listS3Files (line 393 in the integration test): The function is called listS3Files but is used with GCS buckets. This is a pre-existing naming issue.

  6. context.Background() in deferred cleanup (lines 192, 201 in worker_retention_lock.go): Using context.Background() for job stop and stats cleanup is the right pattern here since the original context may be cancelled.

  7. In restore integration test (service_restore_integration_test.go): The simpleBackupWithProperties method is used but its definition isn't shown in this diff — make sure it exists in the test helpers.

No blocking issues found. The code is clean, well-tested, and follows existing patterns.

@Michal-Leszczynski
Michal-Leszczynski merged commit 3fe6dc2 into master May 21, 2026
79 of 81 checks passed
@Michal-Leszczynski
Michal-Leszczynski deleted the ml/1915-retention-lock-sm-side branch May 21, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants