// lesson: duplicate-suppression
Duplicate Suppression
Cache expires, 500 requests arrive in the same millisecond, all 500 miss, all 500 hit the database with the identical query. That's a cache stampede, and the cure is singleflight: if a call for key K is already in flight, don't start another โ wait for the running one and share its result.
The design is a map of in-flight calls guarded by a mutex, plus a done-channel per call:
- First caller for K: insert a
call{done: make(chan struct{})}under the lock, unlock, runfnoutside the lock (never hold a mutex across user code), store the result, then clean up andclose(done). - Every other caller for K: find the entry under the lock, unlock,
<-done, read the shared result.
The trap is the insert, not the cleanup order: checking "is K already in
flight" and inserting a new call for it must happen as one atomic step under
the lock. Split it into two lock acquisitions โ check, unlock, then
insert โ and two callers arriving at the same instant can each conclude
they're first, and both invoke fn. Whether you delete the map entry before
or after closing done doesn't actually matter for correctness: fn's
result is written once, before anyone can observe it, so a caller that finds
the entry still present just gets an already-final result a little late,
and one that finds it gone starts a fresh call โ neither reads a value
that's "about to be overwritten." (For what it's worth, the real
golang.org/x/sync/singleflight releases its waiters before deleting the
map entry โ the opposite of what you might guess โ because both happen
inside one uninterrupted locked section, so no caller can observe one
without the other anyway.) What does matter: never hold the lock while fn
runs โ that would serialize every key, not just repeats of the same one โ
and always remove the entry eventually, so the next caller for K gets a
fresh flight. Also note what singleflight is not: it's not a cache. Once a
flight completes, the next caller starts a fresh one.
โบ Singleflight From Scratch
35 ptsImplement:
type Group struct{ ... } // zero value must be ready to use
func (g *Group) Do(key string, fn func() (int, error)) (int, error)
Required semantics:
- While a call for
keyis in flight, every additionalDo(key, ...)waits for it and returns the same(value, error)โ their ownfnis never invoked. The tests fire 50 concurrent callers at one key and require exactly one invocation. - Errors are shared exactly like values.
- Calls for different keys must not block each other โ the tests
rendezvous two
fns for different keys and require them to overlap in time. - No caching: once a flight completes, the next
Dofor that key invokesfnagain.
Log in to submit a solution and earn points.