Balking — Tasks¶
Hands-on exercises for the Balking pattern, ordered roughly easy → hard. Each task has a goal, requirements, hints, and a solution sketch. Build from junior.md and middle.md.
Table of Contents¶
- Task 1 — Idempotent
start() - Task 2 — Lock-free one-shot
close() - Task 3 — Dirty-flag
save() - Task 4 — Debounced flush
- Task 5 — Prove exactly-once under contention
- Task 6 — Enum state machine balk
- Task 7 — Single-flight
- Task 8 — Idempotent consumer
- Task 9 — Go
sync.Oncevs hand-rolled - Task 10 — Signaled balk with metrics
- How to Practice
Task 1 — Idempotent start()¶
Goal. Make start() safe to call from any thread any number of times. Requirements. Return true only on the call that actually starts; false (balk) otherwise. Check-then-act must be atomic. Hints. Wrap the guard and the flag write in one synchronized method. Solution sketch.
private boolean started = false;
public synchronized boolean start() {
if (started) return false; // balk
started = true;
init();
return true;
}
Task 2 — Lock-free one-shot close()¶
Goal. Run cleanup exactly once with no lock. Requirements. N concurrent close() calls → cleanup runs once; idempotent thereafter. Hints. AtomicBoolean.compareAndSet(false, true) — winner runs, losers balk. Solution sketch.
private final AtomicBoolean closed = new AtomicBoolean(false);
public void close() {
if (!closed.compareAndSet(false, true)) return; // balk
cleanup();
}
Task 3 — Dirty-flag save()¶
Goal. save() does nothing when nothing changed; writes safely when it did. Requirements. Balk if !changed. Do not lose the edit if the write throws. Hints. Reset changed only after a successful write, or restore it in catch. Solution sketch.
public synchronized void save() {
if (!changed) return; // balk
doSave(content); // may throw
changed = false; // reset only on success
}
Task 4 — Debounced flush¶
Goal. flush() runs at most once per 100 ms. Requirements. Thread-safe; balk if last flush was < 100 ms ago; return boolean. Hints. Track lastFlush = System.nanoTime() under the lock; compare against the interval. Solution sketch.
public synchronized boolean flush() {
long now = System.nanoTime();
if (now - lastFlush < INTERVAL_NANOS) return false; // balk
lastFlush = now; doFlush(); return true;
}
Task 5 — Prove exactly-once under contention¶
Goal. Write a test proving the body runs once across many threads. Requirements. Launch ≥ 1000 threads (or an ExecutorService) all calling start(); assert the side-effect counter == 1 and exactly one call returned true. Hints. Use a CyclicBarrier so all threads hit start() simultaneously; count side effects with AtomicInteger. Solution sketch.
var ran = new AtomicInteger();
var wins = new AtomicInteger();
var barrier = new CyclicBarrier(N);
// each thread: barrier.await(); if (obj.start()) wins.incrementAndGet();
// inside init(): ran.incrementAndGet();
assertEquals(1, ran.get());
assertEquals(1, wins.get());
Task 6 — Enum state machine balk¶
Goal. A lifecycle with STOPPED/RUNNING/CLOSED where every transition balks appropriately. Requirements. start() balks unless STOPPED; stop() balks unless RUNNING; close() balks if already CLOSED. No impossible states. Hints. Single enum state field under synchronized, or AtomicReference<State> with compareAndSet per transition. Solution sketch.
Task 7 — Single-flight¶
Goal. Concurrent get(key) triggers one loader call; others balk on loading but await the result. Requirements. Exactly one upstream load per key; all callers get the same value; entry removed after completion. Hints. ConcurrentHashMap.putIfAbsent(key, future) is the atomic balk; losers join() the existing future. Solution sketch. See SingleFlight in senior.md.
Task 8 — Idempotent consumer¶
Goal. Process each message ID once despite at-least-once redelivery. Requirements. Balk on already-seen IDs; the check-and-act on the seen-set must be atomic. Hints. seen.add(id) on a ConcurrentHashMap.newKeySet() returns false if present → balk. For multi-node, use a DB unique constraint or Redis SETNX. Solution sketch.
Task 9 — Go sync.Once vs hand-rolled¶
Goal. Implement once-balk two ways in Go and compare. Requirements. (a) sync.Once.Do; (b) atomic.Bool.CompareAndSwap(false,true). Both must run the body once under concurrent calls. Hints. sync.Once also blocks late callers until the first finishes; the atomic version balks immediately and does not wait for completion — note the semantic difference. Solution sketch.
Task 10 — Signaled balk with metrics¶
Goal. Convert a silent balk into an observable one. Requirements. Return boolean; increment a counter metric on each balk; log at WARN only when the balk indicates a likely bug (e.g. double capture), DEBUG otherwise. Hints. Separate "expected balk" (already running) from "suspicious balk" (already captured) and route them to different log levels. Solution sketch.
if (!captured.compareAndSet(false, true)) {
metrics.counter("capture.balk").increment();
log.warn("capture balked: key {} already processed", key);
return false;
}
How to Practice¶
- Do Tasks 1–4 first; they cement the guard-and-return shape and the dirty-flag ordering trap.
- Always write the concurrency proof (Task 5) — a balk that looks correct single-threaded is the classic source of the check-then-act bug.
- For each task, implement it once with
synchronizedand once with CAS, then articulate which you'd ship and why. - Re-run multithreaded tests dozens of times (or use
jcstress); races surface intermittently, not on the first run. - After Task 10, audit a real codebase for
voidbalks that should return a status — that's the highest-value real-world skill here.
In this topic