diff --git a/hbrdd/dbf/dbf.go b/hbrdd/dbf/dbf.go index ef75b82..2591460 100644 --- a/hbrdd/dbf/dbf.go +++ b/hbrdd/dbf/dbf.go @@ -393,6 +393,25 @@ func (a *DBFArea) Close() error { if a.dirty { a.flushRecord() } + // Shared-mode EOF write — same max-merge as updateHeader. A + // peer Append between our last refresh and this write may have + // grown the file past our local recCount. Writing the EOF + // marker at our stale offset (HeaderLen + localCount*RecLen) + // would overwrite byte 0 of the peer's just-appended record + // (its delete flag) — the record body bytes survive but byte 0 + // flips from ' ' (RecordActive) to 0x1A (EOFMarker). Re-read + // the disk header to pick the larger of {local, disk} before + // computing EOFOffset. + if a.shared { + if _, err := a.dataFile.Seek(0, 0); err == nil { + if hdr, err := ReadHeader(a.dataFile); err == nil { + if hdr.RecCount > a.recCount { + a.recCount = hdr.RecCount + a.header.RecCount = hdr.RecCount + } + } + } + } a.dataFile.WriteAt([]byte{EOFMarker}, a.header.EOFOffset()) a.updateHeader() // Release any held byte-range locks before closing the fd — POSIX @@ -784,8 +803,15 @@ func (a *DBFArea) Append() error { return fmt.Errorf("table is read-only") } - // Batch consecutive APPENDs: save current dirty record to appendBuf instead of writing to disk. - if a.dirty && a.ghost { + // Batch consecutive APPENDs: save current dirty record to + // appendBuf instead of writing to disk. SHARED mode opts out + // of batching — a peer SELECT that opens the file while one + // of our slots is still buffered would iterate to that slot, + // ReadAt zero bytes (slot is reserved on disk but unwritten), + // and treat the record as garbage. Writing each APPEND straight + // through trades some throughput for "self+peer-coherent on + // every cursor move" semantics. EXCLUSIVE mode still batches. + if a.dirty && a.ghost && !a.shared { // Previous was also an APPEND — accumulate in batch buffer (no syscall) recLen := int(a.header.RecordLen) if a.appendBuf == nil {