Skip to content

Swartzn/feat/add xtreemstore rst - #312

Open
swartzn wants to merge 50 commits into
mainfrom
swartzn/feat/add-xtreem-store-rst
Open

swartzn wants to merge 50 commits into
mainfrom
swartzn/feat/add-xtreem-store-rst

Conversation

@swartzn

@swartzn swartzn commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

What does this PR do / why do we need it?

Required for all PRs.

  • Implements s3Provider so the S3Client functionality can be extended in other providers.
  • Adds XtreemStore provider.
  • Providers gain the ability to handle bulk requests.

Related Issue(s)

Required when applicable.

Where should the reviewer(s) start reviewing this?

Only required for larger PRs when this may not be immediately obvious.

Are there any specific topics we should discuss before merging?

Not required.

What are the next steps after this PR?

Not required.

Checklist before merging:

Required for all PRs.

When creating a PR these are items to keep in mind that cannot be checked by GitHub actions:

  • Documentation:
    • Does developer documentation (code comments, readme, etc.) need to be added or updated?
    • Does the user documentation need to be expanded or updated for this change?
  • Testing:
    • Does this functionality require changing or adding new unit tests?
    • Does this functionality require changing or adding new integration tests?
  • Git Hygiene:

For more details refer to the Go coding standards and the pull request process.

@swartzn
swartzn requested a review from iamjoemccormick March 31, 2026 23:48
@swartzn swartzn self-assigned this Mar 31, 2026
@swartzn
swartzn requested a review from a team as a code owner March 31, 2026 23:48
@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch from 407202d to fa556f3 Compare April 1, 2026 10:05
@swartzn swartzn changed the title Swartzn/feat/add xtreem store rst Swartzn/feat/add xtreemstore rst Apr 1, 2026

@iamjoemccormick iamjoemccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Dropping a note I finished with my initial pass through this.

Comment thread ctl/internal/cmd/rst/list.go
Comment thread common/rst/s3.go
Comment thread common/rst/builder.go Outdated
Comment thread common/rst/builder.go Outdated
Comment thread common/rst/s3.go Outdated
Comment thread common/rst/builder.go Outdated
Comment thread rst/remote/internal/job/manager.go Outdated
Comment thread common/rst/rst.go Outdated
Comment thread common/rst/rst.go Outdated
Comment thread common/rst/rst.go Outdated
@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch 3 times, most recently from a2a3f5e to 4e1d63a Compare April 8, 2026 15:06
@swartzn
swartzn requested a review from iamjoemccormick April 8, 2026 15:36
@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch 2 times, most recently from 634f0aa to cce8e6e Compare April 13, 2026 16:20

@iamjoemccormick iamjoemccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Submitting review feedback on the first two commits (up to "add xtreemstore provider").

