chunks

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 6, 2026 License: Apache-2.0 Imports: 19 Imported by: 1

Documentation

Overview

Package chunks is an append-only general purpose file-backed blob Store.

Goals

  • Simple detection and recovery from partial writes and unexpected shutdowns. This is facilitated by using an append-only write strategy, using explicit Checkpoint chunks to mark where Store.Flush was called, by aligning Chunks at 128 byte boundaries, and by checksum protections for everything.
  • Effective write batching with minimal disk write amplification. No series of Write calls is considered finished until they are followed with a Flush call, which writes a Checkpoint chunk and forces all pending writes to disk. If a Flush call returns without error, all writes that started before the Flush call will be on disk.
  • Resilience in the face of out-of-space and disk write errors. This is accomplished by growing the Store in 16 megabyte chunks whenever a requested write would result in the Store having less than 64 K free after all data was written to disk. Data is not copied into the Store until there is enough space free to hold it all. Expanding the Store always involves filling the newly-added space with zeros to catch potential write errors before actual data is written.
  • At-rest data safety All user data is encrypted at rest unless explicitly overridden using random keys that are reset every time the Store is cleared. Users can pass in an additional encryption key that will be mixed in with the random key for added security.
  • Fast iteration over Chunks in the store. Each and EachAfter are designed to perform only large linear reads for the common pattern of iterating over each Chunk and then doing something with its payload.

Anti-Goals

  • Sorting, searching, or really indexing of any kind. The only method we provide for reading a Chunk is Store.ReadAt, and the only method we provide for discovering Chunks is Store.Each and Store.EachAfter.
  • Space efficiency. This library never automatically releases disk space once it has been allocated. If you want to do that, you will need to call one of the Shrink() methods.
  • Deleting individual Chunks. Once a Chunk has been written and flushed, the only way to delete it is to Clear() the Store, which forgets everything and starts all new Writes at the beginning of the Store.

Layout

Each Chunk is written to memory at 128 byte aligned offsets to facilitate corruption detection and recovery. The layout is as follows:

[0:128]:    First chunk.  It establishes the generation number that all subsequent
            chunks in the Store must have to be considered valid.
[n:m]:     Any number of user specified chunks added to the Store by Write.
[m:m+128]: Checkpoint chunk.  Indicates that all previous chunks were Flushed to disk.
           If this Chunk and all previous Chunks are valid and have the same generation
           as the First chunk, they are considered to be valid.
[m+128:m+256]: If this chunk consists of all zeros, that indicates there is no more valid
               data in the Store.

On initial creation, the Store is grown to be 16 megabytes in size, the initial First record is written to establish the initial Generation (1) and then Flushed to disk. Subsequent Writes and Flushes add data to the Store and checkpoint it until one would leave the Store with less than 64K of writable space. At that point, the Store will be expanded by another 16 megabytes.

On Open of an already created Store, the Chunks will be read to verify that they are valid and of the same generation as the First chunk. Any uncommitted Chunks will be discarded, and the presence of any invalid data (Chunks from a different Generation or chunks with an invalid CRC) that is covered by a valid Checkpoint will indicate that the Store needs recovery.

Once you no longer need the data in the Store, you can Clear it, which will increment the current Generation to yield the new Generation, write,and then flush a new First chunk at the beginning of the Store.

If Open indicates that a Store needs recovery, you can copy the valid Chunks into a new Store using the Recover function.

Encryption

By default, all user data will be encrypted at rest using AES-128 in CTR mode. This encryption is intended to defeat casual searching for information using standard forensics tools by default. For added security, pass in an additional key.

Each blob of user data is encrypted using AES-128 using a key defined from the following sources: - 96 bytes of cryptographic random data recorded in the first chunk which establishes the generation of the chunks store. - The optional user-supplied key present in the Open options. - The offset and size that the Chunk is at in the file.

If the first chunk does not contain random data, then no chunks in the Store will be encrypted.

Caveats:

  • Data is not actually flushed to disk until Flush() is called. You should not expect ReadAt (either on the Store or on an unflushed Chunk) to return the data you wrote until after it has been Flushed to disk.

Index

Constants

