Repository navigation
feat(taskflow)!: claim and release tasks #296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
64c7082
feat(taskflow)!: add task claims to the task store interface
WebCooper 6379483
feat(taskflow): store task claims in the gorm task store
WebCooper 3d6ec3c
feat(taskflow): claim and release tasks through the TaskManager
WebCooper cad5148
feat(taskflow): show the task's claim on task views
WebCooper 36bb7e0
docs(taskflow): document task claims
WebCooper efa02bb
docs(taskflow): say how clients take claim state when version does no…
WebCooper 1ed6845
docs(taskflow): fence claim checks with the submission
WebCooper e4d09d0
docs(taskflow): describe claims as UI guidance
WebCooper File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // Copyright (c) 2026 Lanka Software Foundation | ||
|
|
||
| package orchestrator | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "github.com/OpenNSW/core/taskflow/store" | ||
| ) | ||
|
|
||
| // A claim records who is working a task, so the UI can show it and two people can avoid working | ||
| // the same task at once. The holder is an opaque value chosen by the host (for example an internal | ||
| // user ID). taskflow only stores claims. CompleteTaskStep does not read the holder: anyone who can | ||
| // call it can submit the active step. For this MVP a claim is UI guidance. | ||
| // | ||
| // Not to be confused with store.TaskStore.ClaimStep, which makes a step the task's active step, or | ||
| // with renderer.Facts.Claims, which are authorization facts for rendering. | ||
|
|
||
| // ErrHolderRequired is returned by ClaimTask and ReleaseTask when the holder is empty. A caller | ||
| // serving HTTP should answer it with 400; it usually means the caller has no identity to claim as. | ||
| var ErrHolderRequired = errors.New("claim holder is required") | ||
|
|
||
| // ErrClaimHeld means another holder has the task's claim. ClaimTask returns it as a | ||
| // *ClaimHeldError, which names the holder. A caller serving HTTP should answer it with 409. | ||
| var ErrClaimHeld = errors.New("task is claimed by another holder") | ||
|
|
||
| // ErrNotClaimHolder is returned by ReleaseTask when the task is claimed by someone else. A caller | ||
| // serving HTTP should answer it with 403. | ||
| var ErrNotClaimHolder = errors.New("task is not claimed by the caller") | ||
|
|
||
| // ErrTaskCompleted is returned by ClaimTask when the task has completed: there is nothing left to | ||
| // work on. A caller serving HTTP should answer it with 409. | ||
| var ErrTaskCompleted = errors.New("task is completed") | ||
|
|
||
| // ClaimHeldError is the error ClaimTask returns when another holder has the claim. It matches | ||
| // ErrClaimHeld with errors.Is. | ||
| type ClaimHeldError struct { | ||
| // Holder is who has the claim. | ||
| Holder string | ||
| // ClaimedAt is when Holder claimed the task, if known. | ||
| ClaimedAt *time.Time | ||
| } | ||
|
|
||
| func (e *ClaimHeldError) Error() string { | ||
| return fmt.Sprintf("%s: %s", ErrClaimHeld, e.Holder) | ||
| } | ||
|
|
||
| func (e *ClaimHeldError) Is(target error) bool { return target == ErrClaimHeld } | ||
|
|
||
| // claimAttempts bounds the retries of a claim or release whose guard failed because the claim | ||
| // changed between the write and the read that explains it (for example, the holder released in | ||
| // between). Each retry needs another concurrent change to fail again, so a few are plenty. | ||
| const claimAttempts = 3 | ||
|
|
||
| // ClaimTask claims the task for holder. It succeeds when the task is unclaimed or already claimed | ||
| // by holder; a repeat claim keeps the original claim time. The task's steps and version are not | ||
| // changed, so an open step stays current. | ||
| // | ||
| // Errors: ErrHolderRequired, ErrTaskNotFound, ErrTaskCompleted (the task has completed, even if | ||
| // holder had claimed it), or a *ClaimHeldError (another holder has the claim). | ||
| func (tm *TaskManager) ClaimTask(ctx context.Context, taskID, holder string) error { | ||
| if holder == "" { | ||
| return ErrHolderRequired | ||
| } | ||
| for range claimAttempts { | ||
| n, err := tm.db.ClaimTask(ctx, taskID, holder, time.Now().UTC()) | ||
| if err != nil { | ||
| return fmt.Errorf("claiming task %s: %w", taskID, err) | ||
| } | ||
| if n > 0 { | ||
| return nil | ||
| } | ||
|
|
||
| // The guard did not match: read the row to say why. | ||
| record, ok := tm.db.GetTask(ctx, taskID) | ||
| switch { | ||
| case !ok: | ||
| return fmt.Errorf("%w: %s", ErrTaskNotFound, taskID) | ||
| case record.State == store.StateCompleted: | ||
| return fmt.Errorf("%w: %s", ErrTaskCompleted, taskID) | ||
| case record.ClaimedBy != "" && record.ClaimedBy != holder: | ||
| return &ClaimHeldError{Holder: record.ClaimedBy, ClaimedAt: record.ClaimedAt} | ||
| } | ||
| // The row is claimable now, so the claim changed after the write: try again. | ||
| } | ||
| return fmt.Errorf("claiming task %s: the claim kept changing, try again", taskID) | ||
| } | ||
|
|
||
| // ReleaseTask clears holder's claim on the task. Releasing a task that is not claimed succeeds, so | ||
| // a repeated release is harmless. A completed task's claim can still be released. | ||
| // | ||
| // Errors: ErrHolderRequired, ErrTaskNotFound, or ErrNotClaimHolder (someone else has the claim). | ||
| func (tm *TaskManager) ReleaseTask(ctx context.Context, taskID, holder string) error { | ||
| if holder == "" { | ||
| return ErrHolderRequired | ||
| } | ||
| for range claimAttempts { | ||
| n, err := tm.db.ReleaseTask(ctx, taskID, holder) | ||
| if err != nil { | ||
| return fmt.Errorf("releasing task %s: %w", taskID, err) | ||
| } | ||
| if n > 0 { | ||
| return nil | ||
| } | ||
|
|
||
| record, ok := tm.db.GetTask(ctx, taskID) | ||
| switch { | ||
| case !ok: | ||
| return fmt.Errorf("%w: %s", ErrTaskNotFound, taskID) | ||
| case record.ClaimedBy == "": | ||
| return nil | ||
| case record.ClaimedBy != holder: | ||
| return fmt.Errorf("%w: %s", ErrNotClaimHolder, taskID) | ||
| } | ||
| // holder has the claim now, so it changed after the write: try again. | ||
| } | ||
| return fmt.Errorf("releasing task %s: the claim kept changing, try again", taskID) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.