Skip to content

Commit 381b496

Browse files
committed
mpt/internal/pmem: add Mem.DiskSize API
Until now the client of pmem has not needed to know the size of the space used on the on-disk leaf file. Variable-sized keys and values is going to change that. Add tracking of the size of the disk data. Also be more explicit about the invariants guaranteed in transactions with respect to ReadDisk/WriteDisk, and test disks thoroughly in the random testing.
1 parent e0a6396 commit 381b496

2 files changed

Lines changed: 304 additions & 55 deletions

File tree

mpt/internal/pmem/pmem.go

Lines changed: 125 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ var (
9696
// In the worst case, the framing of every data byte in
9797
// a mutation might be framed by a maxVarint-byte offset
9898
// and a 1-byte count. That's 1MB*8 = 8 MB.
99-
// There needs to be headroom for the memory-length patch,
100-
// so the minimum would be 8MB + 8 bytes, but we bump the
101-
// patch block size to 16 MB instead.
99+
// There needs to be headroom for the memory-length and
100+
// disk-length patches, so the minimum would be 8MB + 16 bytes,
101+
// but we bump the patch block size to 16 MB instead.
102102
maxPatch = 16 << 20
103103
)
104104

@@ -143,34 +143,47 @@ func (*devNull) Sync() error { return nil }
143143
// to larger sizes using [Mem.Expand].
144144
// (Shrinking the memory is not implemented.)
145145
//
146-
// Mutations can be grouped into atomic transactions using
146+
// Alongside the memory, a third disk file holds “disk-only” storage
147+
// accessed by [Mem.ReadDisk] and [Mem.WriteDisk].
148+
// This storage is not held in memory, only on disk.
149+
//
150+
// By default, each [Mem.Mutate] or [Mem.WriteDisk] is a separate
151+
// sequenced, atomic transaction. If a program crashes and the Mem is reopened,
152+
// the state of the memory is guaranteed to correspond to the state after
153+
// some arbitrary transaction.
154+
// The state of the disk is guaranteed to include all the writes through that
155+
// transaction, but it may also include later writes that were not recovered.
156+
//
157+
// Mutations and disk writes can be grouped into larger transactions using
147158
// [Mem.BeginGroup] and [Mem.EndGroup].
148159
//
149-
// Calling [Mem.Sync] ensures that all modifications have been flushed
160+
// Calling [Mem.Sync] ensures that all transactions have been flushed
150161
// to the underlying files, guaranteeing that a future [Open] will observe them.
151162
//
152163
// Calling [Mem.Close] closes the memory and leaves the mapping unreadable.
153164
// Future accesses to the slice data returned by [Mem.Data] must be avoided.
154165
// Those accesses will fault, meaning they crash the program unless
155166
// [runtime/debug.SetPanicOnFault] has been used.
156167
type Mem struct {
157-
magic string
158-
id [16]byte
159-
tmp [frameExtra]byte
160-
ptmp [2 * binary.MaxVarintLen64]byte
161-
span *span.Span
162-
mem []byte
163-
patched int // length of “patched” section of memory
164-
current *writer
165-
next *writer
166-
disk File // disk-only (not in memory) storage
167-
diskOff int64 // offset where user writes begin
168-
patch []byte
169-
group int // group start in patch, or -1 if not in group
170-
groupData int // total group data
171-
err error
172-
closed bool
173-
compact compact
168+
magic string
169+
id [16]byte
170+
tmp [frameExtra]byte
171+
ptmp [2 * binary.MaxVarintLen64]byte
172+
span *span.Span
173+
mem []byte
174+
patched int // length of “patched” section of memory
175+
current *writer
176+
next *writer
177+
disk File // disk-only (not in memory) storage
178+
diskOff int64 // offset where user writes begin
179+
diskSize int64 // logical size of disk-only data
180+
diskPatched int64 // last diskSize recorded in patch
181+
patch []byte
182+
group int // group start in patch, or -1 if not in group
183+
groupData int // total group data
184+
err error
185+
closed bool
186+
compact compact
174187

175188
constantFlushing bool
176189

@@ -400,6 +413,7 @@ func open(magic string, file1, file2, disk File) (_ *Mem, err error) {
400413
if err := m.readFile(r1); err != nil {
401414
return nil, err
402415
}
416+
m.diskPatched = m.diskSize
403417
m.current = newWriter(r1.file, r1.seq)
404418
m.current.off = r1.off
405419
m.next = newWriter(r2.file, 0)
@@ -551,9 +565,12 @@ func (m *Mem) replay(patch []byte) error {
551565
}
552566
if isDisk {
553567
// disk patch
554-
if _, err := m.disk.WriteAt(patch[:count], m.diskOff+int64(off)); err != nil {
555-
return m.broken(err)
568+
if count > 0 {
569+
if _, err := m.disk.WriteAt(patch[:count], m.diskOff+int64(off)); err != nil {
570+
return m.broken(err)
571+
}
556572
}
573+
m.diskSize = max(m.diskSize, int64(off+count))
557574
} else {
558575
// memory patch
559576
if off+count > uint64(len(m.mem)) {
@@ -606,16 +623,30 @@ func (m *Mem) Offset(b []byte) (offset int, ok bool) {
606623
return int(off), ok
607624
}
608625

609-
// BeginGroup starts an atomic mutation group.
610-
// Expand and Mutate calls between Begin and [Mem.EndGroup]
611-
// are guaranteed to be observed as an atomic unit
612-
// upon reloading the memory: either they will all be
613-
// present or none of them will be.
626+
// BeginGroup starts an atomic mutation group (a transaction).
627+
//
614628
// Calls to BeginGroup must be followed eventually by a call to EndGroup
615629
// and cannot be nested: it is an error to call BeginGroup twice
616630
// without an intervening EndGroup.
617631
//
618-
// A group is limited to mutation of at most MaxGroupBytes bytes of mutated data.
632+
// For the Expand, Mutate, and WriteDisk calls between Begin and [Mem.EndGroup],
633+
// there are three possible outcomes after a Mem has been reloaded:
634+
//
635+
// 1. All of the effects will be observed (group reloaded).
636+
// 2. None of the effects will be observed (group not reloaded).
637+
// 3. No memory effects will be observed, and the WriteDisk calls
638+
// will not be observable by [Mem.DiskSize], but some or all of the
639+
// disk writes could still be observed by [Mem.ReadDisk].
640+
//
641+
// Considering only the memory, the group is an atomic unit,
642+
// either fully observed or not observed at all.
643+
// The disk is added to the group in a one-sided manner:
644+
// if the memory changes are observed, then all the disk changes are observed too,
645+
// but not the reverse: later disk changes (all of them or a subset of them) can be observed
646+
// without their corresponding memory changes.
647+
//
648+
// A group is limited to mutation of at most MaxGroupBytes bytes of data
649+
// modified by the combination of Mutate and WriteDisk.
619650
func (m *Mem) BeginGroup() error {
620651
if m.err != nil {
621652
return m.err
@@ -624,13 +655,17 @@ func (m *Mem) BeginGroup() error {
624655
return fmt.Errorf("atomic mutation group already begun")
625656
}
626657

627-
// Patch buffer always has room to add an empty mutation
628-
// at the end of the memory, to represent the most recent Expand.
629-
// If the group grows too large, we will flush up to but not
630-
// including the group, so add the empty mutation now.
658+
// Patch buffer always has room to add empty mutations
659+
// at the end of the memory and disk, to represent the most
660+
// recent Expand/WriteDisk. If the group grows too large,
661+
// we will flush up to but not including the group,
662+
// so add the empty mutations now.
631663
if err := m.addMemLenPatch(); err != nil {
632664
return err
633665
}
666+
if err := m.addDiskLenPatch(); err != nil {
667+
return err
668+
}
634669

635670
m.group = len(m.patch)
636671
m.groupData = 0
@@ -664,9 +699,9 @@ func (m *Mem) Mutate(dst, src []byte) error {
664699

665700
// WriteDisk writes src to the disk-only file at offset off.
666701
// It guarantees that on recovery after a crash,
667-
// all disk writes that occurred before the latest recovered Mutate
668-
// will be available for reading.
669-
// (Disk writes that happened after that Mutate may or may not
702+
// all disk writes that occurred in or before
703+
// the latest recovered transaction will be available for reading.
704+
// (Disk writes that happened after that transaction may or may not
670705
// be available for reading as well.)
671706
func (m *Mem) WriteDisk(src []byte, off int64) error {
672707
if m.err != nil {
@@ -680,6 +715,8 @@ func (m *Mem) WriteDisk(src []byte, off int64) error {
680715
if err != nil {
681716
return m.broken(err)
682717
}
718+
m.diskSize = max(m.diskSize, off+int64(len(src)))
719+
m.diskPatched = max(m.diskPatched, off+int64(len(src)))
683720
return nil
684721
})
685722
}
@@ -689,6 +726,12 @@ func (m *Mem) ReadDisk(dst []byte, off int64) error {
689726
if m.err != nil {
690727
return m.err
691728
}
729+
if off < 0 || off > m.diskSize || int64(len(dst)) > m.diskSize-off {
730+
return fmt.Errorf("disk read out of range")
731+
}
732+
if len(dst) == 0 {
733+
return nil
734+
}
692735
_, err := m.disk.ReadAt(dst, m.diskOff+off)
693736
if err != nil {
694737
if err == io.EOF {
@@ -699,6 +742,16 @@ func (m *Mem) ReadDisk(dst []byte, off int64) error {
699742
return nil
700743
}
701744

745+
// DiskSize returns the logical size of the disk-only data.
746+
// This is the maximum offset+length across all WriteDisk calls.
747+
// On recovery after a crash, DiskSize will be the logical disk size
748+
// as of the latest recovered transaction, even if additional disk
749+
// writes to larger offsets happened after that transaction and
750+
// are still present in the file.
751+
func (m *Mem) DiskSize() int64 {
752+
return m.diskSize
753+
}
754+
702755
// mutate logs a write to the patch block, starting a new patch block if necessary.
703756
// It calls commit to apply the actual write once it has checked a few
704757
// error conditions.
@@ -721,7 +774,7 @@ func (m *Mem) mutate(off uint64, src []byte, commit func() error) error {
721774
p := m.ptmp[:0]
722775
p = binary.AppendUvarint(p, off)
723776
p = binary.AppendUvarint(p, uint64(len(src)))
724-
if len(m.patch)+len(p)+len(src)+maxVarint+1 > maxPatch {
777+
if len(m.patch)+len(p)+len(src)+2*(maxVarint+1) > maxPatch {
725778
if err := m.flushPatch(true); err != nil {
726779
return err
727780
}
@@ -765,6 +818,9 @@ func (m *Mem) flushPatch(needSpace bool) error {
765818
if err := m.addMemLenPatch(); err != nil {
766819
return err
767820
}
821+
if err := m.addDiskLenPatch(); err != nil {
822+
return err
823+
}
768824
p = m.patch
769825
}
770826
if len(p) == 0 {
@@ -802,6 +858,10 @@ func (m *Mem) EndGroup() error {
802858
m.mutate(uint64(len(m.mem))<<1, nil, nil)
803859
m.patched = len(m.mem)
804860
}
861+
if m.diskPatched != m.diskSize {
862+
m.mutate(uint64(m.diskSize)<<1|1, nil, nil)
863+
m.diskPatched = m.diskSize
864+
}
805865
m.group = -1
806866

807867
if m.next.seq > 0 && m.compact.off == m.compact.end && len(m.patch) > 0 {
@@ -826,6 +886,22 @@ func (m *Mem) addMemLenPatch() error {
826886
return nil
827887
}
828888

889+
// addDiskLenPatch adds a final "disk length" patch to m.patch.
890+
// This records the current disk data size so that on replay,
891+
// DiskSize is correctly initialized.
892+
func (m *Mem) addDiskLenPatch() error {
893+
if m.disk == nil || m.diskPatched == m.diskSize {
894+
return nil
895+
}
896+
if len(m.patch)+maxVarint+1 > maxPatch {
897+
return m.broken(fmt.Errorf("pmem internal patch overflow"))
898+
}
899+
m.patch = binary.AppendUvarint(m.patch, uint64(m.diskSize)<<1|1)
900+
m.patch = binary.AppendUvarint(m.patch, 0)
901+
m.diskPatched = m.diskSize
902+
return nil
903+
}
904+
829905
// writeFrame writes a frame containing data to w.
830906
func (m *Mem) writeFrame(w *writer, data []byte) error {
831907
f := m.tmp[:frameSize]
@@ -857,7 +933,7 @@ func (m *Mem) writeFrame(w *writer, data []byte) error {
857933
func (m *Mem) maybeCompact(n int) error {
858934
if m.next.seq == 0 && m.current.off < 2*int64(len(m.mem)) {
859935
// Current disk file is less than twice the tree memory.
860-
// Not worth compacting yem.
936+
// Not worth compacting yet.
861937
return nil
862938
}
863939

@@ -935,6 +1011,15 @@ func (m *Mem) maybeCompact(n int) error {
9351011
if err := m.disk.Sync(); err != nil {
9361012
return m.broken(err)
9371013
}
1014+
// Write a disk-length patch to m.next so that on recovery
1015+
// DiskSize is correctly initialized from the compacted file.
1016+
var diskLenPatch []byte
1017+
diskLenPatch = binary.AppendUvarint(diskLenPatch, uint64(m.diskSize)<<1|1)
1018+
diskLenPatch = binary.AppendUvarint(diskLenPatch, 0)
1019+
if err := m.writeFrame(m.next, diskLenPatch); err != nil {
1020+
return err
1021+
}
1022+
m.diskPatched = m.diskSize
9381023
}
9391024

9401025
// Open will start using the tree when the bigger sequence number hits the disk,
@@ -958,6 +1043,7 @@ func (m *Mem) maybeCompact(n int) error {
9581043
setCurrent(m.current.file, true, int(m.current.off))
9591044
setCurrent(m.next.file, false, int(m.next.off))
9601045
m.next.seq = 0
1046+
m.diskPatched = m.diskSize
9611047
return nil
9621048
}
9631049

0 commit comments

Comments
 (0)