mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-01 16:48:19 +00:00
+10
-2
@@ -127,6 +127,7 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, s
|
||||
ObserverID: nullableUUID(v.ObserverID),
|
||||
KnownNeighborCount: v.KnownNeighborCount,
|
||||
NeighborIDs: v.NeighborIds,
|
||||
Stale: v.LastSeen.Valid && v.LastSeen.Time.Before(time.Now().Add(-s.staleThreshold)),
|
||||
}
|
||||
if len(v.Iatas) > 0 {
|
||||
if err := json.Unmarshal(v.Iatas, &node.IATAs); err != nil {
|
||||
@@ -135,7 +136,7 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, s
|
||||
}
|
||||
}
|
||||
if v.RadioFreqMhz != nil && v.RadioSf != nil && v.RadioBwKhz != nil {
|
||||
s := fmt.Sprintf("%.1f,%g,%d", *v.RadioFreqMhz, *v.RadioBwKhz, *v.RadioSf)
|
||||
s := fmt.Sprintf("%g,%g,%d", *v.RadioFreqMhz, *v.RadioBwKhz, *v.RadioSf)
|
||||
node.Radio = &s
|
||||
}
|
||||
items = append(items, node)
|
||||
@@ -170,6 +171,7 @@ func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error
|
||||
ObserverID: nullableUUID(row.ObserverID),
|
||||
DefaultScope: row.DefaultScopeName,
|
||||
KnownNeighborCount: row.KnownNeighborCount,
|
||||
Stale: row.LastSeen.Valid && row.LastSeen.Time.Before(time.Now().Add(-s.staleThreshold)),
|
||||
},
|
||||
LocationSource: row.LocationSource,
|
||||
SupportsMultibytePaths: row.SupportsMultibytePaths,
|
||||
@@ -192,7 +194,7 @@ func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error
|
||||
}
|
||||
}
|
||||
if row.RadioFreqMhz != nil && row.RadioSf != nil && row.RadioBwKhz != nil {
|
||||
s := fmt.Sprintf("%.1f,%g,%d", *row.RadioFreqMhz, *row.RadioBwKhz, *row.RadioSf)
|
||||
s := fmt.Sprintf("%g,%g,%d", *row.RadioFreqMhz, *row.RadioBwKhz, *row.RadioSf)
|
||||
node.Radio = &s
|
||||
}
|
||||
if row.LastAdvertAt.Valid {
|
||||
@@ -288,3 +290,9 @@ func (s *Store) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]api.N
|
||||
func (s *Store) ReconfirmNeighbors(ctx context.Context) error {
|
||||
return s.q.ReconfirmNeighbors(ctx)
|
||||
}
|
||||
|
||||
// DeleteOldNodes deletes nodes not seen since the given cutoff. See the DeleteOldNodes SQL
|
||||
// query for the observer_owners exclusion and known_routes caveat.
|
||||
func (s *Store) DeleteOldNodes(ctx context.Context, cutoff time.Time) error {
|
||||
return s.q.DeleteOldNodes(ctx, pgtype.Timestamptz{Time: cutoff, Valid: true})
|
||||
}
|
||||
|
||||
+84
-2
@@ -242,6 +242,42 @@ func TestListNodes_IATAsUnmarshal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNodes_Stale(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
|
||||
staleID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
freshID := uuid.MustParse("00000000-0000-0000-0000-000000000002")
|
||||
unmeasuredID := uuid.MustParse("00000000-0000-0000-0000-000000000003")
|
||||
|
||||
mock.EXPECT().
|
||||
ListNodes(gomock.Any(), gomock.Any()).
|
||||
Return([]sqlc.ListNodesRow{
|
||||
{ID: staleID, PublicKey: []byte{0x01}, LastSeen: pgtype.Timestamptz{Time: time.Now().Add(-48 * time.Hour), Valid: true}},
|
||||
{ID: freshID, PublicKey: []byte{0x02}, LastSeen: pgtype.Timestamptz{Time: time.Now(), Valid: true}},
|
||||
{ID: unmeasuredID, PublicKey: []byte{0x03}, LastSeen: pgtype.Timestamptz{Valid: false}},
|
||||
}, nil)
|
||||
|
||||
store := &Store{q: mock, staleThreshold: 24 * time.Hour}
|
||||
page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
byID := make(map[uuid.UUID]bool)
|
||||
for _, n := range page.Items {
|
||||
byID[n.ID] = n.Stale
|
||||
}
|
||||
if !byID[staleID] {
|
||||
t.Error("expected node last seen 48h ago to be stale with a 24h threshold")
|
||||
}
|
||||
if byID[freshID] {
|
||||
t.Error("expected node last seen just now to not be stale")
|
||||
}
|
||||
if byID[unmeasuredID] {
|
||||
t.Error("expected a node with no last_seen at all to not be stale")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNodes_RadioStringFormatting(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
@@ -271,8 +307,8 @@ func TestListNodes_RadioStringFormatting(t *testing.T) {
|
||||
if page.Items[0].Radio == nil {
|
||||
t.Fatal("expected Radio to be set")
|
||||
}
|
||||
if *page.Items[0].Radio != "915.0,125,7" {
|
||||
t.Errorf("expected Radio 915.0,125,7, got %s", *page.Items[0].Radio)
|
||||
if *page.Items[0].Radio != "915,125,7" {
|
||||
t.Errorf("expected Radio 915,125,7, got %s", *page.Items[0].Radio)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,6 +375,36 @@ func TestGetNode_LastAdvertAtNil(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNode_Stale(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
|
||||
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
|
||||
mock.EXPECT().
|
||||
GetNodeByID(gomock.Any(), nodeID).
|
||||
Return(sqlc.GetNodeByIDRow{
|
||||
ID: nodeID,
|
||||
PublicKey: []byte{0x01},
|
||||
NodeType: 1, // companion -- Stale applies to every node type, unlike clock drift
|
||||
FirstSeen: pgtype.Timestamptz{Time: time.Now().Add(-72 * time.Hour), Valid: true},
|
||||
LastSeen: pgtype.Timestamptz{Time: time.Now().Add(-48 * time.Hour), Valid: true},
|
||||
}, nil)
|
||||
|
||||
mock.EXPECT().
|
||||
GetNodeNeighbors(gomock.Any(), nodeID).
|
||||
Return([]sqlc.GetNodeNeighborsRow{}, nil)
|
||||
|
||||
store := &Store{q: mock, staleThreshold: 24 * time.Hour}
|
||||
node, err := store.GetNode(context.Background(), nodeID)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !node.Stale {
|
||||
t.Error("expected node last seen 48h ago to be stale with a 24h threshold")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNode_ClockDrift_OutOfSync(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
@@ -624,3 +690,19 @@ func TestListNodes_ExcludeNeighbors_LeavesIDsNil(t *testing.T) {
|
||||
t.Errorf("expected NeighborIDs to stay nil when includeNeighbors is false, got %v", page.Items[0].NeighborIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteOldNodes(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
|
||||
cutoff := time.Now().Add(-30 * 24 * time.Hour)
|
||||
|
||||
mock.EXPECT().
|
||||
DeleteOldNodes(gomock.Any(), gomock.Eq(pgtype.Timestamptz{Time: cutoff, Valid: true})).
|
||||
Return(nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
if err := store.DeleteOldNodes(context.Background(), cutoff); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ func (s *Store) ListObservers(ctx context.Context, iatas []string, observerType,
|
||||
Scopes: v.Scopes,
|
||||
}
|
||||
if v.RadioFreqMhz != nil && v.RadioSf != nil && v.RadioBwKhz != nil {
|
||||
s := fmt.Sprintf("%.1f,%g,%d", *v.RadioFreqMhz, *v.RadioBwKhz, *v.RadioSf)
|
||||
s := fmt.Sprintf("%g,%g,%d", *v.RadioFreqMhz, *v.RadioBwKhz, *v.RadioSf)
|
||||
observer.Radio = &s
|
||||
}
|
||||
if v.DisplayName != nil {
|
||||
|
||||
@@ -128,8 +128,8 @@ func TestListObservers_RadioStringFormatting(t *testing.T) {
|
||||
if page.Items[0].Radio == nil {
|
||||
t.Fatal("expected Radio to be set")
|
||||
}
|
||||
if *page.Items[0].Radio != "915.0,125,7" {
|
||||
t.Errorf("expected Radio 915.0,125,7, got %s", *page.Items[0].Radio)
|
||||
if *page.Items[0].Radio != "915,125,7" {
|
||||
t.Errorf("expected Radio 915,125,7, got %s", *page.Items[0].Radio)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -509,6 +509,18 @@ LIMIT $6;
|
||||
-- packet_observations cascade-delete via FK.
|
||||
DELETE FROM packets WHERE last_heard_at < $1;
|
||||
|
||||
-- name: DeleteOldNodes :exec
|
||||
-- Deletes nodes not seen since the given cutoff. node_iatas and node_neighbors cascade-
|
||||
-- delete via FK. Excludes nodes referenced by observer_owners.owner_node_id -- that FK has
|
||||
-- no ON DELETE action, so deleting one directly would fail the whole statement anyway, and
|
||||
-- an operator manually recorded ownership for that node, so leave it alone even if stale.
|
||||
-- known_routes.node_ids is a plain UUID[] with no FK; a deleted node's id can be left
|
||||
-- dangling in old routes there, but ReconfirmTask already prunes stale/ambiguous routes
|
||||
-- periodically and will clean those up on its own schedule.
|
||||
DELETE FROM nodes
|
||||
WHERE last_seen < $1
|
||||
AND id NOT IN (SELECT owner_node_id FROM observer_owners WHERE owner_node_id IS NOT NULL);
|
||||
|
||||
-- name: DeleteOldChannelIATAs :exec
|
||||
-- Keeps the channel IATA filter in step with packet retention.
|
||||
DELETE FROM channel_iatas WHERE last_heard < $1;
|
||||
|
||||
@@ -57,6 +57,20 @@ func (mr *MockQuerierMockRecorder) DeleteOldChannelIATAs(ctx, lastHeard any) *go
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldChannelIATAs", reflect.TypeOf((*MockQuerier)(nil).DeleteOldChannelIATAs), ctx, lastHeard)
|
||||
}
|
||||
|
||||
// DeleteOldNodes mocks base method.
|
||||
func (m *MockQuerier) DeleteOldNodes(ctx context.Context, lastSeen pgtype.Timestamptz) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteOldNodes", ctx, lastSeen)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteOldNodes indicates an expected call of DeleteOldNodes.
|
||||
func (mr *MockQuerierMockRecorder) DeleteOldNodes(ctx, lastSeen any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldNodes", reflect.TypeOf((*MockQuerier)(nil).DeleteOldNodes), ctx, lastSeen)
|
||||
}
|
||||
|
||||
// DeleteOldPackets mocks base method.
|
||||
func (m *MockQuerier) DeleteOldPackets(ctx context.Context, lastHeardAt pgtype.Timestamptz) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -14,6 +14,14 @@ import (
|
||||
type Querier interface {
|
||||
// Keeps the channel IATA filter in step with packet retention.
|
||||
DeleteOldChannelIATAs(ctx context.Context, lastHeard pgtype.Timestamptz) error
|
||||
// Deletes nodes not seen since the given cutoff. node_iatas and node_neighbors cascade-
|
||||
// delete via FK. Excludes nodes referenced by observer_owners.owner_node_id -- that FK has
|
||||
// no ON DELETE action, so deleting one directly would fail the whole statement anyway, and
|
||||
// an operator manually recorded ownership for that node, so leave it alone even if stale.
|
||||
// known_routes.node_ids is a plain UUID[] with no FK; a deleted node's id can be left
|
||||
// dangling in old routes there, but ReconfirmTask already prunes stale/ambiguous routes
|
||||
// periodically and will clean those up on its own schedule.
|
||||
DeleteOldNodes(ctx context.Context, lastSeen pgtype.Timestamptz) error
|
||||
// Deletes packets and their observations older than the given cutoff.
|
||||
// packet_observations cascade-delete via FK.
|
||||
DeleteOldPackets(ctx context.Context, lastHeardAt pgtype.Timestamptz) error
|
||||
|
||||
@@ -22,6 +22,24 @@ func (q *Queries) DeleteOldChannelIATAs(ctx context.Context, lastHeard pgtype.Ti
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteOldNodes = `-- name: DeleteOldNodes :exec
|
||||
DELETE FROM nodes
|
||||
WHERE last_seen < $1
|
||||
AND id NOT IN (SELECT owner_node_id FROM observer_owners WHERE owner_node_id IS NOT NULL)
|
||||
`
|
||||
|
||||
// Deletes nodes not seen since the given cutoff. node_iatas and node_neighbors cascade-
|
||||
// delete via FK. Excludes nodes referenced by observer_owners.owner_node_id -- that FK has
|
||||
// no ON DELETE action, so deleting one directly would fail the whole statement anyway, and
|
||||
// an operator manually recorded ownership for that node, so leave it alone even if stale.
|
||||
// known_routes.node_ids is a plain UUID[] with no FK; a deleted node's id can be left
|
||||
// dangling in old routes there, but ReconfirmTask already prunes stale/ambiguous routes
|
||||
// periodically and will clean those up on its own schedule.
|
||||
func (q *Queries) DeleteOldNodes(ctx context.Context, lastSeen pgtype.Timestamptz) error {
|
||||
_, err := q.db.Exec(ctx, deleteOldNodes, lastSeen)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteOldPackets = `-- name: DeleteOldPackets :exec
|
||||
DELETE FROM packets WHERE last_heard_at < $1
|
||||
`
|
||||
|
||||
+5
-2
@@ -23,13 +23,16 @@ import (
|
||||
type Store struct {
|
||||
q sqlc.Querier
|
||||
clockDriftThreshold time.Duration // see api.Node.ClockOutOfSync
|
||||
staleThreshold time.Duration // see api.NodeSummary.Stale
|
||||
}
|
||||
|
||||
// New creates a Store backed by the given pgxpool connection pool. clockDriftThreshold is
|
||||
// the |device clock - server clock| magnitude above which a repeater/room server's
|
||||
// clockOutOfSync is reported true; see internal/config.ResolvedConfig.ClockDriftThreshold.
|
||||
func New(pool *pgxpool.Pool, clockDriftThreshold time.Duration) *Store {
|
||||
return &Store{q: sqlc.New(pool), clockDriftThreshold: clockDriftThreshold}
|
||||
// staleThreshold is how long since last_seen before a node's Stale is reported true; see
|
||||
// internal/config.ResolvedConfig.NodeStaleThreshold.
|
||||
func New(pool *pgxpool.Pool, clockDriftThreshold, staleThreshold time.Duration) *Store {
|
||||
return &Store{q: sqlc.New(pool), clockDriftThreshold: clockDriftThreshold, staleThreshold: staleThreshold}
|
||||
}
|
||||
|
||||
func (s *Store) ResolvePathHashes(ctx context.Context, iata string, hashes [][]byte) (map[string][]api.ResolvedPathEntry, error) {
|
||||
|
||||
Reference in New Issue
Block a user