View Source
const (
	// Alignment of Chunks in the file.  Chunks are aligned on 128 byte boundaries to facilitate file checking.
	Alignment = 128
	// SegmentPower is the number of bits to shift to mask out when rounding a segment size.
	SegmentPower = 24
	// SegmentSize is the size of a segment in the file.  The file is grown by this much when more space is required.
	SegmentSize = 1 << SegmentPower
	// ChunkHeaderSize is the size of the Chunk metadata.
	ChunkHeaderSize = 32
	// First is the magic number of the first Chunk in the Store.  It establishes the chunk generation and
	// holds the random key material all user data will be encrypted with.
	First = `1sT!`
	// Checkpoint is the magic number of a Chunk indicating that a sequence of writes has been committed to disk.
	Checkpoint = `cKpT`
)

Variables

View Source
var (

	// ErrBadTargetSize is returned when a read buffer for reading a Chunk is not the exact size
	// of the chunk as returned by chunk.Size().
	ErrBadTargetSize = errors.New("Target slice for Payload not correct size")
	// ErrBadFirstChunk is returned when the chunk at offset 0 is not a valid First chunk and the
	// chunk at offset 128 is not avalid Checkpoint chunk of the same generation as the First chunk.
	ErrBadFirstChunk = errors.New("First chunk in the Store is not a First chunk!")
	// ErrChunkTooShort is returned when the size of the Chunk on disk is smaller than Alignment bytes.
	ErrChunkTooShort = errors.New("Chunk too short")
	// ErrChunkTruncated is returned when the expected size of the Chunk on disk would cause it to be larger than
	// the remaining space on the Store.
	ErrChunkTruncated = errors.New("Chunk truncated")
	// ErrChunkCorrupt is returned if the checksum recorded in the Chunk header does not match the calculated
	// checksum.
	ErrChunkCorrupt = errors.New("Chunk corrupt")
	// ErrChunkMoved is returned when the Offset recorded in the Chunk header does not match the offset it was
	// found at in the Store.
	ErrChunkMoved = errors.New("Chunk at unexpected offset")
	// ErrGenMismatch is returned when the Generation of the chunk is not equal to the Generation of the
	// first chunk in the Store.
	ErrGenMismatch = errors.New("Chunk generation mismatch")
	// ErrLastCheckpointMismatch is returned when the offset of the last valid Checkpoint in the Store
	// is not equal to what it was when the Store was Closed.
	ErrLastCheckpointMismatch = errors.New("Last checkpoint offset changed")
	// ErrZeroChunk is returned when a chunk is all zeroes, which should never happen.
	ErrZeroChunk = errors.New("Zero chunk")
	// ErrNeedRecovery is returned when there is a run of invalid Chunks in between two valid Chunks
	// in the Store.  That indicates that there is corruption in the store, but that there is still
	// valid data that might be recoverable.
	ErrNeedRecovery = errors.New("Needs recovery or clear")
	// ErrReadOnly is returned when a write operation is attempted on a store that was opened with the
	// ReadOnly option set to true.
	ErrReadOnly = errors.New("Store is open read-only")
	// ErrStoreClosed is returned when any method other than Open is called on a Closed store.
	ErrStoreClosed = errors.New("Store is closed")
	// ErrStoreStopped is returned when there is an underlying write error.
	ErrStoreStopped = errors.New("Store is stopped due to a lower-level write error.")
	// ErrBadBatch is returned when the number of Batches to write is not equal to the amount of data in the batches.
	ErrBadBatch = errors.New("Bad batch")
	// ErrNotFlushed is returned if you try to read a Chunk past the end of the last Checkpoint
	ErrNotFlushed = errors.New("Read past flushed data")
)

Functions

func Recover

func Recover(into, from *Store) (err error)

Recover clears into, and then copies the valid Chunks in from. Invalid Chunks will be skipped.

Types

type Buffer added in v0.8.0

type Buffer interface {
	io.ReaderAt
	Size() int64
}

Buffer is anything that satisfies both the io.ReaderAt interface and that has a known Size.

func Buf added in v0.8.0

func Buf(bufs ...[]byte) Buffer

Buf takes any number of byte slices and makes a Buffer out of them.

type Chunk

type Chunk struct {
	// contains filtered or unexported fields
}

Chunk is an individual piece of data stored in the Store. It always starts at an Alignment offset, and has the following structure:

  • [0:4]: crc32.Castagnoli checksum of the rest of the chunk data. Used to detect data corruption. Everything after this is checked by the CRC.
  • [4:8]: Magic number. First (`1sT!`) and Checkpoint (`cKpT`) are used internally for the first chunk and the completed checkpoint marker, all others are available for users.
  • [8:16]: Chunk generation. Generation is established by the first chunk in the sequence, and resetting the Store increments it.
  • [16:24]: The Size of the chunk, including the chunk header.
  • [24:32]: The Offset in the store the Chunk was written to.
  • [32:] Chunk payload. Opaque to the chunk store.

