Skip to content

Hot reload GeoLite2 mmdb databases without a restart - #8

Open
gam6itko wants to merge 3 commits into
akyriako:mainfrom
gam6itko:feat/geoip-hot-reload
Open

Hot reload GeoLite2 mmdb databases without a restart#8
gam6itko wants to merge 3 commits into
akyriako:mainfrom
gam6itko:feat/geoip-hot-reload

Conversation

@gam6itko

@gam6itko gam6itko commented Aug 8, 2026

Copy link
Copy Markdown

Fixes #7

geoipupdate replaces the mmdb files on the host, but the app opened them once at startup and kept serving stale data until the container was restarted. Both City and ASN databases are now picked up at runtime.

How

A single reusable ReloadableGeoIPDB backs both readers, so CityReader and AsnReader share one implementation. Their public API is unchanged, and so are the HTTP endpoints and response formats.

Changes are detected by re-os.Stating the path (ModTime + Size) rather than trusting the open file descriptor, since geoipupdate writes a temp file and renames it over the target. Plain polling, no fsnotify.

Reload order:

  1. the new reader is opened before any lock is taken, so the current one keeps serving lookups;
  2. if Open fails, the previous database stays active, the error is logged and the next cycle retries;
  3. the swap and the old reader's Close() happen under the same write lock. Lookups hold RLock for the whole Lookup+Decode, so while the write lock is held no lookup is inside the old reader and none can enter it.

reader == nil never occurs. atomic.Pointer is deliberately avoided — the mutex keeps the close lifecycle obviously correct. Nothing is logged while the file is unchanged.

Shutdown

main now uses signal.NotifyContext + srv.Shutdown, which also fixes an existing bug: log.Fatal(http.ListenAndServe(...)) calls os.Exit, so defer asn.Close() / defer city.Close() never ran.

Configuration

GEOIP_RELOAD_INTERVAL, default 60s. No docker-compose or Dockerfile changes required. Note that /geolite must be bind-mounted as a directory, not as individual files — a rename on the host is invisible inside the container otherwise. The current compose files already mount the directory.

Tests

A minimal valid MMDB is built in memory, so no large binary fixture is added to the repo. Tests replace the file the way geoipupdate does (write temp + os.Rename) and cover: startup, no reload when unchanged, picking up a replacement, a corrupt replacement leaving the previous database working, recovery on the next valid replacement, concurrent lookups against repeated reloads, watcher shutdown on context cancel, and Close.

go test ./... and go test -race ./... pass.

gam6itko and others added 3 commits August 8, 2026 12:24
Poll GeoLite2-City.mmdb and GeoLite2-ASN.mmdb via os.Stat (ModTime+Size)
and swap the open maxminddb.Reader when geoipupdate replaces the file,
so a container restart is no longer needed.

The new reader is opened before any lock is taken; on failure the previous
database keeps serving and the reload is retried on the next cycle. The old
reader is closed under the same write lock that performs the swap, which
guarantees no lookup is still using it.

Check interval is configurable via GEOIP_RELOAD_INTERVAL (default 60s).
Watchers stop on SIGINT/SIGTERM, alongside a graceful HTTP shutdown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hot reload GeoLite2 databases without restarting ipquery

1 participant