diff --git a/_FiveSql2/src/TSqlSession.prg b/_FiveSql2/src/TSqlSession.prg index 4775469..2df354c 100644 --- a/_FiveSql2/src/TSqlSession.prg +++ b/_FiveSql2/src/TSqlSession.prg @@ -50,6 +50,21 @@ ENDCLASS METHOD New() CLASS TSqlSession + + /* DATA hPlanCache INIT { => } would compile-share one hash + * across all sessions on some gengo paths (same gotcha + * called out in TSqlExecutor.prg's hSubCache comment). + * Without explicit per-instance init, two pgserver + * connections would both end up reading and writing into the + * SAME hPlanCache concurrently — crash on the Go-level map + * with "concurrent map writes". Same for the txn log + the + * savepoints + role-perms hash. */ + ::aTxnLog := {} + ::hSavepoints := { => } + ::hPlanCache := { => } + ::hRolePerms := { => } + ::aOpenAreas := {} + RETURN SELF diff --git a/hbrdd/dbf/dbf.go b/hbrdd/dbf/dbf.go index 1941dfe..dc834d7 100644 --- a/hbrdd/dbf/dbf.go +++ b/hbrdd/dbf/dbf.go @@ -18,6 +18,7 @@ import ( "fmt" "os" "strings" + "sync/atomic" ) // DBFArea implements the DBF database driver. @@ -459,7 +460,8 @@ func (a *DBFArea) RecCount() (uint32, error) { // cross-process freshness (e.g. SqlScan's one-shot row-count // estimate on a workarea we opened this session) can leave the // cache warm. Invalidate on our own Append and dbCloseAll. - if a.recCountCached && a.recCountGen == recCountCacheGen { + gen := atomic.LoadUint64(&recCountCacheGen) + if a.recCountCached && a.recCountGen == gen { return a.recCount, nil } size, err := a.dataFile.Seek(0, 2) @@ -468,7 +470,7 @@ func (a *DBFArea) RecCount() (uint32, error) { } a.recCount = uint32((size - int64(a.header.HeaderLen)) / int64(a.header.RecordLen)) a.recCountCached = true - a.recCountGen = recCountCacheGen + a.recCountGen = gen } return a.recCount, nil } @@ -483,9 +485,11 @@ var recCountCacheGen uint64 = 1 // InvalidateRecCountCache bumps the generation counter so every DBFArea's // cached count becomes stale and the next RecCount() call re-queries the -// filesystem. +// filesystem. Atomic so it can be called from any goroutine without a +// lock — the only invariant readers care about is "if I see a value > X, +// my cache is stale", and atomic.LoadUint64 is sufficient. func InvalidateRecCountCache() { - recCountCacheGen++ + atomic.AddUint64(&recCountCacheGen, 1) } func (a *DBFArea) Deleted() bool { @@ -808,6 +812,16 @@ func (a *DBFArea) Append() error { if _, err := a.dataFile.Seek(0, 0); err == nil { _ = WriteHeader(a.dataFile, &a.header) } + // Bump the global recCount-cache generation so every peer + // DBFArea on this file re-stats on its next RecCount() call. + // Without this, a pgserver connection that holds a cached + // count from before our APPEND keeps seeing the pre-Append + // row total — its SELECT misses the row we just inserted, + // even though loadRecord()'s mmap-fallback path can read + // the record bytes fine. Cheap (single atomic increment); + // the cost is one Seek+stat per peer's next RecCount call, + // which is exactly what cross-connection freshness needs. + InvalidateRecCountCache() } // Promote to owned buffer for writing