Files
map-asset-gateway/services/api-go/internal/basemap/workers.go
T
Harmon 73ad03fa59 Rewrite the gateway around health, retries, slicing and a Windows service
Scanner
- manifest lookup is now depth-bounded and prunes numeric zoom directories, so
  a scan no longer walks the whole tile payload
- rescans only report a version as updated when something actually changed
- overlapping runs are refused instead of interleaving their bookkeeping

Hot path
- token authorisation loads only that token grants and throttles the
  last_used_at write instead of touching it on every tile
- SQLite runs in WAL with a busy timeout and a real connection pool
- the catalog lookup used by /tiles no longer loads every basemap

Security
- the admin surface requires ADMIN_TOKEN (bearer) or ADMIN_USER/ADMIN_PASSWORD
  (basic), and the service refuses to bind off-host without credentials
- CORS is an explicit allow-list; newly minted tokens never reach a redirect URL
- client tokens are redacted from the access log

Operations
- resource health inspection flags a moved or deleted tile root before a 404
- pushes retry with backoff and can be re-queued by hand; the schedule lives in
  the database so a restart does not lose it
- access log plus /metrics counters

Image slicing
- tile_jobs drive the GDAL bundled with QGIS via scripts/gdal2tiles-qgis.bat
- layer_sources remembers what a layer is made of: submit one source to append
  a flight, or all of them to rebuild the layer as a VRT mosaic with no tile
  seams; the console exposes both, including a one-click rebuild
- slicing is available to integrations through a narrow slice token scope
  (submit, poll, idempotent by code, completion callback)

Console
- rebuilt around a fixed rail and single-view navigation with health and
  outstanding work leading, instead of one long scrolling page

Windows service
- native install/start/stop/status with crash recovery, auto-start and a
  rotating log file, so no NSSM wrapper is needed
- start-all.bat and stop-all.bat are removed: they killed the process by port
  with taskkill, which fought the service restart policy

Verified with 49 tests and live runs against real tile data and real GDAL.
2026-10-16 10:35:30 +08:00

99 lines
2.2 KiB
Go

package basemap
import (
"context"
"log"
"time"
)
const (
// pushRetryInterval is how often due retries are looked for.
pushRetryInterval = 15 * time.Second
// tileJobInterval is how often the queue is checked for new jobs.
tileJobInterval = 2 * time.Second
)
// StartBackgroundWorkers launches the periodic maintenance loops: resource
// health inspection, push retries and the tile slicing queue. Close stops them.
// Calling it more than once has no additional effect.
func (s *Store) StartBackgroundWorkers() {
s.bgOnce.Do(func() {
ctx, cancel := context.WithCancel(context.Background())
s.bgCtx = ctx
s.bgCancel = cancel
s.bgWG.Add(1)
go func() {
defer s.bgWG.Done()
s.healthLoop(ctx)
}()
s.bgWG.Add(1)
go func() {
defer s.bgWG.Done()
s.pushRetryLoop(ctx)
}()
s.bgWG.Add(1)
go func() {
defer s.bgWG.Done()
s.tileJobLoop(ctx)
}()
})
}
func (s *Store) healthLoop(ctx context.Context) {
// Prime the catalog immediately so a resource that vanished since the last
// scan shows up without waiting a full interval.
if _, err := s.CheckHealth(ctx); err != nil && ctx.Err() == nil {
log.Printf("health check failed: %v", err)
}
interval := s.cfg.HealthCheckInterval
if interval <= 0 {
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if _, err := s.CheckHealth(ctx); err != nil && ctx.Err() == nil {
log.Printf("health check failed: %v", err)
}
}
}
}
func (s *Store) pushRetryLoop(ctx context.Context) {
ticker := time.NewTicker(pushRetryInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := s.dispatchPushRetries(ctx); err != nil && ctx.Err() == nil {
log.Printf("push retry dispatch failed: %v", err)
}
if err := s.dispatchTileJobCallbacks(ctx); err != nil && ctx.Err() == nil {
log.Printf("tile job callback dispatch failed: %v", err)
}
}
}
}
func (s *Store) tileJobLoop(ctx context.Context) {
ticker := time.NewTicker(tileJobInterval)
defer ticker.Stop()
for {
s.dispatchTileJobs(ctx)
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}