package geo import ( "net" "sync" "time" ) // Snapshot is one answer from one reader: the result, that reader's source, // and whether any reader was loaded at all (geoip spec ยง5.4, codex R2-4). // Headers, analytics and attribution all come from one Snapshot, so a swap // between the lookup and the response can never relabel the data. type Snapshot struct { Result Result Found bool Source string // empty only when Loaded is false Loaded bool } // Swappable is a reader that can be replaced while lookups run. The old // mapping is closed only after the write lock has been taken and released, // so no lookup is ever in flight on an unmapped file. There is deliberately // no Source() method: a source read apart from its lookup is the race this // type exists to prevent. type Swappable struct { mu sync.RWMutex cur *MMDB } func NewSwappable() *Swappable { return &Swappable{} } // Lookup answers under one read lock. func (s *Swappable) Lookup(ip net.IP) Snapshot { s.mu.RLock() defer s.mu.RUnlock() if s.cur == nil { return Snapshot{} } r, ok := s.cur.Lookup(ip) return Snapshot{Result: r, Found: ok, Source: s.cur.Source(), Loaded: true} } // Locate makes Swappable a Locator, so the console's region guess can hold // it without ever seeing the *MMDB underneath (codex R3-2). func (s *Swappable) Locate(ip net.IP) (Point, bool) { s.mu.RLock() defer s.mu.RUnlock() if s.cur == nil { return Point{}, false } return s.cur.Locate(ip) } // Loaded reports the current reader's source and build time. func (s *Swappable) Loaded() (source string, build time.Time, ok bool) { s.mu.RLock() defer s.mu.RUnlock() if s.cur == nil { return "", time.Time{}, false } return s.cur.Source(), s.cur.BuildEpoch(), true } // Swap installs next (nil = none) and closes the previous reader after // every lookup that could see it has finished. Go's RWMutex blocks new // readers while a writer waits, so lookups cannot starve a swap. func (s *Swappable) Swap(next *MMDB) { s.mu.Lock() old := s.cur s.cur = next s.mu.Unlock() if old != nil && old != next { old.Close() } } // LocateFrom is Locate plus the answering reader's source, under one read // lock, so a credit shown beside a guess always names the database that // made it. func (s *Swappable) LocateFrom(ip net.IP) (Point, bool, string) { s.mu.RLock() defer s.mu.RUnlock() if s.cur == nil { return Point{}, false, "" } p, ok := s.cur.Locate(ip) return p, ok, s.cur.Source() }