mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-13 07:55:41 +00:00
fix: move SQL writes and path resolution out of store mutex in backfillResolvedPaths
- Collect immutable snapshots (DecodedJSON, PayloadType) under RLock instead of holding StoreTx pointers that could be evicted between unlock/lock - Resolve paths outside the lock (pm and graph are read-only) - Persist to SQLite without holding any lock (separate RW connection) - Only take write lock for in-memory updates at the end - Remove tautological TestNeighborPersistFileCompiles (compilation already validates symbol accessibility) - Remove unnecessary init() with os.MkdirAll on TempDir
This commit is contained in:
@@ -310,21 +310,28 @@ func unmarshalResolvedPath(s string) []*string {
|
||||
|
||||
// backfillResolvedPaths resolves paths for all observations that have NULL resolved_path.
|
||||
func backfillResolvedPaths(store *PacketStore, dbPath string) int {
|
||||
store.mu.RLock()
|
||||
pm := store.nodePM
|
||||
graph := store.graph
|
||||
// Collect observations needing resolution
|
||||
// Collect pending observations and snapshot immutable fields under read lock.
|
||||
type obsRef struct {
|
||||
obsID int
|
||||
pathJSON string
|
||||
observerID string
|
||||
tx *StoreTx
|
||||
txJSON string // snapshot of DecodedJSON for extractFromNode
|
||||
payloadType *int
|
||||
}
|
||||
store.mu.RLock()
|
||||
pm := store.nodePM
|
||||
graph := store.graph
|
||||
var pending []obsRef
|
||||
for _, tx := range store.packets {
|
||||
for _, obs := range tx.Observations {
|
||||
if obs.ResolvedPath == nil && obs.PathJSON != "" && obs.PathJSON != "[]" {
|
||||
pending = append(pending, obsRef{obs.ID, obs.PathJSON, obs.ObserverID, tx})
|
||||
pending = append(pending, obsRef{
|
||||
obsID: obs.ID,
|
||||
pathJSON: obs.PathJSON,
|
||||
observerID: obs.ObserverID,
|
||||
txJSON: tx.DecodedJSON,
|
||||
payloadType: tx.PayloadType,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,6 +341,30 @@ func backfillResolvedPaths(store *PacketStore, dbPath string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Resolve paths outside the lock — resolvePathForObs only reads pm and graph.
|
||||
type resolved struct {
|
||||
obsID int
|
||||
rp []*string
|
||||
rpJSON string
|
||||
}
|
||||
var results []resolved
|
||||
for _, ref := range pending {
|
||||
// Build a minimal StoreTx for extractFromNode (only needs DecodedJSON + PayloadType).
|
||||
fakeTx := &StoreTx{DecodedJSON: ref.txJSON, PayloadType: ref.payloadType}
|
||||
rp := resolvePathForObs(ref.pathJSON, ref.observerID, fakeTx, pm, graph)
|
||||
if len(rp) > 0 {
|
||||
rpJSON := marshalResolvedPath(rp)
|
||||
if rpJSON != "" {
|
||||
results = append(results, resolved{ref.obsID, rp, rpJSON})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Persist to SQLite (no lock needed — separate RW connection).
|
||||
rw, err := openRW(dbPath)
|
||||
if err != nil {
|
||||
log.Printf("[store] backfill: open rw error: %v", err)
|
||||
@@ -355,28 +386,26 @@ func backfillResolvedPaths(store *PacketStore, dbPath string) int {
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
count := 0
|
||||
store.mu.Lock()
|
||||
for _, ref := range pending {
|
||||
rp := resolvePathForObs(ref.pathJSON, ref.observerID, ref.tx, pm, graph)
|
||||
if len(rp) > 0 {
|
||||
rpJSON := marshalResolvedPath(rp)
|
||||
if rpJSON != "" {
|
||||
stmt.Exec(rpJSON, ref.obsID)
|
||||
// Update in-memory
|
||||
if obs, ok := store.byObsID[ref.obsID]; ok {
|
||||
obs.ResolvedPath = rp
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
for _, r := range results {
|
||||
stmt.Exec(r.rpJSON, r.obsID)
|
||||
}
|
||||
store.mu.Unlock()
|
||||
|
||||
if err := sqlTx.Commit(); err != nil {
|
||||
log.Printf("[store] backfill: commit error: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Update in-memory state under write lock.
|
||||
store.mu.Lock()
|
||||
count := 0
|
||||
for _, r := range results {
|
||||
if obs, ok := store.byObsID[r.obsID]; ok {
|
||||
obs.ResolvedPath = r.rp
|
||||
count++
|
||||
}
|
||||
}
|
||||
store.mu.Unlock()
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -451,24 +450,4 @@ func TestResolvedPathOmittedWhenEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeighborPersistFileCompiles(t *testing.T) {
|
||||
// This test exists just to ensure neighbor_persist.go compiles
|
||||
// and its symbols are accessible. The real tests are above.
|
||||
_ = openRW
|
||||
_ = ensureNeighborEdgesTable
|
||||
_ = ensureResolvedPathColumn
|
||||
_ = loadNeighborEdgesFromDB
|
||||
_ = persistEdge
|
||||
_ = resolvePathForObs
|
||||
_ = marshalResolvedPath
|
||||
_ = unmarshalResolvedPath
|
||||
_ = backfillResolvedPaths
|
||||
_ = buildAndPersistEdges
|
||||
_ = neighborEdgesTableExists
|
||||
}
|
||||
|
||||
// Remove temp files
|
||||
func init() {
|
||||
// Ensure temp dirs are cleaned up
|
||||
os.MkdirAll(os.TempDir(), 0755)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user