Skip to content

feat: Better parallelism - #408

Open
Peeja wants to merge 7 commits into
mainfrom
feat/better-parallelism
Open

feat: Better parallelism#408
Peeja wants to merge 7 commits into
mainfrom
feat/better-parallelism

Conversation

@Peeja

@Peeja Peeja commented Apr 17, 2026

Copy link
Copy Markdown
Member

Previously, the Worker would always run in batches. When signaled, it would find new work to do, then perform all of that work, then wait for a new signal. Some workers would perform their work in parallel, but only within the batch, meaning that one long-lived task in the batch would waste the rest of the parallelism, and no new tasks would be pulled in until the entire batch completed.

Now, Worker understands tasks and parallelism natively. Whenever it has free parallelism, it looks up pending tasks. Then it queues those as it works through them. Notably, each Worker definition is responsible for tracking what's in flight and not re-queuing it. That is, since we don't store that we've placed a task in the queue, we'll keep finding it in the DB until the task is complete, and we need to filter that out. But it's pretty straightforward to manage that by DB ID. We keep that in memory rather than in the DB, because if the process is interrupted it naturally results in a consistent state: the queue is emptied, and so is the set of what we consider "in-flight".

My gut says that there's further we can push this, but this is a significant improvement as it is. I've tested it with doupload. #409 demonstrates it with the (shell version of the) Smelt-based doupload.

Closes #267

Rather than waiting for entire batches to complete, we re-query whenever
there's available parallelism, until we find no more blobs.

Note that this *only* works when new blobs become available while we're
waiting to send some into `addBlobs`. If blobs become available between
`addBlobs` consuming all the ones we had (beginning their uploads) and
all of those uploads completing, we won't catch them until the next
round one level up. In that case, we still waste parallel capacity.
Therefore, this is still an incomplete solution, but moves in the right
direction.
@Peeja Peeja changed the title Better parallelism feat: Better parallelism Apr 17, 2026
@Peeja
Peeja marked this pull request as ready for review April 17, 2026 16:26
@Peeja
Peeja force-pushed the feat/better-parallelism branch from 05d8b18 to fc0cd47 Compare April 17, 2026 16:36
@Peeja
Peeja force-pushed the feat/better-parallelism branch from fc0cd47 to 95a5177 Compare April 17, 2026 16:50

@alanshaw alanshaw 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.

Happy for this to merge but please consider the feedback. I would most like to have a type for the non-fatal task return value - I suggested a "report" that could contain warning messages, but I'm not attached to that naming.

err = fmt.Errorf("failed to add shard %s: %w", shard, err)
// [gtypes.BlobUploadError]s are non-fatal.
var errBlobUpload gtypes.BlobUploadError
if errors.As(err, &errBlobUpload) {

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.

Could just use errors.Is? You're not using errBlobUpload at all.

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.

Minor, but personally I'd flip this round to return early for the fatal case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Could just use errors.Is? You're not using errBlobUpload at all.

errors.Is is for instances, errors.As is for types, unless I'm misunderstanding.

Comment thread pkg/preparation/types/errors.go Outdated
// error that should cause immediate cancellation of all other tasks.
type IDTask struct {
ID id.ID
Run func(context.Context) (error, error)

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.

Perhaps use a type for the non-fatal error e.g.

type Report struct {
  Warnings []error
}

It's just weird IMO to see a function that returns two errors.

}

if err := a.updateBlob(ctx, blob); err != nil {
if err := a.updateBlob(context.WithoutCancel(ctx), blob); err != nil {

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.

Why do we want this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because we're recording something that happened externally. We need to make sure we do that. We can't make the DB update truly transactional with the external side effect, but we can at least make sure we don't give up on recording what happened because the context canceled. Otherwise we might do things like upload the same shard twice. (And given updateBlob() is a simple DB update, the potential cost is low.)

Comment thread pkg/preparation/types/errors.go Outdated
// [id.ID]. The task's Run function returns two errors: a non-fatal error that
// can be collected and reported after all tasks have completed, and a fatal
// error that should cause immediate cancellation of all other tasks.
type IDTask struct {

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.

Do (should?) we have non-ID tasks? Is this just a Task?

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.

Should this live in errors.go?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

My intent here was separation of concerns: the Worker code doesn't handle deduping tasks and leaves it entirely to the worker definition which uses Worker. In particular, that means the non-parallel workers don't have to make up a bogus "ID" to satisfy an interface, they just don't do any deduping. I'm certainly open to thoughts here, though; I debated it a bit myself.

Comment thread pkg/preparation/types/errors.go Outdated
// can be collected and reported after all tasks have completed, and a fatal
// error that should cause immediate cancellation of all other tasks.
type IDTask struct {
ID id.ID

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: add a non-unique Desc string which could be used in error messages.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oooh, I like that.

@Peeja

Peeja commented Apr 24, 2026

Copy link
Copy Markdown
Member Author

@alanshaw @hannahhoward How's this look? Part of the intention with this TaskError model is that we can use it a level up as well to track the non-fatals across the workers a bit more easily, but I've spent enough time on this thing as it is, so I'd rather leave that for the future.

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.

Improve parallelism in Storacha workers

2 participants