Day 30 — Stage III gate
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.
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:
- Performs concurrent work on ≥ 20 items
- Bounds concurrency (pool or
errgroup.SetLimit)
- Accepts a root
context(timeout flag and/or cancel)
- Propagates errors intentionally (fail-fast or best-effort—documented)
- Passes
go test -race -count=20 ./...on Go 1.26
- Ships README with: purpose, run, test, design notes (sync + cancel)
Stretch (not required, impressive)
signal.NotifyContextfor 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 (
0success, 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.JoinDocument 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
- Confirm
go 1.26ingo.modandgo version.
- Re-run full tests with race, count 20.
- Add any missing cancel/limit/empty-input tests.
- Fix README gaps (flags, examples, error policy paragraph).
- 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 drillLab 4 — Deliberate regression drill
- Break a synchronization on purpose (e.g. remove a mutex around a shared counter).
- Show
-racefailing; capture output snippet.
- Restore the fix.
- Note the detector output in
GATE.mdappendix.
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=20Lab 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.mdexplanations 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.