Day 30 — Stage III gate

Updated

July 30, 2026

Day 30 — Stage III gate

Stage III · Gate day · ~3h
Goal: Prove concurrency competence: a race-clean concurrent tool with cancellation, bounded workers, tests, and clear explanations of the Stage III mental model.

Important

Gates are pass/fail against a rubric, not calendar guilt. If something fails, fix it today or schedule a re-gate before Stage IV.

Why this day exists

Stage III (Days 19–29) stacked:

Day Skill
19 Goroutines, GMP lite, join
20 Channels, close ownership
21 Select, timeouts
22 Mutex, atomic, WaitGroup
23 Worker pools, pipelines
24 Context cancel/deadlines
25 Race detector, leaks
26 Patterns catalog
27 errgroup
28 Memory model lite
29 Integration build

Isolated labs create isolated muscles. The gate asks whether these compose under pressure—not whether you memorized slogans. Generics, fuzzing, and I/O depth in Stage IV will multiply concurrent bugs if your mental model is still hand-wavy.


Theory 1 — Gate artifact (definition of done)

You need a single module (Day 29 project refined is ideal) that:

  1. Performs concurrent work on ≥ 20 items
  2. Bounds concurrency (pool or errgroup.SetLimit)
  3. Accepts a root context (timeout flag and/or cancel)
  4. Propagates errors intentionally (fail-fast or best-effort—documented)
  5. Passes go test -race -count=20 ./... on Go 1.26
  6. Ships README with: purpose, run, test, design notes (sync + cancel)

Stretch (not required, impressive)

  • signal.NotifyContext for Ctrl-C
  • Structured logging of worker IDs (slog)
  • Benchmark with/without race detector noted
  • Metrics counter for completed jobs (even a simple atomic)

Explicit non-goals

  • No HTTP server requirement (that is Stage V+)
  • No database
  • No perfect CLI UX framework
  • No “must use every primitive from Days 19–28”

Theory 2 — What “race-clean under pressure” means

A single green go test -race is weak evidence. Flaky races show up under:

  • Higher -count
  • Parallel tests (t.Parallel)
  • Contended maps without mutex
  • Closing channels from the wrong side

Gate standard:

go test -race -count=20 ./...

If it fails once in twenty, it fails the gate. Fix the happens-before edge; do not paper over with time.Sleep.

Mini mental model (must be explainable)

Concept One-liner you must own
Data race Concurrent write/write or write/read without sync
Happens-before Channel, mutex, WaitGroup, atomics, etc. create edges
Cooperative cancel ctx.Done() observed; work stops voluntarily
Bound Limit in-flight work so you cannot create unbounded goroutines

Theory 3 — Oral/written explanation board

Answer in writing (half page each) in GATE.md. Use your project as the example, not textbook prose alone.

A. GMP

Explain G, M, P and what GOMAXPROCS limits. One misconception you no longer hold (e.g. “more goroutines always means more cores used”).

B. Channel ownership

Who closes? What happens on send-to-closed? When is a nil channel useful in a select?

C. Context

Why cancellation is cooperative. Why defer cancel() matters. When not to use context values for optional config.

D. Race

Define data race. Paste or paraphrase one race you fixed and the happens-before edge you introduced (mutex, channel send/receive, atomic, wait).

E. Mutex vs channel vs errgroup

One sentence each for when you pick them in this project.

F. Error policy (added for gate clarity)

Fail-fast (cancel siblings, return first error) vs best-effort (collect errors, continue). Quote which you chose and why.


Theory 4 — Rubric (score yourself)

Criterion Weight 0 1 2
Race-clean under -count=20 25% fails flaky solid
Bounded concurrency 15% unbounded informal explicit limit
Context cancel works 15% none partial demonstrated
Error policy clear 10% silent ad-hoc documented
Tests quality 15% none smoke cancel+limit+happy
Explanations A–F 15% missing shallow crisp
README / UX 5% none minimal clear

Pass: ≥ 80% weighted with no zero in race-clean or explanations.

Self-score honestly in GATE.md:

## Rubric

| Criterion | Score (0–2) | Notes |
|-----------|-------------|-------|
| Race-clean | 2 | count=20 green |
| Bounded concurrency | 2 | SetLimit(8) |
| Context cancel | 2 | -timeout works |
| Error policy | 1 | documented late |
| Tests | 2 | cancel + happy |
| Explanations | 2 | A–F written |
| README | 2 | demo script OK |
| **Weighted** | **≈ 95%** | PASS |

Theory 5 — Graceful shutdown checklist

For the gate tool (CLI/worker, not full HTTP drain):

  • Timeout flag or signal cancels root context
  • Workers observe ctx.Done() on each unit of work
  • Wait / errgroup / join completes after cancel
  • No permanent hang on empty input or immediate error
  • Process exits with meaningful code (0 success, non-zero hard fail)

“Graceful” here means cooperative cancel + join, not Stage V+ http.Server.Shutdown.

Pattern sketch (fail-fast pool)

func Run(ctx context.Context, jobs []Job, limit int) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(limit)
    for _, job := range jobs {
        job := job
        g.Go(func() error {
            if err := ctx.Err(); err != nil {
                return err
            }
            return process(ctx, job)
        })
    }
    return g.Wait()
}

Pattern sketch (best-effort with mutex)

