Skip to content

Commit 07e3c0e

Browse files
rscFiloSottile
authored andcommitted
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 82ec823 commit 07e3c0e

2 files changed

Lines changed: 327 additions & 58 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

@@ -401,6 +414,7 @@ func open(magic string, file1, file2, disk File) (_ *Mem, err error) {
401414
if err := m.readFile(r1); err != nil {
402415
return nil, err
403416
}
417+
m.diskPatched = m.diskSize
404418
m.current = newWriter(r1.file, r1.seq)
405419
m.current.off = r1.off
406420
m.next = newWriter(r2.file, 0)
@@ -552,9 +566,12 @@ func (m *Mem) replay(patch []byte) error {
552566
}
553567
if isDisk {
554568
// disk patch
555-
if _, err := m.disk.WriteAt(patch[:count], m.diskOff+int64(off)); err != nil {
556-
return m.broken(err)
569+
if count > 0 {
570+
if _, err := m.disk.WriteAt(patch[:count], m.diskOff+int64(off)); err != nil {
571+
return m.broken(err)
572+
}
557573
}
574+
m.diskSize = max(m.diskSize, int64(off+count))
558575
} else {
559576
// memory patch
560577
if off+count > uint64(len(m.mem)) {
@@ -607,16 +624,30 @@ func (m *Mem) Offset(b []byte) (offset int, ok bool) {
607624
return int(off), ok
608625
}
609626

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

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

636671
m.group = len(m.patch)
637672
m.groupData = 0
@@ -665,9 +700,9 @@ func (m *Mem) Mutate(dst, src []byte) error {
665700

666701
// WriteDisk writes src to the disk-only file at offset off.
667702
// It guarantees that on recovery after a crash,
668-
// all disk writes that occurred before the latest recovered Mutate
669-
// will be available for reading.
670-
// (Disk writes that happened after that Mutate may or may not
703+
// all disk writes that occurred in or before
704+
// the latest recovered transaction will be available for reading.
705+
// (Disk writes that happened after that transaction may or may not
671706
// be available for reading as well.)
672707
func (m *Mem) WriteDisk(src []byte, off int64) error {
673708
if m.err != nil {
@@ -681,6 +716,8 @@ func (m *Mem) WriteDisk(src []byte, off int64) error {
681716
if err != nil {
682717
return m.broken(err)
683718
}
719+
m.diskSize = max(m.diskSize, off+int64(len(src)))
720+
m.diskPatched = max(m.diskPatched, off+int64(len(src)))
684721
return nil
685722
})
686723
}
@@ -690,6 +727,12 @@ func (m *Mem) ReadDisk(dst []byte, off int64) error {
690727
if m.err != nil {
691728
return m.err
692729
}
730+
if off < 0 || off > m.diskSize || int64(len(dst)) > m.diskSize-off {
731+
return fmt.Errorf("disk read out of range")
732+
}
733+
if len(dst) == 0 {
734+
return nil
735+
}
693736
_, err := m.disk.ReadAt(dst, m.diskOff+off)
694737
if err != nil {
695738
if err == io.EOF {
@@ -700,6 +743,16 @@ func (m *Mem) ReadDisk(dst []byte, off int64) error {
700743
return nil
701744
}
702745

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

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

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

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

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

0 commit comments

Comments
 (0)