fix(P07): concurrency safety — SQLite, flock, cache, atomic writes (REQ-156)

- SQLite busy_timeout(5000) + SetMaxOpenConns(1) on all 4 DSNs
- secrets file flock (concurrent set on same ns no longer loses data)
- upgrade lock file (refuse concurrent orca upgrade)
- backup lock file (refuse concurrent backup)
- cache invalidation by writes (read-after-write consistency)
- Executor.Run mutex scope fix (hold only for DB inserts)
- ns create/inherit/set-constraint atomic writeNSMdAtomic
- writeCurrentLead + rotateSSHKeys atomic
- consolidate 3 writeAtomic impls onto security.WriteAtomic
- WebAuthn session stores guarded with sync.Mutex

Tests: concurrent secrets set, upgrade lock rejection, cache
read-after-write, WebAuthn session thread-safety (pass under -race).

---ci---
project: orca
phase: 7
milestone: v0.13
status: complete
requirements:
  covered: [156]
---/ci---
This commit is contained in:
Jon Chery
2026-08-10 12:27:05 +00:00
parent 978334a4bc
commit 0358efe95b
18 changed files with 932 additions and 68 deletions
+21
View File
@@ -101,6 +101,27 @@ func cachePutList(class, key string, list any, ttl time.Duration) {
cachePopulate(class, key, val, ttl)
}
// cacheInvalidate drops all entries for the given cache class
// (REQ-156, P07 T5). It is called after write operations (node
// join/leave, ns create/delete, job run/stop) so the very next read
// does not surface a stale cached list. Errors are logged but never
// returned — a failed invalidation must not break the write command
// (the cache entry will simply expire at its TTL).
func cacheInvalidate(class string) {
if !cacheAvailable() {
return
}
c, err := cache.Open(paths.CacheDB())
if err != nil {
slog.Warn("cache: open failed during invalidate", "class", class, "err", err)
return
}
defer c.Close()
if err := c.Invalidate(class); err != nil {
slog.Warn("cache: invalidate failed", "class", class, "err", err)
}
}
// Per-class TTLs (P00-T2).
const (
cacheNodeTTL = 30 * time.Second