feat(rdd): real POSIX file/record locking + gap analysis doc
Replaces the FLOCK/DBRLOCK/DBRUNLOCK no-op stubs with actual
fcntl(F_SETLK) byte-range advisory locks, matching Harbour's
hb_fsLockLarge implementation.
Before: rtlDbRLock always returned .T. regardless of contention.
Multi-process writers could silently corrupt records.
After: Non-blocking POSIX byte-range locks per file descriptor.
Cross-process exclusion verified by a subprocess-spawning
Go test that witnesses BUSY vs OK transitions.
New files:
hbrdd/dbf/locks_posix.go fcntl F_WRLCK/F_UNLCK wrappers
hbrdd/dbf/locks_windows.go stub (TODO: LockFileEx)
hbrdd/dbf/lock_multi_test.go cross-process verification
docs/gap-analysis.md honest Harbour parity assessment
Modified:
hbrdd/dbf/dbf.go
- DBFArea gains fileLocked bool + lockedRecs map
- Close() calls releaseAllLocks() before dropping the fd
hbrtl/database.go
- rtlDbRLock / rtlDbRUnlock now delegate to DBFArea.LockRecord /
UnlockRecord instead of returning fixed .T./NIL
- New rtlFLock / rtlDbUnlock for FLOCK() / DBUNLOCK()
hbrtl/register.go
- FLOCK and DBUNLOCK symbols registered (were missing entirely)
compiler/analyzer/analyzer.go
- FLOCK / DBUNLOCK added to RTL known-function set
Lock region layout (non-overlapping on purpose):
FLOCK region [0, HeaderLen+1)
Record N region [RecordOffset(N), RecordLen)
So a workarea can hold FLOCK and multiple DBRLOCK simultaneously
on the same fd without conflict.
Design rationale (captured in locks_posix.go header):
* POSIX fcntl, not flock(2) — byte-range + NFS-safe
* Non-blocking F_SETLK — matches Clipper FLOCK() → .F. semantics
* Released explicitly on Close to avoid workarea-sharing races
* Windows falls back to no-op (TODO: LockFileEx)
Verification:
go test ./hbrdd/dbf/ -run TestFLockBlocksAcrossProcesses PASS
go test ./hbrdd/dbf/ -run TestRLockBlocksAcrossProcesses PASS
go test ./... ALL PASS
FiveSql2 43/43 100%
compat_harbour 51/51 100%
The gap-analysis doc (docs/gap-analysis.md) is a running inventory
of what works vs what's still missing vs Harbour 3.2, written for
users evaluating Five for production — not a sales pitch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ package hbrtl
|
||||
import (
|
||||
"five/hbrt"
|
||||
"five/hbrdd"
|
||||
"five/hbrdd/dbf"
|
||||
)
|
||||
|
||||
// FIELDPUT(nField, xValue) → xValue
|
||||
@@ -320,19 +321,121 @@ func rtlDbRecall(t *hbrt.Thread) {
|
||||
t.RetNil()
|
||||
}
|
||||
|
||||
// DBRLOCK([nRecNo]) → lSuccess — always succeeds in Five (single-threaded)
|
||||
// DBRLOCK([nRecNo]) → lSuccess
|
||||
// Acquires an advisory byte-range lock on a single record via fcntl(F_SETLK).
|
||||
// Non-blocking: returns .F. if another process already holds the lock.
|
||||
// Harbour: SELF_LOCK(a, &lockInfo) with DBLM_EXCLUSIVE.
|
||||
func rtlDbRLock(t *hbrt.Thread) {
|
||||
nParams := t.ParamCount()
|
||||
t.Frame(nParams, 0)
|
||||
defer t.EndProcFast()
|
||||
t.RetBool(true) // always succeeds
|
||||
wam := getWA(t)
|
||||
if wam == nil {
|
||||
t.RetBool(false)
|
||||
return
|
||||
}
|
||||
area := wam.Current()
|
||||
if area == nil {
|
||||
t.RetBool(false)
|
||||
return
|
||||
}
|
||||
da, ok := area.(*dbf.DBFArea)
|
||||
if !ok {
|
||||
// Non-DBF drivers: assume success (in-memory, etc.)
|
||||
t.RetBool(true)
|
||||
return
|
||||
}
|
||||
var recNo uint32
|
||||
if nParams >= 1 && !t.Local(1).IsNil() {
|
||||
recNo = uint32(t.Local(1).AsNumInt())
|
||||
}
|
||||
locked, err := da.LockRecord(recNo)
|
||||
if err != nil {
|
||||
t.RetBool(false)
|
||||
return
|
||||
}
|
||||
t.RetBool(locked)
|
||||
}
|
||||
|
||||
// DBRUNLOCK([nRecNo]) → NIL
|
||||
// Releases a specific record lock, or all record locks held by this
|
||||
// workarea if called without arguments. Harbour: SELF_UNLOCK.
|
||||
func rtlDbRUnlock(t *hbrt.Thread) {
|
||||
nParams := t.ParamCount()
|
||||
t.Frame(nParams, 0)
|
||||
defer t.EndProcFast()
|
||||
wam := getWA(t)
|
||||
if wam == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
}
|
||||
area := wam.Current()
|
||||
if area == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
}
|
||||
da, ok := area.(*dbf.DBFArea)
|
||||
if !ok {
|
||||
t.RetNil()
|
||||
return
|
||||
}
|
||||
var recNo uint32
|
||||
if nParams >= 1 && !t.Local(1).IsNil() {
|
||||
recNo = uint32(t.Local(1).AsNumInt())
|
||||
}
|
||||
_ = da.UnlockRecord(recNo)
|
||||
t.RetNil()
|
||||
}
|
||||
|
||||
// FLOCK() → lSuccess
|
||||
// Acquires an exclusive file-wide lock via fcntl. Non-blocking: returns .F.
|
||||
// if another process holds the file lock. Harbour: SELF_LOCK with DBLM_FILE.
|
||||
func rtlFLock(t *hbrt.Thread) {
|
||||
t.Frame(0, 0)
|
||||
defer t.EndProcFast()
|
||||
wam := getWA(t)
|
||||
if wam == nil {
|
||||
t.RetBool(false)
|
||||
return
|
||||
}
|
||||
area := wam.Current()
|
||||
if area == nil {
|
||||
t.RetBool(false)
|
||||
return
|
||||
}
|
||||
da, ok := area.(*dbf.DBFArea)
|
||||
if !ok {
|
||||
t.RetBool(true) // in-memory etc.
|
||||
return
|
||||
}
|
||||
locked, err := da.LockFile()
|
||||
if err != nil {
|
||||
t.RetBool(false)
|
||||
return
|
||||
}
|
||||
t.RetBool(locked)
|
||||
}
|
||||
|
||||
// DBUNLOCK() → NIL
|
||||
// Releases all locks (file + record) held by the current workarea.
|
||||
// Harbour: SELF_UNLOCK with no record number.
|
||||
func rtlDbUnlock(t *hbrt.Thread) {
|
||||
t.Frame(0, 0)
|
||||
defer t.EndProcFast()
|
||||
wam := getWA(t)
|
||||
if wam == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
}
|
||||
area := wam.Current()
|
||||
if area == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
}
|
||||
if da, ok := area.(*dbf.DBFArea); ok {
|
||||
_ = da.UnlockRecord(0)
|
||||
_ = da.UnlockFile()
|
||||
}
|
||||
t.RetNil()
|
||||
}
|
||||
|
||||
|
||||
@@ -177,6 +177,8 @@ func RegisterRTL(vm *hbrt.VM) {
|
||||
hbrt.Sym("DBCOMMIT", hbrt.FsPublic, rtlDbCommit),
|
||||
hbrt.Sym("DBRLOCK", hbrt.FsPublic, rtlDbRLock),
|
||||
hbrt.Sym("DBRUNLOCK", hbrt.FsPublic, rtlDbRUnlock),
|
||||
hbrt.Sym("FLOCK", hbrt.FsPublic, rtlFLock),
|
||||
hbrt.Sym("DBUNLOCK", hbrt.FsPublic, rtlDbUnlock),
|
||||
hbrt.Sym("DBSEEK", hbrt.FsPublic, rtlDbSeek),
|
||||
hbrt.Sym("DBSELECTAREA", hbrt.FsPublic, rtlDbSelectArea),
|
||||
hbrt.Sym("DBPACK", hbrt.FsPublic, rtlDbPack),
|
||||
|
||||
Reference in New Issue
Block a user