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>
72 lines
1.4 KiB
Go
72 lines
1.4 KiB
Go
package db_updater
|
|
|
|
import (
|
|
"github.com/go-resty/resty/v2"
|
|
"github.com/rs/zerolog/log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"thuanle.me/ip-info/configs"
|
|
"time"
|
|
)
|
|
|
|
func download(url string) bool {
|
|
log.Info().Str("url", url).Msg("Downloading DB")
|
|
filename := configs.GeoDbFolder + path.Base(url)
|
|
filenameEtag := filename + ".etag"
|
|
|
|
etag, _ := readFile(filenameEtag)
|
|
|
|
client := resty.New().
|
|
SetTimeout(2 * time.Minute).
|
|
SetRetryCount(2).
|
|
SetRetryWaitTime(5 * time.Second)
|
|
resp, err := client.R().
|
|
SetHeader("If-None-Match", etag).
|
|
Get(url)
|
|
|
|
if err != nil {
|
|
log.Err(err).
|
|
Str("url", url).
|
|
Msg("Failed to fetch DB")
|
|
return false
|
|
}
|
|
|
|
switch resp.StatusCode() {
|
|
case http.StatusNotModified:
|
|
log.Info().
|
|
Str("url", url).
|
|
Str("etag", etag).
|
|
Int("status_code", resp.StatusCode()).
|
|
Str("status", resp.Status()).
|
|
Msg("No update needed")
|
|
return false
|
|
case http.StatusOK:
|
|
_ = writeFileSByte(filename, resp.Body())
|
|
_ = writeFileString(filenameEtag, resp.Header().Get("Etag"))
|
|
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func readFile(file string) (string, error) {
|
|
data, err := os.ReadFile(file)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func writeFileString(file, content string) error {
|
|
return writeFileSByte(file, []byte(content))
|
|
}
|
|
|
|
func writeFileSByte(file string, content []byte) error {
|
|
return os.WriteFile(file, content, 0644)
|
|
}
|