Share a Context Deadline Across Steps

Derive one deadline for a multi-step operation, check the remaining budget before each call, and fail fast with a wrapped error.

GoResilience

Code

snippet
package main

import (
	"context"
	"fmt"
	"time"
)

type Step struct {
	Name string
	Cost time.Duration
}

func runSteps(ctx context.Context, steps []Step) error {
	for _, step := range steps {
		if err := ctx.Err(); err != nil {
			return fmt.Errorf("before step %q: %w", step.Name, err)
		}
		if deadline, ok := ctx.Deadline(); ok {
			remaining := time.Until(deadline)
			if remaining < step.Cost {
				return fmt.Errorf(
					"step %q needs %s but only %s remains: %w",
					step.Name,
					step.Cost,
					remaining.Round(time.Millisecond),
					context.DeadlineExceeded,
				)
			}
		}

		timer := time.NewTimer(step.Cost)
		select {
		case <-ctx.Done():
			timer.Stop()
			return fmt.Errorf("step %q interrupted: %w", step.Name, ctx.Err())
		case <-timer.C:
			fmt.Printf("completed %q in %s\n", step.Name, step.Cost)
		}
	}
	return nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
	defer cancel()

	steps := []Step{
		{Name: "load-config", Cost: 20 * time.Millisecond},
		{Name: "fetch-profile", Cost: 25 * time.Millisecond},
		{Name: "render-report", Cost: 60 * time.Millisecond},
	}
	if err := runSteps(ctx, steps); err != nil {
		fmt.Println("stopped:", err)
	}
}