Comment thread common/rst/s3.go
Comment thread common/rst/s3.go Outdated
Comment thread common/rst/s3.go Outdated
Comment thread common/rst/rst.go
Comment thread rst/remote/internal/job/job.go Outdated
Comment on lines +108 to +113
executeAfter := rst.GetJobExecuteAfter(j.Get())
j.Segments = make([]*Segment, 0, len(workRequests))
for _, wr := range workRequests {
if executeAfter != nil {
wr.SetExecuteAfter(executeAfter)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

todo: unless I'm missing something doesn't this still do what we were trying to avoid? Generating an timestamp on one node (Remote) that is then passed to the Sync nodes which who's times may not be in sync?

I would prefer instead we make this a DelayExecution field on the job request that uses the protobuf duration type that is propagated to a DelayExecution field on the work requests.

I would also propose this is set by the GenerateWorkRequests method, if appropriate for that RST client type. It doesn't feel right to set this one field in GenerateSubmission().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I saw this but was distracted by the other comments and forgot to come back to it. Anyway, this required adding a DelayExecution field to both beeremote.JobRequest and flex.WorkRequest which allows the sync node to convert the delay to ExecuteAfter in SubmitWorkRequest. GenerateWorkRequests is still called by remote.

See e85cf75

Comment thread rst/remote/internal/job/manager.go Outdated
Comment thread rst/sync/internal/workmgr/manager.go Outdated
Comment thread rst/sync/internal/workmgr/manager.go
zap.Bool("hasWorkResult", entry.WorkResult != nil),
zap.Bool("hasStatus", status != nil),
)
m.scheduler.AddRescheduleWorkToken(submissionId, time.Time{})

@iamjoemccormick iamjoemccormick Apr 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: so when this happens we still add a token because when the journal is replayed later this WR will still get picked up and presumably the invalid result sent back to Remote?

Edit: I see this was added with the "update job state to running..." commit. Was this a bug? If so it'd be worth a mention in the commit message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, the work needs to be processed in order to complete/update remote.

It was a bug. I missed adding m.scheduler.AddRescheduleWorkToken(submissionId, time.Time{}) the first time. Showed up in my testing.

@iamjoemccormick iamjoemccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Posting review comments for the remaining commits.

Comment thread common/rst/s3.go Outdated
Comment thread common/rst/s3.go Outdated
Comment thread common/rst/rst.go Outdated
Comment thread common/rst/s3.go Outdated
Comment thread common/rst/builder.go Outdated
Comment thread common/rst/builder.go Outdated
Comment on lines +311 to +313
if _, err := w.beeRemoteClient.UpdateWorkRequest(work.ctx, result.Work); err != nil {
log.Warn("unable to update remote job status to running; continuing work request without retrying", zap.Error(err))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion(blocking): we discussed this on Slack, but adding a note here so we don't loose track.

Sync intentionally never informed Remote when a work request shifted to "running" to reduce load on Remote and the jobs DB. I would prefer we keep it that way, as for the most part Remote never needs to know about this unless a user checks the job status.

What we could do is implement this issue https://github.com/ThinkParQ/bee-remote/issues/14 so if the user runs remote status or remote job list Remote will reach out and refresh the job and work request statuses. The GetJobsRequest message already has a UpdateWorkResults field that was intended to control if only the latest results known to Remote are returned, or if it also probes the Sync nodes.

We could either set UpdateWorkResults by default for CTL commands, or add a new --refresh-results flag for this. My thinking is for the most part users only care to know once jobs reach a terminal state, the intermediate states aren't that interesting unless you're trying to debug specific job issues.

@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch from cce8e6e to b01a2c0 Compare April 15, 2026 18:37
@swartzn
swartzn requested a review from iamjoemccormick April 16, 2026 14:16
@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch from 2ba6ab2 to 9d97563 Compare April 17, 2026 12:57

@iamjoemccormick iamjoemccormick left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a handful of remaining observations, my comment on GenerateSubmission() is the main blocker.

}

} else {
workRequests = rst.RecreateWorkRequests(j.Get(), j.GetSegments())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

todo: the RecreateWorkRequests() path was updated to handle SetDelayExecution(), but I think we also need to set this in the GenerateWorkRequests() path?

I'm actually not sure if the RecreateWorkRequests() path in GenerateSubmission() is ever executed anymore, elsewhere just calls RecreateWorkRequests() directly.

Comment thread common/rst/builder.go Outdated
if client.IncludeInBulkRequest(walkCtx, jobRequest) {
rstId := jobRequest.GetRemoteStorageTarget()

bulkRequestStatesMu.Lock()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion(non-blocking): to avoid lock contention in a hot path consider making this a RWMutex as after the initial population of bulkRequestStates the map is only read.

Assisted-by: Claude:claude-opus-4-6

Comment thread common/rst/rst.go Outdated

type SubmitBulkRequestFn func(ctx context.Context)
type EmitBulkRequestFn func(ctx context.Context, request *beeremote.JobRequest)
type AppendBulkRequestFn func(ctx context.Context, request *beeremote.JobRequest)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion(non-blocking): we should mention AppendBulkRequestsFn is called under a lock so it should be fast. For example of a provider did I/O in append it could really slow everything down.

Assisted-by: Claude:claude-opus-4-6

@iamjoemccormick iamjoemccormick mentioned this pull request Apr 28, 2026
9 tasks
Comment thread common/rst/xtreemstore.go Fixed
Comment thread common/rst/xtreemstore.go Fixed
Comment thread common/rst/xtreemstore.go Fixed
Comment thread common/rst/xtreemstore.go Fixed
Comment thread common/rst/xtreemstore.go Fixed
Comment thread rst/sync/internal/workmgr/work.go Outdated
return
}

// getBuilderResults generates a status message based on the builder submission counters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here were some generated examples,

Builder / SchedulerResult State Status message
submitted=250
clean, mid-walk
Reschedule=true
RESCHEDULED waiting for builder job to continue; 250 job request(s) submitted
submitted=100, errors=5
Reschedule=true
RESCHEDULED waiting for builder job to continue; builder cancelled: 100 job request(s) submitted; 5 submitted with errors
submitted=42
clean, aborted
Err=MarkBuilderCancelled("job builder request was aborted: context canceled")
CANCELLED job builder failed to complete: job builder request was aborted: context canceled; 42 job request(s) submitted
submitted=10, errors=3
Err=MarkBuilderFailed("job builder request was aborted: unable to abort bulk operation: connection refused")
FAILED job builder failed to complete: job builder request was aborted: unable to abort bulk operation: connection refused; builder cancelled: 10 job request(s) submitted; 3 submitted with errors
submitted=7, alreadyExist=2
Err="unexpected nil bulk result"
unclassified
FAILED job builder returned unclassified error: unexpected nil bulk result; 7 job request(s) submitted; 2 already exist
submitted=180, alreadyComplete=6, notAllowed=2, errors=4
Err=nil, Reschedule=false
CANCELLED completed with errors; builder cancelled: 180 job request(s) submitted; 6 already complete; 2 not allowed; 4 submitted with errors
submitted=500
clean run
Err=nil, Reschedule=false
COMPLETED completed successfully; 500 job request(s) submitted
alreadyComplete=1000
nothing new
Err=nil, Reschedule=false
COMPLETED completed successfully; 1000 already complete
notAllowed=50
all conflicted
Err=nil, Reschedule=false
CANCELLED completed with errors; builder cancelled: 50 not allowed
submitted=999, alreadyOffloaded=1
Err=nil, Reschedule=false
COMPLETED completed successfully; 999 job request(s) submitted; 1 already offloaded
0 matches
local path /data/empty-dir
mid-walk
Reschedule=true
RESCHEDULED waiting for builder job to continue; builder cancelled: no matches found in local path: /data/empty-dir
0 matches
remote path s3://bucket/incoming/
Err=nil, Reschedule=false
CANCELLED completed with errors; builder cancelled: no matches found in remote path: s3://bucket/incoming/
0 matches
download fallback to local walk /local/mirror
Err=nil, Reschedule=false
CANCELLED completed with errors; builder cancelled: walked local path since --remote-path was not provided; No matches found in path: /local/mirror
0 jobs counted
aborted immediately
local path /data/x
Err=MarkBuilderCancelled("job builder request was aborted: context canceled")
CANCELLED job builder failed to complete: job builder request was aborted: context canceled; builder cancelled: no matches found in local path: /data/x

@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch from d23cac0 to 6d4e8a0 Compare July 21, 2026 11:18
@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch 3 times, most recently from 52b9071 to 4bf837e Compare August 7, 2026 19:32
@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch 2 times, most recently from 4c5cab0 to 394073f Compare August 19, 2026 16:12
…iginal request

The ctl previously set the remote path to the in-mount path for upload requests. This prevented
the s3Client.GenerateWorkRequests from having a chance to update the remote path based on the last
job or normalizing the key.
BuildJobRequest set RemoteSize, RemoteMtime and IsArchived unconditionally so a missing remote
object left lockedInfo.RemoteMtime holding a zero timestamp rather than nil. Both
IsFileAlreadySynced and HasRemotePathInfo key off that distinction.
The work journal entries were passed by value instead of reference so a copy of ExecuteAfter was
modified instead of the database.

Few cleanups with no behavior changes
* Added appendMessage helper to help aggregate error messages for ctl.
Walks were previously limited to file or object count and then a resume token would be returned.
This prevents the caller from terminating as needed. So now,
* GetWalk returns an explicit stop function.
* All emitted results contain a valid resume token.
Add a trailing slash when comparing a directory with the resume token to avoid skipping directories
because of situations like a directory b sitting next to a file b.txt. Without the trailing slash,
the directory is skipped on resume even though it is emitted after that file.
GetLockedInfo previously handled rstId logic on a provided config which is removed so the caller can
explicitly handle it.

* Rename GetLockedInfo as GetPathState to better reflect the intent.
* Return PathState object with complete entryInfo so caller can use it.
Providers previously could not distinguish between a context error for cancelling a job and sync
shutting down. This made it impossible to shutdown gracefully.

* Add shutdown context explicitly to ExecuteWorkRequestPart and IsWorkRequestReady.
* Update sync worker to support gracefully shutdown and resume on restart.
Previously the local file state was immediately changed in preparation for work requests. So if the
caller needed to undo the changes there was no clear path.

* Extract the required work request changes and steps to undo into returned functions.
* Leave existing file contents intact until data is written when downloading. This is more efficient
  than resizing and then truncating the file, and improves cleanup when the job is cancelled.
* IsFileOffloaded now requires the file to exist.
* Classify all job submission responses as rst sentinels.
* Wrap remote connection issues with ErrUnavailable.
* Track average sync node work load saturation across a short and long time window.
* Pass load information to sync workers and builder jobs for rescheduling logic.
…elay contexts

Gracefully shutting down sync nodes should allow any inflight tasks to complete before stopping.
This includes rpc and api calls that may be slow. WithCancellationDelay and grace checkpoints will
be added to critical paths so they can complete.

* Add reporting to logs to show remaining inflight tasks.
… work failures

Clients need have to opportunity to handle work failures. In many instances the failures can be
automatically be set into a valid state such as in uploads or when the local file state can be
restored. This change will be especially important for adding bulk operation support since requests
will potentially hold critical resources that should always be released regardless of the individual
file state. Note the client can still fail the request when the file state requires manual
intervention; only that the client is making that determination instead of remote enforcing it.

* Add support for rst clients to remedy work request failures instead of assuming they should be failed.
* Implement started flag to work request part to indicate when work has commenced.
* Improve S3Client cleanup when completing a job request by,
  * replacing an incomplete download with a stub file.
  * restoring the original stub if there was one.
  * removing the file if it didn't exist.
  * otherwise restore the file's size and mtime.

* Fix remote's work assignment process so it stops assigning work immediately when a request fails
  to be assigned.
* Add support types and interface for bulk operations.
* Update s3 client to satisfy Provider changes.
* Update mock client to satisfy Provider changes with tests.
Split builder.go into supporting files with clear responsibilities:
* buildercontroller.go coordinates the walk, the worker pool and the bulk operations for one builder
  job, and collects the results.
* builderjobrequest.go turns a walked path into a job request, including resolving path state and
  planning the local file state changes each request needs, with an undo for every step so a failure
  walks them back.
* builderbulk.go holds the bulk operation registry and manager, persisting each operation's state
  under the mount point so it survives a restart.

* Job requests are built and submitted by the goroutine that prepared it, so the submission outcome is
  known while everything needed to roll that path back is still in scope.
* Sync worker and job builder can now distinguish between work cancelled and sync shutting down. This
  allows work to conclude before the shutdown without stepping backwards.
…AXPROCS

The AWS SDK's default MaxIdleConns/MaxIdleConnsPerHost caps connection reuse well below what a
single RST config's concurrency needs, since it only ever talks to one host. Size it off the same
multiplier the request build controller uses for its own concurrency so the two stay in sync.
* Add support for clients to reschedule work requests.
* Update s3 client so work is rescheduled on graceful shutdown instead of failing.
    * Uploads resume after the last completed work request part.
    * Downloads resume after the last byte written.
Previous downloading empty an object would fail to overwrite the size and mtime of the local file.

* Add support for downloading empty files to PlanFileStateForWorkRequests.
Add support for configuration files to reference enum values by name. The enum name is case
insensitive and hyphens can be used inplace of underscores.
Xtreemstore is s3-compatible and therefore extends the s3 client implementation.

* Add xtreemstore provider
* Add bulk retrieval support for downloading archived objects.
  * Independently manage multiple asynchronous bulk restore operations across multiple builder jobs.
    * Active bulk operations will reschedule until the last object is downloaded.
    * Bulk operation managers that are blocked by another's active retrieve session will reschedule
      for a later time.
  * Only one retrieve session can be active at a time so the bulk operation manager keeps the
    builder job alive until all archived objects are downloaded. This mechanism is necessary to
    manage xtreemstore's tape buffer.
  * State and request statuses are persistently stored on the beegfs mount so the bulk
    operation can be restored after a shutdown or crash.
Inflight work requests must be given a chance to complete while its sync worker is shutting down. So,
* Only a single attempt to update a job request's work status is permitted during shutdown.
* Any failures are logged.
…testing.

This race condition was exposed when require.Eventually replaced the explicity sleep.
* Fix the name the interface a new RST type must implement.
* Update GenerateWorkRequests description to include availableWorkers.
@swartzn
swartzn force-pushed the swartzn/feat/add-xtreem-store-rst branch from cb8d2be to 1a5a348 Compare September 18, 2026 16:37
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