var (
    mu   sync.Mutex
    errs []error
)
// each worker:
if err := process(ctx, job); err != nil {
    mu.Lock()
    errs = append(errs, err)
    mu.Unlock()
}
// after WaitGroup: join errs with errors.Join

Document which shape you shipped.


Theory 6 — Anti-patterns that fail the gate

Anti-pattern Why it fails
Sleep-based “sync” Hides races; flaky under load
Ignoring -race failures Gate criterion is race-clean
Unbounded go in a loop on the production path OOM / scheduler thrash
Closing channels from multiple goroutines unsafely Panic / races
Panic as control flow Unreadable; recover is not a strategy
Giant untested main No evidence under -count=20
Sharing a map without mutex or confinement Classic race
Forgetting defer cancel() Context leak; children outlive intent

Theory 7 — Minimum test matrix

Your tests should cover at least:

Case Intent
Happy path ≥20 jobs complete correctly
Cancel / timeout Work stops; no hang; returns context error or wrapped
Bound smoke Limit is set (even if hard to assert exactly)
Empty input No deadlock
func TestRun_Cancel(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    cancel()
    err := Run(ctx, makeJobs(50), 4)
    if !errors.Is(err, context.Canceled) && err != nil {
        // accept wrapped cancel depending on design
        t.Logf("err=%v (document policy)", err)
    }
}

Prefer asserting no hang with t.Deadline / short timeout over perfect error equality if your design aggregates.


Lab 1 — Harden Day 29 into gate shape

Workspace: Day 29 module or ~/lab/90daysofx/01-go/day30

  1. Confirm go 1.26 in go.mod and go version.
  2. Re-run full tests with race, count 20.
  3. Add any missing cancel/limit/empty-input tests.
  4. Fix README gaps (flags, examples, error policy paragraph).
  5. Record rubric scores honestly in GATE.md.
go version   # go1.26.x
go test -race -count=20 ./...
go vet ./...

Lab 2 — Live demo script

Write DEMO.md (or scripts/demo.sh) with commands a stranger can run:

# happy — process enough work to exercise the pool
go run ./cmd/tool --workers 4 --input ./testdata/jobs.txt

# timeout — must exit promptly
go run ./cmd/tool -timeout 1ms --workers 8 --input ./testdata/jobs.txt

# tests
go test -race -count=20 ./...

Run the demo once end-to-end without editing mid-flight. If a command fails, fix code/docs, then re-run the full script.


Lab 3 — Explanation document

Create GATE.md with answers A–F and the rubric table. Timebox 45 minutes. Clarity > length. Every answer should mention a concrete file or type in your module.

Template:

# Stage III Gate — day30

Date:
Module path:
Go version:

## Artifact
- Binary/cmd:
- Bound mechanism:
- Cancel mechanism:
- Error policy: fail-fast | best-effort

## Explanations
### A. GMP
...
### B. Channel ownership
...
### C. Context
...
### D. Race fixed
...
### E. Mutex vs channel vs errgroup
...
### F. Error policy
...

## Rubric
...

## Appendix — race detector drill

Lab 4 — Deliberate regression drill

  1. Break a synchronization on purpose (e.g. remove a mutex around a shared counter).
  2. Show -race failing; capture output snippet.
  3. Restore the fix.
  4. Note the detector output in GATE.md appendix.

This proves you can use the detector under stress—not that you never introduce races.

# after deliberate break:
go test -race ./internal/... -count=5
# restore, re-run count=20

Lab 5 — Exit codes and UX polish (short)

Scenario Expected exit
All jobs OK 0
Hard failure / fail-fast error non-zero
Cancel via timeout non-zero (or documented zero—be consistent)
func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()
    // or: context.WithTimeout from -timeout flag
    if err := realMain(ctx); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Hour-by-hour suggestion

Time Focus
0:00–0:30 Inventory Day 29 gaps vs gate bar
0:30–1:15 Harden code + tests (race, cancel)
1:15–1:45 DEMO.md dry run
1:45–2:30 GATE.md explanations A–F
2:30–2:50 Regression drill + rubric
2:50–3:00 Commit; list Stage IV risks

Common gotchas

Gotcha Fix
“Works on my machine” once -count=20 with -race
README outdated flags Re-run demo from zero
Explanations copy docs Use your project as example
Partial gate Schedule re-sit; don’t enter Stage IV blind
Timeout too long in tests Use short WithTimeout in unit tests
errgroup without SetLimit Add explicit bound
Logging from many goroutines without care slog is safe; still avoid huge allocations in hot paths
Treating cancel as optional Gate requires demonstrated cancel

Checkpoint

  • Rubric ≥ 80% with no zero on race-clean or explanations
  • go test -race -count=20 ./... pass on Go 1.26
  • Cancel/timeout demonstrated in demo
  • GATE.md explanations A–F complete
  • DEMO.md / demo script commands work cold
  • Regression drill appendix present
  • Honest list of remaining weaknesses

Commit

git add .
git commit -m "day30: stage III gate — race-clean concurrent tool + GATE notes"

Write three personal gotchas before continuing.


Tomorrow

Stage IV begins — Day 31 Generics: type parameters and constraints after you have felt real duplication pain in concurrent and sequential code. Enter Stage IV only with a race-clean mental model—or schedule a re-gate first.