Documentation
¶
Index ¶
- Constants
- Variables
- func DefaultDiskReserve(maxSizeBytes int64) int64
- func DirectoryBytes(walDir string) (int64, error)
- func DiskPressureLevels(reserve, maxSizeBytes int64, total uint64) (on, offPoint int64, clamped bool)
- func InstancesOnDisk(dir, skip string, logger zerolog.Logger) ([]uint64, error)
- func MeasurementName(m interface{}) (string, bool)
- func ParseEnvelope(payload []byte, defaultDB string) (database string, msgpackData []byte)
- func ParseRecoveryRowIdentity(identity string) (parent string, start, end int, ok bool)
- func PrepareReserveAtBoot(walDir string, logger zerolog.Logger) bool
- func ReserveFilePath(walDir string) string
- func WithReplayCoverage(ctx context.Context, rc ReplayCoverage) context.Context
- type ColumnarEntry
- type ColumnarRecoveryCallback
- type ColumnarRecoveryCallbackWithTagColumns
- type Coverage
- type CoverageHold
- type CoverageSpan
- type DiskMonitor
- type DiskMonitorConfig
- type Entry
- type PreparedAppend
- type Reader
- type Recovery
- func (r *Recovery) CleanupOldWALs(maxAge time.Duration) (int, int64, error)
- func (r *Recovery) ListWALFiles() (active []string, recovered []string, err error)
- func (r *Recovery) Recover(ctx context.Context, callback RecoveryCallback) (*RecoveryStats, error)
- func (r *Recovery) RecoverWithOptions(ctx context.Context, callback RecoveryCallback, opts *RecoveryOptions) (*RecoveryStats, error)
- type RecoveryCallback
- type RecoveryOptions
- type RecoveryStats
- type ReplayCoverage
- type ReplicationEntry
- type ReplicationHook
- type RetainedFile
- type SyncMode
- type TrackedRecoveryCallback
- type Writer
- func (w *Writer) ActiveFileBytes() int64
- func (w *Writer) AdoptRetained(files []RetainedFile)
- func (w *Writer) Append(records []map[string]interface{}) error
- func (w *Writer) AppendRaw(payload []byte) error
- func (w *Writer) AppendRawTracked(payload []byte) ([]string, error)
- func (w *Writer) AppendRawWithMeta(database string, payload []byte) error
- func (w *Writer) AppendRawWithMetaTracked(database string, payload []byte) ([]string, error)
- func (w *Writer) AppendTracked(records []map[string]interface{}) ([]string, error)
- func (w *Writer) Close() error
- func (w *Writer) CoverageLost(key string, cseq uint64) bool
- func (w *Writer) CurrentCheckpointHashes() ([]string, error)
- func (w *Writer) CurrentFile() string
- func (w *Writer) DiskFullObserved() bool
- func (w *Writer) ForgetTracked(hashes []string)
- func (w *Writer) HeldFiles() int
- func (w *Writer) MarkFlushed(hashes []string) error
- func (w *Writer) MarkFlushedContext(ctx context.Context, hashes []string) error
- func (w *Writer) MarkFlushedRetained(hashes []string) error
- func (w *Writer) MinUnflushedSequence() uint64
- func (w *Writer) PendingUnflushedCount() int
- func (w *Writer) PrepareCovered(database, key string, payload []byte) (*PreparedAppend, error)
- func (w *Writer) PurgeAll() (int, error)
- func (w *Writer) PurgeFlushed(minUnflushedSeq uint64) (int, error)
- func (w *Writer) PurgeHint() <-chan struct{}
- func (w *Writer) PurgeInactive() (int, error)
- func (w *Writer) Quarantined(path, quarantinedAs string) int
- func (w *Writer) ReleaseRegistered(hashes []string)
- func (w *Writer) ReleaseRetained() (int, error)
- func (w *Writer) RetainedFiles() int
- func (w *Writer) Rotate() error
- func (w *Writer) RotateContext(ctx context.Context) error
- func (w *Writer) SetCoverageHold(hold func() CoverageHold)
- func (w *Writer) SetReplicationHook(hook ReplicationHook)
- func (w *Writer) Stats() map[string]interface{}
- func (w *Writer) TrackedInstance() uint64
- type WriterConfig
Constants ¶
const ( // DefaultDiskReserveMin is the least default reserve: below this much // free space on the WAL volume, live writes are refused. DefaultDiskReserveMin = int64(512) << 20 // DiskPressureMinOn is the least time the gate stays on once it trips, so // clients and MQTT subscriptions are not flapped by every purge. DiskPressureMinOn = 5 * time.Second )
WAL volume disk pressure (#676). The writer PVC holds the WAL, Raft, the registration journal, arc.db, compaction temp and the standby store, so the constraint is the volume's free space, not the WAL's bytes. A WAL write that fails for space loses an entry the client was already told was accepted; refusing the write up front (503, retryable) loses nothing.
const ( // ReserveFileName is the reserve's name in the WAL directory. It must // never end in ".wal": every WAL listing, the purge and recovery match // that suffix, and the reserve is not a WAL file. ReserveFileName = ".arc-wal-reserve" // ReserveFileSize is how much the reserve holds. ReserveFileSize = int64(64) << 20 // ReserveBootMinFree: a start that finds less free space than this on the // WAL volume removes the reserve before anything writes there. ReserveBootMinFree = uint64(32) << 20 )
The WAL reserve file (#676): space held on the WAL volume so a node whose volume filled up can still start. A full volume made the first WAL header write fail with ENOSPC, and the pod crash-looped; releasing the reserve at boot gives the start, and recovery, room to free space.
const ( WALChecksumCRC32 = 0x01 // CRC32 checksum type // Entry format: [Length: 4 bytes] [Timestamp: 8 bytes] [Checksum: 4 bytes] [Payload: N bytes] WALEntryHeaderSize = 16 WALFileHeaderSize = 7 // Magic(4) + Version(2) + ChecksumType(1) // MaxWALPayloadSize is the maximum allowed payload size for a single WAL entry. // This limit prevents integer overflow during buffer allocation (CWE-190) and // aligns with the replication protocol limit (100MB). MaxWALPayloadSize = 100 * 1024 * 1024 // 100MB // WALEnvelopeMarker is the first byte of an enveloped WAL payload. // Enveloped format: [0x01][2-byte db name length][db name][original msgpack] // Since msgpack maps/arrays always start with bytes >= 0x80, 0x01 is unambiguous. WALEnvelopeMarker = 0x01 WALCheckpointMarker = 0x02 WALTrackedMarker = 0x03 // WALCoveredMarker is a tracked entry that also carries the ingest batch's // coverage id in a trailer (#1207); see coverage.go for the layout. WALCoveredMarker = 0x04 )
const ( // MaxCoverageKeyLen bounds the buffer key a covered entry carries; a // longer key is written as a plain tracked entry, without coverage. MaxCoverageKeyLen = 1 << 12 )
Covered entry layout (marker 0x04):
[0x04][instance u64][walseq u64][logical payload][key][keyLen u16][chunk u16][chunks u16][cseq u64]
The logical payload is what a 0x03 entry carries (an enveloped msgpack value), so everything that reads a tracked entry reads a covered one the same way once the header and trailer are stripped, and the identity token is the same "instance+walseq" form — checkpoints, the purge floor and recovery identities are unchanged.
CSeq is last so the CRC of everything before it can be computed outside the shard lock: Commit only writes 8 bytes and folds them into the CRC.
Variables ¶
var ( WALMagic = []byte{'A', 'R', 'C', 'W'} // Magic bytes WALVersion = uint16(0x0001) // Version 1 )
WAL file format constants
var ErrCoverageTrailer = errors.New("WAL covered entry has a malformed coverage trailer")
ErrCoverageTrailer reports a 0x04 entry whose trailer does not fit.
var ErrPayloadTooLarge = errors.New("WAL payload exceeds maximum allowed size")
ErrPayloadTooLarge indicates the payload exceeds MaxWALPayloadSize.
var ErrWALDropped = errors.New("WAL entry dropped: async buffer full")
ErrWALDropped is returned by Append/AppendRaw/AppendRawWithMeta when the async entry channel is full and the entry is dropped. Previous behavior returned nil and silently incremented DroppedEntries — callers logging "data preserved in WAL for recovery" downstream were reporting durability they did not actually have. Returning a sentinel lets callers (the ingestion buffer in particular) increment their own error counters and surface accurate operator-facing messages. Use errors.Is to detect.
Functions ¶
func DefaultDiskReserve ¶
DefaultDiskReserve is the reserve wal.disk_min_free_mb = 0 selects: max(512 MiB, two WAL files).
func DirectoryBytes ¶
DirectoryBytes reports the bytes occupied by every file in the WAL directory except the reserve file (ReserveFileName, #676).
Every file but the reserve, not a suffix list. The names that are not `*.wal` — `.wal.recovery` attempt sidecars, `.wal-recovery-*` temporaries leaked by a crash, legacy `.wal.recovered` files — are exactly what accumulates when recovery is failing, which is the state this gauge's capacity alert exists for. A suffix list also has to be revisited every time the WAL grows a new artifact name, and was not.
It therefore measures DIRECTORY occupancy. On a volume dedicated to the WAL, which is how `wal.directory` is meant to be pointed, that is the figure an operator wants; aimed at a shared path it will include whatever else is there.
The reserve file is left out: it is a fixed 64 MiB held for a start on a full volume, not WAL growth, and wal.max_directory_mb is enforced on this figure — counting the reserve would spend the cap on the space set aside for the case the cap exists to prevent.
func DiskPressureLevels ¶
func DiskPressureLevels(reserve, maxSizeBytes int64, total uint64) (on, offPoint int64, clamped bool)
DiskPressureLevels returns the effective free-space levels of the gate on a volume of total bytes: it turns on below reserve and off at or above offPoint. reserve <= 0 selects DefaultDiskReserve. The reserve and the band above it (two WAL files) are each clamped to a fifth of the volume; clamped reports that the reserve was. total 0 (unknown) clamps nothing.
The standby store's own free-space reserve on the same volume (#1266) must stay above offPoint, or a standby fills the volume to where its promotion starts under disk pressure.
func InstancesOnDisk ¶
InstancesOnDisk returns the instances of the intact tracked and covered entries of every WAL file in dir (*.wal and quarantined *.failed) except skip (the active file, whose instance the caller knows), sorted. A file's answer is cached by its size and modification time. A file that is gone, or not a WAL file at all (a damaged quarantined one), holds nothing; only an I/O error fails the call.
func MeasurementName ¶
MeasurementName reads the "m" of a columnar payload the way ingest does (ingest.extractMeasurement): a string as is, an integer as "measurement_<n>". The WAL keeps the payload the client sent, so a reader that accepted only a string failed every entry written with an integer measurement (#1225).
func ParseEnvelope ¶
ParseEnvelope extracts the database name and msgpack payload from a WAL entry. If the payload uses the envelope format [0x01][2-byte dbLen][dbName]msgpack, it returns the database name and the inner msgpack bytes. Otherwise, it returns defaultDB and the original payload unchanged.
func ParseRecoveryRowIdentity ¶
ParseRecoveryRowIdentity recognizes a checkpoint for the half-open row range [start,end) of one tracked WAL entry. The parent is a writer-instance/sequence identity, never a legacy content hash: identical legacy payloads can represent distinct writes. The encoding is independent of the configured batch size.
func PrepareReserveAtBoot ¶
PrepareReserveAtBoot runs before anything writes to the WAL volume at start (#676). It keeps an existing reserve file, and removes it when the volume has less than ReserveBootMinFree free, logging an Error: the start then has the reserve's room for the WAL header, the registration journal and SQLite. It never creates the reserve (the disk pressure monitor does, once there is room) and never refuses a start: a statfs error or a failed remove is logged and the start goes on. It reports whether it released.
func ReserveFilePath ¶
ReserveFilePath is the reserve file's path in walDir.
func WithReplayCoverage ¶
func WithReplayCoverage(ctx context.Context, rc ReplayCoverage) context.Context
WithReplayCoverage attaches rc to the context recovery passes to its replay callbacks.
Types ¶
type ColumnarEntry ¶
type ColumnarEntry struct {
Database string // From envelope metadata (empty = "default")
Measurement string
Columns map[string][]interface{}
TagColumns []string
}
ColumnarEntry represents a columnar WAL entry written via the zero-copy path
type ColumnarRecoveryCallback ¶
type ColumnarRecoveryCallback func(ctx context.Context, database, measurement string, columns map[string][]interface{}, walIdentity string) error
ColumnarRecoveryCallback is called for columnar WAL entries during recovery.
walIdentity is the identity of the entry being replayed, so the re-buffered batch can inherit it and have its eventual flush checkpoint the ORIGINAL entry — which is what stops a later pass replaying it again. It is empty for an entry that carries no tracked identity. database may be empty if the WAL entry predates the envelope format (defaults to "default").
type ColumnarRecoveryCallbackWithTagColumns ¶
type ColumnarRecoveryCallbackWithTagColumns func(ctx context.Context, database, measurement string, columns map[string][]interface{}, walIdentity string, tagColumns []string) error
ColumnarRecoveryCallbackWithTagColumns also receives tag metadata preserved in raw MessagePack entries. When set, it takes precedence over ColumnarCallback.
type Coverage ¶
Coverage identifies the ingest batch a WAL entry belongs to (#1207).
The ingest buffer mints CSeq per (writer instance, buffer key) under the shard lock at the moment the batch is committed, so the batches of one key commit in CSeq order and a flush of that key covers one contiguous range. A batch too large for one entry is split into Chunks entries that share the CSeq; Chunk is this entry's index.
type CoverageHold ¶
CoverageHold reports whether any batch of instance and buffer key with a coverage sequence in [lo, hi] is still waiting for its file to register. Implementations are immutable snapshots, safe for concurrent use.
type CoverageSpan ¶
CoverageSpan is a run of coverage sequences of one instance and key.
type DiskMonitor ¶
type DiskMonitor struct {
// contains filtered or unexported fields
}
DiskMonitor decides WAL volume disk pressure (#676). A goroutine of its own samples the volume every second and at once on a rotation or a disk-full write: the WAL maintenance goroutine can block for minutes in recovery. Between samples the gate is exact: every byte admitted to the WAL queue since the sample is charged against the free space it saw, so a burst cannot run past the reserve before the next sample.
It trips when free - bytes since the sample - reservations < reserve, when the directory cap is set and directory bytes + bytes since + reservations > cap, or on any write that failed for space (DiskFullObserved only trips). Requests reserve their size at admission (Reserve), so a large request, or several at once, is refused before it can cross the reserve. It clears only at or above the off-point (reserve + two WAL files, both clamped) and after DiskPressureMinOn: a trip is never delayed by a minimum time in the off state, because the trip is what keeps the volume from filling.
func NewDiskMonitor ¶
func NewDiskMonitor(w *Writer, cfg DiskMonitorConfig) *DiskMonitor
NewDiskMonitor builds the disk pressure monitor of w. Start it before serving writes.
func (*DiskMonitor) Close ¶
func (m *DiskMonitor) Close() error
Close stops the sampler goroutine. The gate keeps its last state.
func (*DiskMonitor) Levels ¶
func (m *DiskMonitor) Levels() (reserve, offPoint int64)
Levels returns the effective reserve (the gate is on below it) and off-point (it clears at or above it), in free bytes on the WAL volume, as of the last sample.
func (*DiskMonitor) OnChange ¶
func (m *DiskMonitor) OnChange(fn func(on bool))
OnChange registers fn for every change of the gate, called from the sampler goroutine; fn(true) is called at once when the gate is already on. fn must not block for long and must not call OnChange.
func (*DiskMonitor) Pressure ¶
func (m *DiskMonitor) Pressure() bool
Pressure reports whether live writes are refused now. The hot path: an atomic flag, one snapshot load and two counter loads. Writes that would cross a budget trip the gate here, at once, and wake the sampler.
func (*DiskMonitor) Release ¶
func (m *DiskMonitor) Release(n int64)
Release returns n reserved bytes: queued since (now charged as queued), or never written.
func (*DiskMonitor) Reserve ¶
func (m *DiskMonitor) Reserve(n int64) bool
Reserve admits a request of about n WAL bytes (#676): it fits when the bytes queued since the sample, every reservation not yet queued and n stay within both budgets; it is then charged until Release. A request that does not fit is refused without turning the gate on, unless what is already charged is over a budget. Concurrent reservations see each other.
func (*DiskMonitor) Start ¶
func (m *DiskMonitor) Start()
Start takes the first sample, so the gate is armed before the first write, and starts the sampler goroutine. Idempotent.
type DiskMonitorConfig ¶
type DiskMonitorConfig struct {
// Dir is the WAL directory; its volume is the one sampled.
Dir string
// ReserveBytes is wal.disk_min_free_mb in bytes; 0 = DefaultDiskReserve.
ReserveBytes int64
// MaxSizeBytes is wal.max_size_mb in bytes (the hysteresis band is two).
MaxSizeBytes int64
// MaxDirectoryBytes is wal.max_directory_mb in bytes; 0 = no cap. For a
// volume whose statfs does not describe the limit that applies (an
// emptyDir with a sizeLimit reports the node's disk).
MaxDirectoryBytes int64
// ReserveFile keeps the reserve file (ReserveFileName) in Dir.
ReserveFile bool
Logger zerolog.Logger
// ActiveBytes and Rotate let the gate clear when the active WAL file is
// what holds it on (NewDiskMonitor sets both from the writer): while
// writes are refused nothing rotates that file, so its bytes, flushed or
// not, would never be purged. Nil: never rotate.
ActiveBytes func() int64
Rotate func() error
}
DiskMonitorConfig configures a DiskMonitor.
type Entry ¶
type Entry struct {
TimestampUS uint64 // Microseconds since epoch
PayloadHash string // Entry identity: the writer-assigned "instanceSeq" (32 hex) for a tracked entry, else a SHA-256 of the logical payload (64 hex)
CheckpointHashes []string // Flush checkpoint identities, when present
Records []map[string]interface{} // Row format (from Append path)
ColumnarData *ColumnarEntry // Columnar format (from AppendRaw path)
Coverage *Coverage // Ingest batch coverage, for a covered (0x04) entry (#1207)
// DecodeErr is set for an entry whose bytes are intact (checksum valid)
// but whose payload this build cannot decode. Its identity is still known,
// so recovery can tell whether it was already flushed (#1225).
DecodeErr error
}
Entry represents a single WAL entry
func DecodeLogical ¶
DecodeLogical decodes a logical WAL payload ([0x01][dbLen][db]msgpack or bare msgpack): row form into Records, columnar form into ColumnarData, and anything else into an entry carrying DecodeErr. The standby store keeps these payloads as the origin wrote them (#1207).
type PreparedAppend ¶
type PreparedAppend struct {
// contains filtered or unexported fields
}
PreparedAppend is a covered WAL append built outside the ingest shard lock and committed under it (#1207). Prepare allocates the WAL sequences — they are pending in the purge floor from that moment — and assembles every entry except the coverage sequence. Commit stamps the sequence, fires the replication hook and enqueues. Abandon releases the sequences: nothing was written, so a write refused to the client never reaches the WAL or a replica.
Exactly one of Commit or Abandon must be called; Abandon after Commit is a no-op, so a deferred Abandon is the intended pattern.
func (*PreparedAppend) Abandon ¶
func (p *PreparedAppend) Abandon()
Abandon releases the sequences of a prepared append that was never committed. Safe to defer: a no-op after Commit.
func (*PreparedAppend) Chunks ¶
func (p *PreparedAppend) Chunks() int
Chunks is the number of WAL entries the batch was split into.
func (*PreparedAppend) Commit ¶
func (p *PreparedAppend) Commit(cseq uint64) ([]string, error)
Commit stamps cseq, fires the replication hook and enqueues every entry in order. It is cheap — no allocation proportional to the payload — because it runs under the ingest shard lock.
On a full queue it stops at the first dropped entry and returns ErrWALDropped with the tokens of the entries already enqueued: those are in the WAL and must be checkpointed by the flush like any other, while the dropped and unsent ones are released here. The batch itself is committed to the buffer either way; the WAL is for durability, not correctness.
func (*PreparedAppend) Instance ¶
func (p *PreparedAppend) Instance() uint64
Instance is the writer instance the prepared entries belong to.
type Reader ¶
type Reader struct {
// Metrics
TotalEntries int64
TotalBytes int64
CorruptedEntries int64
// TruncatedEntries counts incomplete entries at EOF after an interrupted
// append. These are not corrupt complete entries and cannot be replayed.
TruncatedEntries int64
// UndecodableEntries counts intact entries whose payload this build
// cannot decode. They are returned with DecodeErr set, and are included
// in TotalEntries.
UndecodableEntries int64
// contains filtered or unexported fields
}
Reader reads WAL files for recovery operations
func (*Reader) ReadCheckpointHashes ¶
ReadCheckpointHashes scans a WAL file for flush checkpoints without materializing or decoding ordinary data entries. Non-checkpoint payloads are skipped by seeking over their validated on-disk lengths; the recovery pass still reads and checksum-validates entries before replaying them.
func (*Reader) ReadTrackedIdentities ¶
ReadTrackedIdentities returns the identity tokens of the intact tracked (0x03) and covered (0x04) entries of a WAL file, without decoding their payloads (#1225). A damaged entry is counted in CorruptedEntries and skipped; a torn tail ends the scan.
func (*Reader) ScanCovered ¶
ScanCovered streams the intact covered (0x04) entries of the file whose coverage match accepts, in file order; only those payloads are decoded (#1207 S4b1c2b: an origin republishing parked batches reads them back). A damaged entry is counted and skipped; a torn tail ends the scan. An error from fn stops it and is returned.
type Recovery ¶
type Recovery struct {
// contains filtered or unexported fields
}
Recovery manages WAL recovery operations
func NewRecovery ¶
NewRecovery creates a new WAL recovery manager
func (*Recovery) CleanupOldWALs ¶
CleanupOldWALs removes legacy .recovered WAL files older than the specified age. Note: As of the current implementation, WAL files are deleted immediately after successful recovery, so this function is primarily for cleaning up legacy files from previous versions that renamed files to .recovered instead of deleting them.
func (*Recovery) ListWALFiles ¶
ListWALFiles lists all WAL files in the directory. Returns active (pending) WAL files and legacy .recovered files. Note: As of the current implementation, WAL files are deleted immediately after successful recovery, so the recovered list will typically be empty or contain only legacy files from previous versions.
func (*Recovery) Recover ¶
func (r *Recovery) Recover(ctx context.Context, callback RecoveryCallback) (*RecoveryStats, error)
Recover scans the WAL directory and replays all WAL files
func (*Recovery) RecoverWithOptions ¶
func (r *Recovery) RecoverWithOptions(ctx context.Context, callback RecoveryCallback, opts *RecoveryOptions) (*RecoveryStats, error)
RecoverWithOptions scans the WAL directory and replays WAL files with configurable options
type RecoveryCallback ¶
RecoveryCallback is called for each batch of records during recovery (row format)
type RecoveryOptions ¶
type RecoveryOptions struct {
// SkipActiveFile is the path to the currently active WAL file that should be skipped
// during periodic recovery (to avoid reading a file being actively written)
SkipActiveFile string
// AdditionalCheckpointHashes contains checkpoints read safely from the
// active file, which is intentionally excluded from recovery scans.
AdditionalCheckpointHashes []string
// BatchSize limits how many records are replayed per callback invocation
// This provides backpressure during mass recovery after prolonged outages
// 0 means no limit (all records in an entry replayed at once)
BatchSize int
// ColumnarCallback handles columnar WAL entries from the zero-copy write path
ColumnarCallback ColumnarRecoveryCallback
// ColumnarCallbackWithTagColumns preserves raw MessagePack tag metadata.
ColumnarCallbackWithTagColumns ColumnarRecoveryCallbackWithTagColumns
// TrackedRowCallback handles row-format entries in batches bounded by
// BatchSize. Legacy content-hash entries inherit no checkpoint identity.
TrackedRowCallback TrackedRecoveryCallback
// ValidateTrackedRows validates the complete row entry before any of its
// batches are submitted. This preserves whole-entry validation when batching.
ValidateTrackedRows func([]map[string]interface{}) error
// CheckpointRecovered checkpoints original parent identities after all of
// their row ranges are durable and before deleting their files. Production
// callers use Writer.MarkFlushed to release the original pending sequence.
// A failure retains the files for retry. Nil is suitable for synchronous
// callbacks with no live writer pending identities to release.
CheckpointRecovered func(context.Context, []string) error
// MinFileAge, when > 0, skips WAL files modified more recently than this.
// Defense against the #594 class beyond the SkipActiveFile name match:
// the periodic recovery reads CurrentFile() and then scans — a rotation
// landing between those instants would put the NEW active (header-only)
// file in the scan, and deleting it re-creates the unlinked-inode data
// loss. Production call sites pass a few seconds; a genuinely
// recoverable young file is picked up by the next pass. Zero disables
// the guard (tests recover freshly-written files).
MinFileAge time.Duration
// MinFileAgeExemptFiles are known-closed files that may be recovered even
// when their modification time is recent. The periodic recovery path uses
// this for the file that Writer.Rotate just closed; MinFileAge still
// protects files that rotate while recovery is scanning.
MinFileAgeExemptFiles []string
// BeforeDelete is an epoch barrier that must make every replayed entry in a
// batch durable before the corresponding WAL files are removed. The
// callback runs after BarrierBatchFiles files or BarrierBatchRows records
// have replayed, whichever threshold is reached first. A nil callback means
// the recovery callback itself provides synchronous durability.
// A batch containing only entries covered by durable checkpoints needs no
// flush; an earlier replay still pending in that batch must be fenced.
BeforeDelete func(context.Context) error
// BarrierBatchFiles bounds how many successfully replayed files are covered
// by one BeforeDelete call. Defaults to 16 when BeforeDelete is configured.
BarrierBatchFiles int
// BarrierBatchRows optionally triggers BeforeDelete after this many rows
// have replayed, allowing callers to align barriers with max_buffer_size.
BarrierBatchRows int
// MaxReplayFailures is the number of failed replay passes before a poison
// WAL file is quarantined as .wal.failed. Defaults to 3.
MaxReplayFailures int
// Held returns the registration hold (#1207 S4c). It is consulted after
// each barrier, so the batches this pass just replayed are in it. Nil
// holds nothing.
Held func() CoverageHold
// OnQuarantine is called after a WAL file is moved to quarantinedAs, so
// the writer can stop waiting for the entries in it (#1225).
OnQuarantine func(path, quarantinedAs string)
}
RecoveryOptions configures WAL recovery behavior
type RecoveryStats ¶
type RecoveryStats struct {
RecoveredFiles int
RecoveredBatches int
RecoveredEntries int
// CorruptedEntries counts entries this pass could not replay: damaged
// ones, ones whose replay failed, and intact ones that cannot be decoded
// and were not flushed. An undecodable entry already flushed is skipped
// like any other flushed entry, and not counted.
CorruptedEntries int
SkippedFiles int
KeptFiles int
BarrierFailures int
QuarantinedFiles int
// PartialRowEntries counts the WAL files this pass left on disk for which at
// least one row RANGE of a parent entry was handed off. Their checkpoints
// land on the next flush, and a binary without row-range support treats
// those identities as inert and replays the parent entry in full — so this
// is the state the restart compatibility guidance is about, made checkable.
//
// It describes what THIS pass observed, so it is a floor on the directory's
// residue rather than a census: a file whose replay failed before any range
// was handed off contributes nothing, and so does the out-of-range
// checkpoint case, which breaks before the ranges are computed even though
// a bad coverage map is itself evidence that ranges exist.
PartialRowEntries int
RecoveryDuration time.Duration
// Retained lists the files this pass replayed (or found checkpointed) but
// kept because a batch in them is still waiting for its file to register
// (#1207 S4c), and every file after the first such one: a later file can
// hold the only checkpoint for an earlier one's entries. Not failures: they
// are not counted in KeptFiles; the writer deletes them through
// ReleaseRetained once nothing in them is held.
Retained []RetainedFile
}
RecoveryStats holds statistics about WAL recovery
type ReplayCoverage ¶
ReplayCoverage travels with a replayed covered entry from recovery to the ingest buffer, so the file it is flushed into registers with the batch's original coverage (#1207). RowTotal is the entry's row count for a row-format entry, which recovery may replay in row ranges; zero for a columnar entry, which is always replayed whole.
func ReplayCoverageFrom ¶
func ReplayCoverageFrom(ctx context.Context) (ReplayCoverage, bool)
ReplayCoverageFrom returns the coverage of the entry being replayed, if any.
type ReplicationEntry ¶
type ReplicationEntry struct {
// TimestampUS is the entry timestamp in microseconds since epoch
TimestampUS uint64
// Payload is the raw msgpack data
Payload []byte
// Coverage identifies the ingest batch the entry belongs to, for entries
// appended through PrepareCovered (#1207). Nil otherwise.
Coverage *Coverage
}
ReplicationEntry is a WAL entry as the replication hook receives it: the origin stream (#1207) sends it to standby writers.
type ReplicationHook ¶
type ReplicationHook func(entry *ReplicationEntry)
ReplicationHook is called for each WAL entry before it's written locally: the origin stream's, on a primary writer with WAL coverage (#1207).
type RetainedFile ¶
type RetainedFile struct {
Path string
Coverage []CoverageSpan
}
RetainedFile is a WAL file recovery kept for registrations still pending, or because an earlier file of the same pass was kept for them. Coverage is the covered batches its entries belong to (possibly none held, for a file kept for its position).
type TrackedRecoveryCallback ¶
type TrackedRecoveryCallback func(ctx context.Context, records []map[string]interface{}, walIdentity string) error
TrackedRecoveryCallback receives an entry identity, or a recovery row-range identity when BatchSize splits a tracked entry. A flush must checkpoint exactly the supplied identity, never the parent of a partially flushed entry.
type Writer ¶
type Writer struct {
// Metrics (atomic for lock-free reads)
TotalEntries int64
TotalBytes int64
TotalSyncs int64
FailedSyncs int64 // Syncs that returned an error
TotalRotations int64
DroppedEntries int64 // Entries dropped due to full buffer
FailedWrites int64 // Entries the writer loop could not write (lost)
DiskFullWrites int64 // FailedWrites whose error was ENOSPC or EDQUOT
// RecoveredWrites counts failed entry writes the retry on a new WAL file
// recovered: nothing was lost, but the WAL file or its device failed.
RecoveredWrites int64
// contains filtered or unexported fields
}
Writer is a Write-Ahead Log writer with configurable durability
func NewWriter ¶
func NewWriter(cfg *WriterConfig) (*Writer, error)
NewWriter creates a new WAL writer
func (*Writer) ActiveFileBytes ¶
ActiveFileBytes is the size of the WAL file being written, header included (#676).
func (*Writer) AdoptRetained ¶
func (w *Writer) AdoptRetained(files []RetainedFile)
AdoptRetained takes over the files recovery retained. Files this process created are left to PurgeFlushed; a file already adopted keeps its first summary (a later pass reads the same entries).
func (*Writer) AppendRaw ¶
AppendRaw writes raw (already serialized) msgpack bytes to the WAL asynchronously This is a zero-copy optimization - use this when you already have msgpack bytes
func (*Writer) AppendRawTracked ¶
AppendRawTracked writes a raw msgpack payload with a process-local sequence identity and returns the identities the eventual flush must checkpoint. It is used by replication followers so their local WAL participates in the same sequence-floor purge as primary ingest.
func (*Writer) AppendRawWithMeta ¶
AppendRawWithMeta writes raw msgpack bytes with database metadata envelope. Format: [0x01 marker][2-byte db name length][db name][original msgpack] This preserves the database name for correct recovery routing.
Unlike calling AppendRaw with a pre-built envelope, this method builds the WAL entry in a single allocation to avoid copying the payload twice.
A payload whose logical entry exceeds MaxWALPayloadSize is split into multiple entries, each carrying the same database envelope (#677) — a wide ingest request previously landed here as a wholesale ErrPayloadTooLarge rejection, so the WAL silently recorded nothing while ingest reported healthy.
func (*Writer) AppendRawWithMetaTracked ¶
AppendRawWithMetaTracked is the tracked counterpart to AppendRawWithMeta.
func (*Writer) AppendTracked ¶
AppendTracked writes a row-format entry and returns its payload identity. The identity is used by flush checkpoints to avoid replaying data that was already durably written to storage.
func (*Writer) CoverageLost ¶
CoverageLost reports whether a covered entry of this writer's batch cseq under buffer key was lost to a failed write (#676): the WAL holds no complete copy of the batch, so recovery will never replay it.
func (*Writer) CurrentCheckpointHashes ¶
CurrentCheckpointHashes returns flush checkpoints in the active WAL file. The writer lock keeps the file stable while the reader scans it.
func (*Writer) CurrentFile ¶
CurrentFile returns the current WAL file path
func (*Writer) DiskFullObserved ¶
DiskFullObserved reports that the most recent entry write or rotation failed because the volume or the quota was full (ENOSPC, EDQUOT), and no entry has been written since (#676).
func (*Writer) ForgetTracked ¶
ForgetTracked drops identities whose data will never be flushed, because the write that produced them was abandoned after its WAL append.
This is not the same as MarkFlushed: no checkpoint is written, because nothing reached storage. It only stops the floor waiting for data no buffer holds. Without it a single rejected write — a type-mismatched column, a client that disconnects during a schema-change flush, the schema-churn guard, a closing shard — would pin the floor for the life of the process, and because PurgeFlushed stops at the first retained file, the WAL would never reclaim anything again (#676).
Callers must only pass identities they are certain no buffer and no flush in flight still owns.
func (*Writer) HeldFiles ¶
HeldFiles reports the WAL files kept because batches in them are waiting for their files to register: files of this process holding a held batch, and files recovery retained. While registrations cannot land (a lost quorum, a start without the cluster) every later file is kept too, by the purge order; arc_wal_dir_bytes shows that growth.
func (*Writer) MarkFlushed ¶
MarkFlushed appends a checkpoint after the corresponding data entries have reached durable storage. A failed checkpoint is safe: it can only cause a replay duplicate, never data loss.
Admission is non-blocking — a full queue returns ErrWALDropped — because this is the production ingest flush path (ArrowBuffer.markWALFlushed). Completion already blocks on the writer loop's reply. Use MarkFlushedContext from recovery, which needs the opposite trade.
func (*Writer) MarkFlushedContext ¶
MarkFlushedContext is MarkFlushed with a cancellable, blocking admission wait instead of the drop-on-full enqueue.
Recovery's parent finalization runs under the same sustained queue pressure that made the forced maintenance rotation unreliable (#1009) — same pass, same queue — but was left on the dropping path when rotation moved off it. A dropped checkpoint retains the replayed file for another pass, which is safe but is one of the ways a file stays retained long enough to matter.
ctx bounds ADMISSION only. Once an entry is admitted the checkpoint will be written, so the reply is waited for unconditionally — see markFlushed. On a cancelled admission or a closed writer this returns an error, so the caller retains the WAL and replays later: at-least-once, never at-most-once.
func (*Writer) MarkFlushedRetained ¶
MarkFlushedRetained writes the same durable checkpoint as MarkFlushed — so recovery does not replay the entries — but keeps their sequences pending in the purge floor. A covered flush (#1207) holds its WAL entries until the files they became are registered; the caller releases them with ReleaseRegistered once that happens.
func (*Writer) MinUnflushedSequence ¶
func (*Writer) PendingUnflushedCount ¶
MinUnflushedSequence returns the lowest tracked sequence this process has appended that no durable flush checkpoint covers. With nothing pending it returns one past the highest sequence issued.
Deliberately NOT one past infinity. The caller reads the floor and then calls PurgeFlushed with it, under a different lock acquisition, so an append and a rotation can land in between; a MaxUint64 floor is above every sequence the writer can ever issue, and would purge that file. highest+1 is above everything pending and below anything issued later, which closes that window at no cost.
Deliberately a plain scan of the pending set, too. An earlier revision walked a forward-only cursor over the sequence space to avoid it, which was both unsound and slower: unsound because a sequence allocated but not yet published let the cursor park above it and never return to it, and slower because every pending sequence lies in [min, highest], so the range the cursor walks is always at least as large as the set it is scanning. PendingUnflushedCount reports how many tracked sequences are awaiting a durable flush checkpoint. It is the floor's working set, and it grows for as long as flushes keep failing — during a prolonged object-store outage that is one entry per write request, which is worth being able to see rather than infer. Detect and report rather than cap, as with the ingest buffers.
func (*Writer) PrepareCovered ¶
func (w *Writer) PrepareCovered(database, key string, payload []byte) (*PreparedAppend, error)
PrepareCovered prepares a covered append of payload (msgpack) for database under buffer key key. An oversized payload is split into chunks exactly as AppendRawWithMetaTracked splits it.
func (*Writer) PurgeAll ¶
PurgeAll deletes all WAL files in the directory. Call this after a clean shutdown where all data has been flushed to storage, so that recovery on next startup doesn't replay already-persisted data.
func (*Writer) PurgeFlushed ¶
PurgeFlushed deletes rotated WAL files whose data is known to have reached storage, where "known" means every sequence they hold is below minUnflushedSeq — the lowest sequence that is still only in memory.
This replaces purging by modification time, which assumed flush latency was at most a fixed age and deleted acknowledged-but-unflushed data whenever that assumption broke (#966, #1009; PR #997 measured 48,500 records lost this way). It is clock-free: no mtime granularity, no NTP steps.
Three rules, each of which is a data-loss or duplicate bug if dropped:
- Only files THIS process created are candidates. A file from a previous process is absent from w.fileSeqs, and nothing here knows whether its data was flushed; only recovery may delete those.
- A file holding an untracked data entry is never a candidate. Nothing will ever report that entry flushed, so its sequence bound means nothing.
- Walk in rotation order and STOP at the first file that must be retained. A flush checkpoint is appended like any other entry, so a checkpoint covering file R's entries can live in R or in any later file. Deleting a later file while R is retained can therefore destroy the only record that R's entries were flushed, and recovery would replay them — permanent duplicate rows for a measurement without tags.
Returns the number of files deleted.
func (*Writer) PurgeHint ¶
func (w *Writer) PurgeHint() <-chan struct{}
PurgeHint is signalled when a purge could now delete something: a rotation finished a file, or the purge floor or a pin moved (#676). Hints coalesce (capacity 1); the receiver runs PurgeFlushed and ReleaseRetained, rate limited — the WAL directory otherwise grows until the periodic pass.
func (*Writer) PurgeInactive ¶
PurgeInactive deletes all WAL files except the currently active one. Use this during normal operation (unlike PurgeAll which is for shutdown) to clean up rotated WAL files after their data has been flushed to storage.
func (*Writer) Quarantined ¶
Quarantined tells the writer that recovery moved path to quarantinedAs after repeated replay failures (#1225). If this process wrote it, entries in it may still be waiting for a flush: recovery can no longer replay them from it, so keeping them pending would hold the purge floor at this file for the life of the process, and no WAL file would be reclaimed again (#676). Their sequences are released. Returns how many were.
Every pending entry of the file is released, including any this pass replayed into a buffer: if that flush fails too, nothing can replay it again. Nothing else owns them: only the periodic recovery pass quarantines a file this process wrote, after a rotation and a flush of every buffer.
The sequences come from the quarantined file itself. An entry whose bytes are damaged has no readable identity; its sequence stays pending until a restart.
func (*Writer) ReleaseRegistered ¶
ReleaseRegistered drops identities checkpointed with MarkFlushedRetained from the purge floor once their files are registered (#1207). Also the release for a checkpoint that failed, exactly as ForgetTracked is today.
func (*Writer) ReleaseRetained ¶
ReleaseRetained deletes adopted files whose batches are no longer held. It walks every WAL file this process did not create in name order — the order recovery replays and retains them — and stops at the first one it must keep: a held file, or an earlier-process file it was never given (recovery kept it for a failure and will process it again). Stopping, not skipping, keeps the proof ordering: a later file can hold the only checkpoint for an earlier one's entries. Returns the number of files deleted.
func (*Writer) RetainedFiles ¶
RetainedFiles reports how many files recovery retained that are still kept.
func (*Writer) Rotate ¶
Rotate closes the active WAL file and starts a new one, and does not return until every entry enqueued before the call has been written to the OLD file.
That ordering is the whole point. A forced rotation exists so recovery can reach data that is only in the active file (#1009): the file is rotated away and then replayed. A rotation that simply took w.mu and swapped the handle would leave every entry already sitting in the queue — up to BufferSize of them — to be written to the NEW file, where the pass that rotated would not look. Going through the writer loop puts the rotation behind those entries.
func (*Writer) RotateContext ¶
RotateContext waits for queue capacity instead of dropping a maintenance command when writers fill the queue. The command uses the same FIFO as data, so all preceding accepted entries are processed before the rotation.
Cancellation bounds both admission and completion waits. Once admitted, the command remains owned by the writer loop and can complete after cancellation; callers must not infer a rotation boundary from an error. The buffered reply lets the writer finish even after its caller has stopped waiting.
func (*Writer) SetCoverageHold ¶
func (w *Writer) SetCoverageHold(hold func() CoverageHold)
SetCoverageHold installs the source of the hold. hold is called once per purge pass, outside the writer lock; nil (or a nil result) holds nothing.
func (*Writer) SetReplicationHook ¶
func (w *Writer) SetReplicationHook(hook ReplicationHook)
SetReplicationHook sets the hook function called for each WAL entry (the origin stream's, #1207). The hook is called synchronously before the entry is written locally.
func (*Writer) TrackedInstance ¶
TrackedInstance is this writer's instance id: the prefix of every identity token it mints and the instance of every coverage id it writes.
type WriterConfig ¶
type WriterConfig struct {
WALDir string // Directory for WAL files
SyncMode SyncMode // Sync mode: fsync, fdatasync, async
MaxSizeBytes int64 // Rotate WAL when it reaches this size (default: 100MB)
MaxAge time.Duration // Rotate WAL after this duration (default: 1 hour)
SyncInterval time.Duration // Request a sync at least this often while bytes are unsynced (default: 100ms; 0 = default)
SyncBytes int64 // Request a sync once this many bytes are unsynced (default: 1MB; 0 = default)
BufferSize int // Size of async write buffer (default: 10000)
Logger zerolog.Logger
}
WriterConfig holds configuration for WAL writer