Files
seaweedfs/weed/format/layout.go
T
Chris Lu 77a2b1b378 format: compute interior chunk cuts lazily
The cutter materialized every interior cut up front, so a tiny sidecar
declaring one enormous extent could allocate gigabytes of cut offsets
before any media byte arrived. Keep only the extent start offsets,
bounded by the extent count, and derive each cut arithmetically.
2026-08-10 00:31:16 -07:00

200 lines
5.6 KiB
Go

package format
import (
"bytes"
"encoding/binary"
"fmt"
"math"
"sort"
)
const (
layoutVersion = 1
// MaxExtentCount bounds decoded layouts; it also bounds the chunk count a
// layout can force on an entry.
MaxExtentCount = 1 << 20
// MaxPayloadBytes bounds the adapter payload carried in entry metadata.
MaxPayloadBytes = 16 << 20
)
// TotalSize returns the file size the layout describes.
func (l *Layout) TotalSize() int64 {
var total int64
for _, size := range l.ExtentSizes {
total += size
}
return total
}
// ExtentRange returns the byte range of extent i.
func (l *Layout) ExtentRange(i int) (offset, size int64, ok bool) {
if i < 0 || i >= len(l.ExtentSizes) {
return 0, 0, false
}
for _, extentSize := range l.ExtentSizes[:i] {
offset += extentSize
}
return offset, l.ExtentSizes[i], true
}
// Validate checks layout consistency. A negative fileSize skips the total size
// check.
func (l *Layout) Validate(fileSize int64) error {
if l.Format == "" {
return fmt.Errorf("layout has no format name")
}
if l.Align < 1 {
return fmt.Errorf("layout align %d is invalid", l.Align)
}
if len(l.ExtentSizes) == 0 {
return fmt.Errorf("layout has no extents")
}
if len(l.ExtentSizes) > MaxExtentCount {
return fmt.Errorf("layout has too many extents: %d", len(l.ExtentSizes))
}
if len(l.Payload) > MaxPayloadBytes {
return fmt.Errorf("layout payload is too large: %d bytes", len(l.Payload))
}
var total int64
for i, size := range l.ExtentSizes {
if size <= 0 {
return fmt.Errorf("extent %d has invalid size %d", i, size)
}
if size > math.MaxInt64-total {
return fmt.Errorf("extent %d overflows the file size", i)
}
total += size
}
if fileSize >= 0 && total != fileSize {
return fmt.Errorf("layout describes %d bytes but the file has %d", total, fileSize)
}
return nil
}
// Encode serializes the layout for the LayoutKey extended attribute.
func (l *Layout) Encode() ([]byte, error) {
if err := l.Validate(-1); err != nil {
return nil, err
}
out := []byte{layoutVersion}
out = binary.AppendUvarint(out, uint64(len(l.Format)))
out = append(out, l.Format...)
out = binary.AppendUvarint(out, uint64(l.Align))
out = binary.AppendUvarint(out, uint64(len(l.ExtentSizes)))
for _, size := range l.ExtentSizes {
out = binary.AppendUvarint(out, uint64(size))
}
out = binary.AppendUvarint(out, uint64(len(l.Payload)))
out = append(out, l.Payload...)
return out, nil
}
// DecodeLayout parses an encoded layout and validates it.
func DecodeLayout(data []byte) (*Layout, error) {
reader := bytes.NewReader(data)
version, err := reader.ReadByte()
if err != nil || version != layoutVersion {
return nil, fmt.Errorf("unsupported layout version")
}
name, err := readUvarintBytes(reader, 256)
if err != nil {
return nil, fmt.Errorf("read layout format: %w", err)
}
align, err := binary.ReadUvarint(reader)
if err != nil || align > math.MaxInt64 {
return nil, fmt.Errorf("read layout align: invalid")
}
count, err := binary.ReadUvarint(reader)
if err != nil || count > MaxExtentCount {
return nil, fmt.Errorf("read layout extent count: invalid")
}
sizes := make([]int64, count)
for i := range sizes {
size, err := binary.ReadUvarint(reader)
if err != nil || size > math.MaxInt64 {
return nil, fmt.Errorf("read extent %d size: invalid", i)
}
sizes[i] = int64(size)
}
payload, err := readUvarintBytes(reader, MaxPayloadBytes)
if err != nil {
return nil, fmt.Errorf("read layout payload: %w", err)
}
if reader.Len() != 0 {
return nil, fmt.Errorf("layout has %d trailing bytes", reader.Len())
}
layout := &Layout{Format: string(name), ExtentSizes: sizes, Align: int64(align), Payload: payload}
if err := layout.Validate(-1); err != nil {
return nil, err
}
return layout, nil
}
func readUvarintBytes(reader *bytes.Reader, limit uint64) ([]byte, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return nil, err
}
if length > limit || length > uint64(reader.Len()) {
return nil, fmt.Errorf("length %d is out of bounds", length)
}
if length == 0 {
return nil, nil
}
data := make([]byte, length)
if _, err := reader.Read(data); err != nil {
return nil, err
}
return data, nil
}
// Cutter yields upload chunk boundaries: every extent boundary, plus
// align-quantized cuts inside extents larger than maxChunkSize. A non-positive
// maxChunkSize keeps each extent in one chunk. Interior cuts are computed
// lazily, so memory stays bounded by the extent count no matter how large an
// untrusted layout declares its extents to be.
type Cutter struct {
starts []int64 // extent start offsets, ascending
total int64
quantum int64 // interior cut spacing; 0 = one chunk per extent
}
func (l *Layout) Cutter(maxChunkSize int64) *Cutter {
quantum := maxChunkSize
if quantum > 0 && l.Align > 1 {
quantum -= quantum % l.Align
if quantum <= 0 {
// An align larger than the chunk limit still cuts on whole atoms.
quantum = l.Align
}
}
starts := make([]int64, len(l.ExtentSizes))
var offset int64
for i, size := range l.ExtentSizes {
starts[i] = offset
offset += size
}
return &Cutter{starts: starts, total: offset, quantum: quantum}
}
// NextChunkSize returns the size of the chunk starting at offset, or 0 past
// the end. It satisfies the filer upload loop's ChunkBoundaries interface.
func (c *Cutter) NextChunkSize(offset int64) int64 {
if offset < 0 || offset >= c.total {
return 0
}
i := sort.Search(len(c.starts), func(i int) bool { return c.starts[i] > offset }) - 1
end := c.total
if i+1 < len(c.starts) {
end = c.starts[i+1]
}
remaining := end - offset
if c.quantum > 0 {
if step := c.quantum - (offset-c.starts[i])%c.quantum; step < remaining {
return step
}
}
return remaining
}