func (*Chunk) Crc

func (c *Chunk) Crc() uint32

Crc is the Castagnoli CRC32 of the rest of the Chunk.

func (*Chunk) Equal added in v0.8.0

func (c *Chunk) Equal(other *Chunk) bool

Check to see if two Chunk objects are equal. Checks jsut the headers, not the payload, which is Good Enough for test purposes.

func (*Chunk) Gen

func (c *Chunk) Gen() uint64

Gen is the Generation of the Chunk. All valid Chunks in the Store have the same Gen as the First chunk in the Store.

func (*Chunk) Magic

func (c *Chunk) Magic() string

Magic number of the Chunk.

func (*Chunk) Offset

func (c *Chunk) Offset() int64

Offset in the Store the chunk was written to.

func (*Chunk) OnDiskSize added in v0.8.0

func (c *Chunk) OnDiskSize() int64

Size of the Chunk on disk, including the header and padding.

func (*Chunk) Payload

func (c *Chunk) Payload(buf []byte) error

Payload copies the data stored in the Chunk into target, which must be large enough to store all the data. If you only need to read part of the data, use ReadAt instead.

func (*Chunk) ReadAt added in v0.7.4

func (c *Chunk) ReadAt(p []byte, off int64) (int, error)

ReadAt implements io.ReaderAt. It can be used to read parts of the payload. If the Store is encrypted, use of multiple small ReadAt calls in the same Chunk will have significant overhead due to the Go standard library not being able to cache and reuse stream cipher handlers. If the Go cipher devs ever choose to expose their XORKeyStreamAt method, that situation will change.

func (*Chunk) Size

func (c *Chunk) Size() int64

Size of the Chunk payload.

func (*Chunk) TargetSlice added in v0.6.0

func (c *Chunk) TargetSlice(s []byte) []byte

TargetSlice is a utility that grows the passed-in slice until it can hold the entire payload.

func (*Chunk) Zero added in v0.8.0

func (c *Chunk) Zero() bool

type Options added in v0.6.0

type Options struct {
	// Key is an optional key used for encrypting data at rest,  If set, this key is
	// mixed in with the random per-generation key that data at rest is encrypted with as well.
	Key []byte
	// Plaintext determines whether written data should not be encrypted.  By default, all data will be
	// encrypted at rest.
	Plaintext bool
	// ReadOnly determines whether this Store can be written to.
	// If set, all write operations will fail.
	ReadOnly bool
	// Unsafe determines whether the Flush method will force sync to disk.
	// If set, it will not.
	Unsafe bool
	// Clear determines whether the Store will be cleared upon open.
	Clear bool
}

Options contain the options that the store should be opened with.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store stores Chunks.

func Open

func Open(at string, opts Options) (*Store, error)

Open the file and determine if it needs recovery.

func OpenScratch

func OpenScratch(at string, opts Options) (*Store, error)

OpenScratch opens a scratch chunk store. Any data in the Store will be cleared, and the Flush operation will not force data to disk.

func (*Store) CheckAt

func (s *Store) CheckAt(offset int64) error

CheckAt checks the Chunk at offset to ensure that it is well-formed and that the data has not been altered.

func (*Store) Clear

func (s *Store) Clear() error

Clear clears the Store by writing a new First chunk with a new Generation and then flushing that chunk.

func (*Store) Close

func (s *Store) Close() (err error)

Close closes a Store by unmapping the backing file then closing it. You must call Flush() beforehand to guarantee that all data has been written first.

func (*Store) CopyInto added in v0.8.0

func (s *Store) CopyInto(src []Chunk) (res []Chunk, err error)

CopyInto copies the Chunks in src into itself, The returned list of Chunks are members of s.

Chunk data is not guaranteed to be present on disk or accessible via chunk.Payload or chunk.ReadAt until the store has been flushed.

func (*Store) Each

func (s *Store) Each(thunk func(Chunk, Buffer) error) error

Each calls thunk once on each flushed Chunk. Calling Each concurrent with Write or Clear may result in undefined behaviour.

func (*Store) EachAfter added in v0.7.4

func (s *Store) EachAfter(start Chunk, thunk func(Chunk, Buffer) error) (err error)

