mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-16 18:10:00 +00:00
release: v2.0.1 (#209)
- fix(meshmapper): refresh the longest-waiting scope catalogue first - feat(config): default scope catalogue refresh to 24h - chore: bump version
This commit is contained in:
+1
-1
@@ -45,7 +45,7 @@ import (
|
||||
var version = "dev"
|
||||
|
||||
// @title MeshCore Beacon API
|
||||
// @version 2.0.0
|
||||
// @version 2.0.1
|
||||
// @description MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.
|
||||
// @description REST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.
|
||||
// @description WebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ ratelimit:
|
||||
#meshmapper:
|
||||
# scopes:
|
||||
# enabled: false
|
||||
# refresh_interval: 1h # 1h-24h (MeshMapper allows one get_scopes.php per region per 55m); one source per 15s tick
|
||||
# refresh_interval: 24h # 1h-24h (MeshMapper allows one get_scopes.php per region per 55m); one source per 15s tick
|
||||
#
|
||||
# Region boundaries (Zones API). Every known IATA, including ones auto-created
|
||||
# from traffic, is looked up in its country's zone list, then its get_geojson.php
|
||||
|
||||
+1
-1
@@ -5811,7 +5811,7 @@ const docTemplate = `{
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "2.0.0",
|
||||
Version: "2.0.1",
|
||||
Host: "localhost:8080",
|
||||
BasePath: "/api/v1",
|
||||
Schemes: []string{"http", "https"},
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
"license": {
|
||||
"name": "AGPL-3-or-later"
|
||||
},
|
||||
"version": "2.0.0"
|
||||
"version": "2.0.1"
|
||||
},
|
||||
"host": "localhost:8080",
|
||||
"basePath": "/api/v1",
|
||||
|
||||
+1
-1
@@ -1635,7 +1635,7 @@ info:
|
||||
name: AGPL-3-or-later
|
||||
termsOfService: https://github.com/MeshCore-Beacon/beacon-server
|
||||
title: MeshCore Beacon API
|
||||
version: 2.0.0
|
||||
version: 2.0.1
|
||||
paths:
|
||||
/admin/accounts:
|
||||
get:
|
||||
|
||||
@@ -60,7 +60,7 @@ type MeshMapperScopesConfig struct {
|
||||
|
||||
func (c MeshMapperScopesConfig) Interval() time.Duration {
|
||||
if c.RefreshInterval.Duration == 0 {
|
||||
return time.Hour
|
||||
return 24 * time.Hour
|
||||
}
|
||||
return c.RefreshInterval.Duration
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestMeshMapperConfig(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (MeshMapperScopesConfig{}).Interval() != time.Hour {
|
||||
if (MeshMapperScopesConfig{}).Interval() != 24*time.Hour {
|
||||
t.Fatal("wrong default")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,15 @@ func (i *Importer) Refresh(ctx context.Context) (err error) {
|
||||
return fmt.Errorf("list IATAs for MeshMapper scopes: %w", err)
|
||||
}
|
||||
i.track(iatas, nil)
|
||||
for n := range i.sources {
|
||||
// Longest-waiting first, so a pass longer than the interval can't starve the tail.
|
||||
order := make([]int, len(i.sources))
|
||||
for n := range order {
|
||||
order[n] = n
|
||||
}
|
||||
slices.SortStableFunc(order, func(a, b int) int {
|
||||
return i.sources[a].cache.NextAttempt.Compare(i.sources[b].cache.NextAttempt)
|
||||
})
|
||||
for _, n := range order {
|
||||
s := &i.sources[n]
|
||||
if now.Before(s.cache.NextAttempt) {
|
||||
continue
|
||||
|
||||
@@ -425,3 +425,62 @@ func TestScopesRequestCutShortStillCounts(t *testing.T) {
|
||||
t.Fatal("abandoned request not persisted", row)
|
||||
}
|
||||
}
|
||||
|
||||
// newSweep serves a CA zone list whose IATAs each have a catalogue; fail lists IATAs answering 500.
|
||||
func newSweep(t *testing.T, store *memoryStore, fail ...string) (*Importer, *[]string) {
|
||||
t.Helper()
|
||||
var hits []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/get_zones.php" {
|
||||
var zones []string
|
||||
for _, iata := range store.iatas {
|
||||
zones = append(zones, fmt.Sprintf(`{"code":%q,"url":"%s/%s/","has_boundary":true,"group":null}`, iata, "http://"+r.Host, iata))
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, `{"country":"CA","zones":[%s]}`, strings.Join(zones, ","))
|
||||
return
|
||||
}
|
||||
iata := strings.Split(r.URL.Path, "/")[1]
|
||||
hits = append(hits, iata)
|
||||
if slices.Contains(fail, iata) {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(strings.ReplaceAll(string(catalogue("x")), "YOW", iata)))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
dir := NewDirectory(newZoneListMemory())
|
||||
dir.listURL = server.URL + "/get_zones.php"
|
||||
imp, err := New(context.Background(), config.MeshMapperScopesConfig{Enabled: true}, store, dir, scopestore.New(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
imp.scopesURL = func(site string) (string, bool) { return site + "get_scopes.php", true }
|
||||
refreshN(t, imp, 1) // zone list fetch
|
||||
return imp, &hits
|
||||
}
|
||||
|
||||
func TestScopesLongestWaitingGoesFirst(t *testing.T) {
|
||||
store := newMemoryStore("YOW", "YUL", "YVR", "YYZ")
|
||||
now := time.Now()
|
||||
// Restored mid-pass: YOW/YUL done, YVR never reached; YOW is already due again.
|
||||
store.rows[sourceKey{"YOW", "old"}] = Cache{AttemptedAt: now.Add(-time.Hour), NextAttempt: now.Add(-time.Minute)}
|
||||
store.rows[sourceKey{"YUL", "old"}] = Cache{AttemptedAt: now, NextAttempt: now.Add(time.Hour)}
|
||||
store.rows[sourceKey{"YVR", "old"}] = Cache{AttemptedAt: now.Add(-48 * time.Hour), NextAttempt: now.Add(-47 * time.Hour)}
|
||||
imp, hits := newSweep(t, store)
|
||||
refreshN(t, imp, 3)
|
||||
if want := []string{"YYZ", "YVR", "YOW"}; !reflect.DeepEqual(*hits, want) {
|
||||
t.Fatalf("refresh order %v, want %v", *hits, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopesFailureDoesNotStallThePass(t *testing.T) {
|
||||
store := newMemoryStore("YOW", "YUL", "YVR")
|
||||
imp, hits := newSweep(t, store, "YOW")
|
||||
refreshN(t, imp, 4)
|
||||
if want := []string{"YOW", "YUL", "YVR"}; !reflect.DeepEqual(*hits, want) {
|
||||
t.Fatalf("refresh order %v, want %v", *hits, want)
|
||||
}
|
||||
if s := imp.sources[0]; s.iata != "YOW" || time.Until(s.cache.NextAttempt) > failureRetry {
|
||||
t.Fatalf("failed source not retried early: %+v", s.cache)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user