backupMmdb() could return an error but the code still proceeded into
mergeMmdb(), which truncates the canonical mmdb. With no backup, a later
merge/reload failure left the file corrupt and unrecoverable on restart.
Now we skip the rebuild entirely when the backup fails, and rollBackMmdb
no longer takes the backup error (the only failure path returns early).
Co-Authored-By: Claude <noreply@anthropic.com>
1. /json client IP (codex #1, Medium): HandleJson now validates
c.ClientIP() and returns 400 on a malformed value instead of letting
Query fail into a 500. HandleIpInfo gained a selfLookup flag so a valid
but DB-absent self-IP preserves the 9d07639 graceful 200 {"ip": ip}
default (private/loopback addresses are never in a public GeoIP DB),
while the explicit /:ip route still returns 404 on not-found.
2. updated_at=0 on initial load (codex #2, Low): stampDbUpdatedAt() now
records the canonical mmdb's mtime both on initial load (existing
files) and after each successful update, so /metrics never reports 0
while serving real data.
3. Merge corruption -> permanent outage: mmdbmeld.WriteMMDB truncates the
canonical mmdb before writing; a failed merge previously left it empty
and the saved etag made the next daily run 304-skip the retry, so a
restart failed to open the file and the service went 503 permanently.
fetchDbs now backs up the canonical file before merge, restores it on
merge/reload failure, and drops the etags so the next run retries.
4. Temp-file leaks: Reload removes the temp clone and restores d.dbFile
when reader.Open fails (it previously assigned d.dbFile before Open);
cloneDBFile removes the temp on a failed copy.
Verified: go build, go vet, gofmt -l, go test -race ./... all pass.
Co-Authored-By: Claude <noreply@anthropic.com>
Concurrency & correctness:
- #10: guard IpDb Query/IsLoaded with RLock so Reload() (which reassigns d.r
and closes the old reader) cannot race with concurrent lookups. Confirmed
via -race regression test.
- #12: replace DbUpdatedAt time.Time with atomic.Int64 (cron writes,
/metrics reads) to fix the read/write data race.
- #11: graceful HTTP shutdown (srv.Shutdown with 10s timeout) instead of
srv.Close() aborting in-flight requests.
- #14: stop swallowing mergeMmdb() errors in fetchDbs() — keep the previous
DB when a merge fails instead of reloading a possibly-empty output.
- #13: add resty timeout (2m) + retry (x2) to download() so a hung CDN can't
stall the daily cron forever.
- #15: correct HTTP status codes (503 db loading, 404 not found via new
ErrNotFound sentinel, 500 otherwise) instead of 200 on query error.
Robustness:
- #16: surface osx.Copy dstFile.Close() errors (flush may fail) via named
return + defer.
Dependency migration:
- #17: migrate maxminddb-golang v1 -> v2. v2 is a breaking API
(LookupNetwork -> Lookup returning Result, netip.Addr), so Query was
rewritten; v1 dropped from go.mod.
Tests:
- #21: add internal/data unit tests (valid/invalid/not-found lookup) plus a
concurrent Query/Reload race regression test. chdir to repo root in TestMain
because data helpers use relative paths.
Cleanup:
- #18: README Go version 1.22 -> 1.25.
- #19: replace stray fmt.Printf with zerolog in ipdb.go.
- #20: .env.example API_PORT 28080 -> 8080 (container port, matches
docker-compose 28080:8080 mapping) with an explanatory comment.
Closes#10, #11, #12, #13, #14, #15, #16, #17, #18, #19, #20, #21
Co-Authored-By: Claude <noreply@anthropic.com>
Renovate bumped github.com/robfig/cron/v3 to v3.0.1 in go.mod (edde31c)
without regenerating go.sum, leaving only the v3.0.0 hash. The Docker CI
build (go build -mod=readonly) then failed with 'missing go.sum entry for
module providing package github.com/robfig/cron/v3' on both amd64/arm64.
Run go mod tidy to regenerate go.sum (also corrects indirect markers and
drops the unused spaolacci/murmur3 entry). Verified the exact CI build
command now passes locally.
Co-Authored-By: Claude <noreply@anthropic.com>