Skip to content

Add support for contexts #14

Open

Description

@mafredri

This might be a stretch for this library, but it would be cool if we had support for contexts, whether directly in this package or a sub-package like qzcontext.

Now, rather than doing context.WithTimeout you'd do clock.Context.WithTimeout(ctx, duration, "tag") (likewise for deadline) and be able to control expiry by advancing the clock.

Activity

  1. spikecurtis commented on May 22, 2025

    @spikecurtis
    Collaborator

    IIRC, the reason I have been gunshy is how to deal with contexts that already have deadlines based on a real clock being passed into the mock clock.

  2. mafredri commented on May 22, 2025

    @mafredri
    MemberAuthor

    Oh, that's an interesting thought. Off the top of my head

    1. Discard timeouts not originating from quartz and print warning
    2. Error, because the context timeout did not originate from quartz (limits usability, but maybe still useful within services)
    3. Start watching the context that was passed into quartz context and track how long until it was cancelled -> measure against quartz elapsed time and if less, keep the quartz context active, otherwise cancel it.

    For option 3, I imagine something like:

    func quartzContext(clock *quartz.Clock, parentCtx context.Context) context.Context {
    	detachedCtx, detachedCancelCause := context.WithCancelCause(context.WithoutCancel(parentCtx))
    
    	startTime := time.Now()
    
    	go func() {
    		<-parentCtx.Done()
    		duration := time.Since(startTime)
    		parentErr := parentCtx.Err()
    
    		// if clock advance > duration
    		//   detachedCancelCause(parentErr)
    		//
    		// else track advancement and cancel eventually
    	}()
    
    	return detachedCtx
    }

    Edit: Oh yeah, forgot about ctx.Deadline() which could also be analyzed rather than the above timing. The context.WithoutCancel would still be useful.

  3. added 3 commits that reference this issue on May 7, 2026
    8122ed5
    c7c5550
    53c5486
  4. mafredri commented on May 8, 2026

    @mafredri
    MemberAuthor

    FWIW #30 takes a fourth option, rather than trying to be clever, it allows real-clock timeouts to cancel quartz contexts early. Thanks to context.AfterFunc, the propagation is made very simple with no added goroutines.

    In coder/coder, a common use-case we have is:

    ctx := testutil.Context(t, 30 * time.Second)

    That's the upper bound we want the test to run for. Given that quartz does not interfere with the deadline, tests will properly time out rather than waiting indefinitely if quartz clock isn't being advanced.

    The user can still use context.WithoutCancel or similar in places where they want quartz to have full management.

    If you wanted to control context timeouts for e.g. http handlers, you could:

    srv := http.Server{
    	BaseContext func(net.Listener) context.Context {
    		return svc.clock.WithTimeout(svc.ctx, 30 * time.Second)
    	}
    }

    Giving you the ability to clock.Advance and test the handler properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions