better fetch

This commit is contained in:
Polen 2026-04-07 23:06:04 -04:00
parent 3fdb38d3a0
commit 67c3090434

43
main.go
View file

@ -31,6 +31,7 @@ const (
geojsonURL = "https://regieessencequebec.ca/stations.geojson.gz" geojsonURL = "https://regieessencequebec.ca/stations.geojson.gz"
defaultPort = "8080" defaultPort = "8080"
pollInterval = 5 * time.Minute pollInterval = 5 * time.Minute
fetchCooldown = 1 * time.Minute
) )
// GeoJSON structures matching the upstream format. // GeoJSON structures matching the upstream format.
@ -88,6 +89,13 @@ var (
cachedResp *StationsResponse cachedResp *StationsResponse
) )
// Fetch cooldown state: prevents request-triggered fetches from
// firing more often than fetchCooldown.
var (
fetchMu sync.Mutex
lastFetchTime time.Time
)
var db *sql.DB var db *sql.DB
// tmpl is the parsed template set, loaded once at startup. // tmpl is the parsed template set, loaded once at startup.
@ -231,6 +239,8 @@ func handleRoot(w http.ResponseWriter, r *http.Request) {
// handleMapPage renders the full map page (or just the content block for htmx). // handleMapPage renders the full map page (or just the content block for htmx).
func handleMapPage(w http.ResponseWriter, r *http.Request) { func handleMapPage(w http.ResponseWriter, r *http.Request) {
triggerFetch()
cacheMu.RLock() cacheMu.RLock()
resp := cachedResp resp := cachedResp
cacheMu.RUnlock() cacheMu.RUnlock()
@ -718,17 +728,48 @@ func initDB(path string) (*sql.DB, error) {
// poller fetches immediately then every pollInterval. // poller fetches immediately then every pollInterval.
func poller() { func poller() {
fetchAndStore() safeFetchAndStore()
ticker := time.NewTicker(pollInterval) ticker := time.NewTicker(pollInterval)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
safeFetchAndStore()
}
}
// safeFetchAndStore calls fetchAndStore and recovers from any panic so the
// poller goroutine stays alive.
func safeFetchAndStore() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in fetchAndStore: %v", r)
}
}()
fetchAndStore() fetchAndStore()
} }
// triggerFetch kicks off a background fetchAndStore if at least fetchCooldown
// has elapsed since the last fetch attempt. It returns immediately and never
// blocks the caller.
func triggerFetch() {
fetchMu.Lock()
if time.Since(lastFetchTime) < fetchCooldown {
fetchMu.Unlock()
return
}
lastFetchTime = time.Now()
fetchMu.Unlock()
go safeFetchAndStore()
} }
// fetchAndStore fetches upstream data, updates the in-memory cache, and // fetchAndStore fetches upstream data, updates the in-memory cache, and
// persists a snapshot to SQLite if the data has a new generated_at value. // persists a snapshot to SQLite if the data has a new generated_at value.
func fetchAndStore() { func fetchAndStore() {
// Reset cooldown so poller-initiated fetches also prevent redundant
// request-triggered fetches.
fetchMu.Lock()
lastFetchTime = time.Now()
fetchMu.Unlock()
resp, err := fetchAndParse() resp, err := fetchAndParse()
if err != nil { if err != nil {
log.Printf("fetch error: %v", err) log.Printf("fetch error: %v", err)