Skip to content

feat: fail test on trapped but unreleased calls #15

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 2 commits into from
May 23, 2025

Conversation

spikecurtis
Copy link
Collaborator

@spikecurtis spikecurtis commented May 23, 2025

part of #13

Will fail test with an error message if you trap a call and don't release it.

I've also added 2 test cases to the unit test.

One is a skipped test that you can unskip to see what it looks like if you don't release the trapped call.

One tests what happens if two different traps catch the same call. It gets a little awkward because when you Release() the call, it waits for the call to complete. One of the main purposes is to ensure that a timer or ticker is set, and if we don't wait for the call to complete, you can't ensure the timer is set before you advance the clock. The awkward consequence is that the Release() calls will deadlock if they happen on the same goroutine because all traps have to release the call before it will return.

We separate out the trapping of the call and releasing of the call so that you have a chance to manipulate the clock before the call returns. But, actually, there are really 3 phases to a trapped call:

  1. Call is trapped
  2. All traps released, we get the time and do the work (e.g. actually setting the timer)
  3. Call completes

After trap.Wait() returns, we know phase 1 is complete. But, Release() actually conflates phase 2 and 3, so there is no way to release the trap without waiting for phase 3.

Generally we don't care that much about the distinction, it's really only in the case of multple traps that you'd need to release without waiting to avoid the deadlock.

We could make those phases explicit: trap.Wait().Release().WaitForComplete(), but that seems pretty involved for what I think is generally an edge case.

WDYT?

Copy link
Collaborator Author

spikecurtis commented May 23, 2025

@spikecurtis spikecurtis requested a review from mafredri May 23, 2025 10:17
@spikecurtis spikecurtis self-assigned this May 23, 2025
@spikecurtis spikecurtis marked this pull request as ready for review May 23, 2025 10:18
Copy link
Member

@mafredri mafredri left a comment

Choose a reason for hiding this comment

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

This looks good IMO, thanks for fixing! Added some discussion around the implementation details and a suggestion for the test.

mClock.Advance(time.Second)
// the two trapped call instances need to be released on separate goroutines since they each wait for the Now() call
// to return, which is blocked on both releases happening. If you release them on the same goroutine, in either
// order, it will deadlock.
Copy link
Member

Choose a reason for hiding this comment

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

This is kind of unfortunate. It'd be nice if Release also took a context, considering it can be blocking.

I wonder if there's a case to be made for erroring or warning if multiple traps can capture the same call? Perhaps quartz could also detect deadlocks and have a built-in timeout that fails the test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I do like the idea of Release() taking a context, and adding a MustRelease(ctx) which fails the test. Would be API breaking change, tho, unless we only did the latter. Personally I'd rather just break the API to make something safer. It's a real easy upgrade.

In the PR above this in the stack I add some logging, so you get to see that multiple traps capture, and we can make the error messages if your context expires suggest that people check for multiple captures.

c.releases.Done()
<-c.complete
c.apiCall.releases.Done()
<-c.apiCall.complete
Copy link
Member

Choose a reason for hiding this comment

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

Wish we could use t.Context() here but I did some experiments and unless the use-case is very specific, it won't help at all (go test -timeout=5s for instance does not cancel the context before panicing).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, alas.

Copy link
Collaborator Author

spikecurtis commented May 23, 2025

Merge activity

  • May 23, 11:33 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • May 23, 11:33 AM UTC: @spikecurtis merged this pull request with Graphite.

@spikecurtis spikecurtis merged commit d87ad1f into main May 23, 2025
1 check passed
@spikecurtis spikecurtis deleted the spike/fail-unreleased-calls branch June 3, 2025 05:29
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