better fetch

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

51
main.go
View file

@ -28,9 +28,10 @@ var staticFiles embed.FS
var templateFiles embed.FS
const (
geojsonURL = "https://regieessencequebec.ca/stations.geojson.gz"
defaultPort = "8080"
pollInterval = 5 * time.Minute
geojsonURL = "https://regieessencequebec.ca/stations.geojson.gz"
defaultPort = "8080"
pollInterval = 5 * time.Minute
fetchCooldown = 1 * time.Minute
)
// GeoJSON structures matching the upstream format.
@ -88,6 +89,13 @@ var (
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
// 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).
func handleMapPage(w http.ResponseWriter, r *http.Request) {
triggerFetch()
cacheMu.RLock()
resp := cachedResp
cacheMu.RUnlock()
@ -718,17 +728,48 @@ func initDB(path string) (*sql.DB, error) {
// poller fetches immediately then every pollInterval.
func poller() {
fetchAndStore()
safeFetchAndStore()
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
for range ticker.C {
fetchAndStore()
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()
}
// 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
// persists a snapshot to SQLite if the data has a new generated_at value.
func fetchAndStore() {
// Reset cooldown so poller-initiated fetches also prevent redundant
// request-triggered fetches.
fetchMu.Lock()
lastFetchTime = time.Now()
fetchMu.Unlock()
resp, err := fetchAndParse()
if err != nil {
log.Printf("fetch error: %v", err)