Skip to content

fix race condition when updating labels - #4861

Merged
mislav merged 2 commits into
cli:trunkfrom
despreston:4835-labels-race
Dec 8, 2021
Merged

mislav merged 2 commits into
cli:trunkfrom
despreston:4835-labels-race

Conversation

@despreston

Copy link
Copy Markdown
Contributor

Update labels using the addLabelsToLabelable and
removeLabelsFromLabelable mutations instead of via the updateIssue
mutation. This prevents the update from clobbering any unseen changes to
the list of labels. This could happen if the list of labels was updated
after the metadata was fetched, which would cause the local list to be
out-of-date.

Fixes #4835

@despreston
despreston requested a review from a team as a code owner December 6, 2021 16:13
@despreston
despreston requested review from vilmibm and removed request for a team December 6, 2021 16:13
@cliAutomation cliAutomation added the external pull request originating outside of the CLI core team label Dec 6, 2021

@mislav mislav left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wow, thanks for working on this! I just took a cursory glance and it looks great so far.

One question: how does this handle when the set of labels was edited interactively via prompts and multi-selects in the gh issue edit flow?

@despreston

Copy link
Copy Markdown
Contributor Author

I can change the tests for interactive mode to cover that case. I think it'll have the same functionality since these changes are downstream from where the logic splits b/w interactive and non-interactive.

@despreston

Copy link
Copy Markdown
Contributor Author

Oof, I was off about how the values are parsed during interactive mode here.

This is going to require some more lines in order to diff what that multi select survey returns vs what labels existed before. That's gonna be necessary in order to know which labels to add and which ones to delete in separate mutations. Shouldn't be too much work — glad to continue working on this.

@despreston
despreston requested a review from mislav December 6, 2021 23:04

@mislav mislav left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, this looks great!

Does updateIssue() still run even if the only edits that the user has requested was regarding labels? Can we somehow skip updateIssue firing if there are no other edits?

Finally, can updateIssue() and label operations, if they both need to happen, run concurrently so that the whole operation finishes earlier?

Comment thread pkg/cmd/pr/shared/editable_http.go Outdated
Comment thread pkg/cmd/pr/shared/editable_http.go Outdated
Comment thread pkg/cmd/pr/shared/editable_http.go Outdated
}
}
if len(options.Labels.Remove) > 0 {
removeLabelIds, err := options.Metadata.LabelsToIDs(options.Labels.Remove)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

addLabels and removeLabels operations are now done sequentially. If they both need to happen, could be do them concurrently? I would typically recommend using golang.org/x/sync/errgroup for this, except for the side-effect that a failed HTTP call would automatically cancel all other requests in the group, which is not what we'd want here.

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 thought about this but ultimately decided not to to avoid doing a partial update in the case that something goes wrong with one of the requests. Seemed better to do an all-or-none update. Idk if there's precedent for doing partial updates in this app. Curious to what your thoughts are about that.

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.

That's also related to your second question in your first comment above.

@mislav mislav Dec 7, 2021 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I thought about this but ultimately decided not to to avoid doing a partial update in the case that something goes wrong with one of the requests. Seemed better to do an all-or-none update.

Not sure about what you mean by "partial update", but here is what I was thinking: if I currently do gh issue edit 123 --title newTitle --add-label bug --remove-label feature, this will result in 3 separate API requests:

  1. addLabelsToLabelable
  2. removeLabelsFromLabelable
  3. updateIssue/updatePullRequest with the title parameter

Right now these 3 API requests will be fired sequentially, but since they are not at all dependent on each other, you could fire them concurrently (e.g. via an errgroup.Group) and wait for them all to finish before proceeding. If any of these requests failed, the rest still have a chance of succeeding, and if all of them succeed, the whole edit command would take drastically less time overall. In the error scenario, if that is what you meant by "partial update", I think it's fine: the GitHub API doesn't have a functionality to execute a series of updates in a "transaction", so any GitHub API client risks getting caught in an in-between state whenever we do multiple edits. This is acceptable and shouldn't be an impediment for parallelization.

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.

Gotcha. Yeah you understood my point and answered my question. Agree parallelization makes sense here.

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 implemented parallelism using a regular WaitGroup. This way all requests are run even if one fails. I took the liberty of changing the signature of UpdateIssue to accept an io.Writer for error output. This means all errors can be printed to stderr.

@mislav mislav self-assigned this Dec 7, 2021

@Aspsolo Aspsolo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👀🤔🤣

despreston and others added 2 commits December 8, 2021 13:29
Update labels using the `addLabelsToLabelable` and
`removeLabelsFromLabelable` mutations instead of via the `updateIssue`
mutation that replaces the entire set of labels. This prevents the edit
operation from clobbering any unseen changes to the list of labels.

Co-authored-by: Mislav Marohnić <[email protected]>

@mislav mislav left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you! I've switched to errgroup.Group after I've found out that the whole group won't get cancelled on first error unless you use errgroup.WithContext. I find the error handling behavior cleaner this way, and it is also acceptable that only the first HTTP error will get reported and the rest get swallowed. I've moved away from these update operations having a writer stream for error output because I don't think lower-level API functionality should be writing to output streams at all.

@despreston

Copy link
Copy Markdown
Contributor Author

sounds good! Agree on all of that. I really mulled over how to handle errors. Didnt want to change too much of the code and wasn't sure if it'd be preferable to output all errors or just the first!

@mislav
mislav merged commit c509a3d into cli:trunk Dec 8, 2021
@despreston
despreston deleted the 4835-labels-race branch December 8, 2021 15:36
missingbulb pushed a commit to missingbulb/Claudinite that referenced this pull request Aug 13, 2026
RESEARCH.md synthesizes four web sweeps (job queues, schedulers,
workflow orchestrators, leases + issues-as-queue prior art) against the
design's decisions: the occurrence guard is the coalesced run-once
catch-up policy every scheduler defaults to; the claim protocol is a
visibility-timeout lease with verify-after-write; the agent lease is
the single-use task-token pattern built from comments. Fed back into
DESIGN.md: granular-only label writes (the cli/cli#4861 clobber class),
native issue dependencies as standard mirror (GA Aug 2025), executor
timeout-minutes bounded by the executing leash (the stalled-worker
overlap), ref-CAS claims and invocation idempotency keys on record in
§15 — and, answering the owner's idempotency concern, the honest
requirement ladder plus a per-task on_interrupt: 'requeue'|'needs-human'
opt-out for genuinely one-shot side effects.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PRpBQTmz8yKjceC3diHWiz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external pull request originating outside of the CLI core team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix case where the CLI unintentionally removes labels when adding labels

4 participants