EachAfter iterates over all the committed chunks after start. If start is an empty Chunk, iteration will start at the beginning of the Store.

Inside of

func (*Store) Expand

func (s *Store) Expand(target int64) error

Expand grows the Store until it is larger than target.

func (*Store) Flush

func (s *Store) Flush() error

Flush writes modified data back to the disk. Any data not flushed will be lost in a crash or when the store is closed. Flush will wait on all pending writes to finish.

func (*Store) Open

func (s *Store) Open(readOnly bool) error

Open a previously-closed Store. This assumes that the Store had been flushed and closed without error. It will error out if the state of the Store has changed.

func (*Store) OpenAndClear

func (s *Store) OpenAndClear() (err error)

OpenAndClear opens a new Store and clears it out all in one operation. This should be used when you explicitly want to ignore the data in a Store.

func (*Store) ReadAt

func (s *Store) ReadAt(gen uint64, offset int64) (Chunk, error)

ReadAt reads a Chunk starting at Offset. It will panic if offset does not point at a valid Chunk. The returned Chunk is only valid until Write needs to remap the Store due to needing more space on disk.

func (*Store) ResizeTo added in v0.7.3

func (s *Store) ResizeTo(target int64) error

ResizeTo will resize the Store to as close to the target size it can get without losing data.

func (*Store) Shrink

func (s *Store) Shrink() error

Shrink will discard extra segments past the end of the write barrier at the end of the Store. It is equivalent to calling s.ShrinkTo(0)

func (*Store) ShrinkTo added in v0.7.3

func (s *Store) ShrinkTo(target int64) error

ShrinkTo will discard extra segments until either the size equals target rounded to the nearest segment boundary.

func (*Store) Size

func (s *Store) Size() int64

Size returns the size of the Store.

func (*Store) StreamInto added in v0.8.0

func (s *Store) StreamInto(target *Store, after Chunk) (next Chunk, err error)

StreamInto streams chunks from s into target, starting at the chunk immediatly following after and ending with the last Chunk that has been flushed to disk. Both the after Chunk and the next Chunk are members of s, not target. StreamInto will block anything on Target that waits on all pending writes.

Chunk data is not guaranteed to be present on disk or accessible via chunk.Payload or chunk.ReadAt until the target store has been flushed.

func (*Store) Write

func (s *Store) Write(magic string, src Buffer) (Chunk, error)

Write data as a new Chunk into the Store, expanding the Store as needed. You must Flush the Store to ensure data is written to disk and checkpointed. Failure to call Flush will result in the data accumulated since the last Flush being discarded the next time the Store is opened.

Write returns the Chunk that was written. The Chunk will remain valid until the store is Cleared. Chunk data is not guaranteed to be present on disk or accessible via chunk.Payload or chunk.ReadAt until the store has been flushed.

func (*Store) WriteBatch added in v0.8.0

func (s *Store) WriteBatch(magic string, res []Chunk, chunkBufs []Buffer) error

WriteBatch should be used when you have a bunch of individual Chunks using the same magic number to write.

magic is the magic string that all the Chunks will have on disk. res is a slice of Chunks that will be populated with the Chunks that were written to disk. It must be either nil or the same length as chunkBufs. chunkBufs contains the buffers that each Chunk should be populated with.

If an error occurs during the write process, a non-nil error will be returned, and you should ignore anything in the res slice.

The following code is equivalent in effect:

chunks := make([]Chunk,500)
var err error
for i := range chunks {
    chunks[i], err = s.Write("StUf",[][]byte{[]byte("foo")})
}

chunks := make([]Chunk,500)
payloads := make([][][]byte, 500)
foo = []byte("foo")
for i := range payloads {
    payloads[i] = [][]byte{foo}
}
err = s.WriteBatch("StUf",chunks,payloads)

WriteBatch is much more performant than Write in a loop when flushing a bunch of small Chunks.

Chunk data is not guaranteed to be present on disk or accessible via chunk.Payload or chunk.ReadAt until the store has been flushed.

type Thunk added in v0.8.0

type Thunk func(Chunk, Buffer) error

Thunk is the function type that Each and EachAfter take to let calling code do something with the Chunks being iterated over. You can copy the Chunk outside of the Thunk, and it will work normally with the usual Chunk lifecycle rules. If you need to do something with the payload the Chunk carries, use the ReadAt method on the Buffer that gets passed in to avoid excess disk IO.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL