Documentation
¶
Overview ¶
Package reader implements a single-file, forward-only cursor over Tarantool xlog / snap / vylog (and any other file using the same envelope: RUN, INDEX). It is built on top of the pure-byte `format` package and adds the I/O concerns the format package deliberately stays out of.
The cursor surfaces three views over the same byte stream:
- Row-level: (*Reader).Next / (*Reader).Rows — one format.XRow at a time; (*Reader).Scan / (*Reader).Row for the zero-alloc cursor.
- Logical-tx-level: (*Reader).NextTx / (*Reader).Txs — rows grouped by tsn / commit semantics into *Transaction values; (*Reader).ScanTx / (*Reader).Tx for the zero-alloc cursor.
- Block-level: (*Reader).NextBlockRaw — verbatim on-disk bytes per tx block, for the copy/truncate fast path paired with writer.WriteRawBlock (must not be interleaved with the row/tx cursors).
Strictness defaults to strict: CRC mismatch, unknown magic mid-stream, and missing EOF marker all raise unless the caller opts in to leniency via SkipCorruptTx / IgnoreMissingEOF.
Index ¶
- Constants
- Variables
- func ReadHeader(path string, opts ...Option) (*format.Meta, error)
- func ReadHeaderFS(fsys fs.FS, name string, opts ...Option) (*format.Meta, error)
- type Option
- type Reader
- func NewReader(rs io.ReadSeeker, opts ...Option) (*Reader, error)
- func NewReaderBytes(b []byte, opts ...Option) (*Reader, error)
- func Open(path string, opts ...Option) (*Reader, error)
- func OpenAt(path string, blockOffset int64, opts ...Option) (*Reader, error)
- func OpenFS(fsys fs.FS, name string, opts ...Option) (*Reader, error)
- func OpenMmap(path string, opts ...Option) (*Reader, error)
- func (r *Reader) Close() error
- func (r *Reader) Err() error
- func (r *Reader) Meta() *format.Meta
- func (r *Reader) Next() (format.XRow, error)
- func (r *Reader) NextBlockRaw() ([]byte, error)
- func (r *Reader) NextTx() (*Transaction, error)
- func (r *Reader) Offset() int64
- func (r *Reader) Recycle()
- func (r *Reader) Row() format.XRow
- func (r *Reader) Rows() iter.Seq2[format.XRow, error]
- func (r *Reader) SawEOFMarker() bool
- func (r *Reader) Scan() bool
- func (r *Reader) ScanTx() bool
- func (r *Reader) Tx() []format.XRow
- func (r *Reader) Txs() iter.Seq2[*Transaction, error]
- type Transaction
Examples ¶
Constants ¶
const MaxTxPayloadLen = 1 << 30
MaxTxPayloadLen caps the per-tx payload the reader will allocate for. The fixheader's length field is an untrusted uint32 (up to 4 GiB); without a ceiling a 19-byte hostile file claiming a huge length would force a giant allocation up front. 1 GiB is far above any real Tarantool tx (the autocommit-flush threshold is 128 KiB; the largest single tuple is bounded by memtx_max_tuple_size) while bounding the worst case.
Variables ¶
var ( // ErrTruncated is returned when the cursor reaches an unexpected EOF // before observing the 4-byte EOFMarker. Suppressed to clean // io.EOF when IgnoreMissingEOF is set. ErrTruncated = errors.New("reader: file truncated before EOF marker") // ErrCorruptCRC is returned when a tx block's computed CRC32C does // not match the value in its fixheader. With SkipCorruptTx // the cursor resyncs instead of surfacing this error. ErrCorruptCRC = errors.New("reader: tx block CRC mismatch") // ErrUnknownMagic is returned when a 4-byte magic prefix is neither // RowMarker, ZRowMarker, nor EOFMarker. With SkipCorruptTx the cursor // scans forward for the next valid magic instead. ErrUnknownMagic = errors.New("reader: unknown magic bytes in stream") // ErrTxTooLarge is returned when a fixheader declares a tx payload // length above MaxTxPayloadLen — a corrupt/hostile length prefix must // not drive an unbounded allocation before the bytes are even read. ErrTxTooLarge = errors.New("reader: tx payload length exceeds MaxTxPayloadLen") // ErrZeroLengthDecode is returned when the row decoder consumes zero // bytes for a row within a tx payload — a malformed payload that would // otherwise spin the split loop forever. ErrZeroLengthDecode = errors.New("zero-length decode") )
Public sentinel errors. Mid-stream failures map to one of these; meta errors propagate from the format package unwrapped.
Functions ¶
func ReadHeader ¶
ReadHeader opens path, parses its meta header, closes the file, and returns an independent copy of the parsed header. It is the one-shot convenience for callers that want only the header, not a row cursor — equivalent to Open + Meta + Close, but it reads just the header region (tx blocks are never touched). The returned *format.Meta is a Clone the caller may freely mutate (e.g. to feed a writer or tools.RewriteMetaFields).
It accepts the same Options as Open (e.g. AcceptV012).
Example ¶
ExampleReadHeader reads only the meta header of an xlog (filetype, version, instance UUID, vclock) without scanning any tx blocks. It is the one-shot convenience for "what is this file?" inspection.
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
// Stage the sample bytes to a file, since ReadHeader takes a path.
path := filepath.Join(os.TempDir(), "go-xlog-example.xlog")
if err := os.WriteFile(path, sampleXlog(), 0o600); err != nil {
panic(err)
}
defer func() { _ = os.Remove(path) }()
m, err := reader.ReadHeader(path)
if err != nil {
panic(err)
}
fmt.Printf("filetype=%s version=%s uuid=%s\n", m.Filetype, m.Version, m.InstanceUUID)
}
Output: filetype=XLOG version=example uuid=11111111-2222-3333-4444-555555555555
Types ¶
type Option ¶
type Option func(*readerCfg)
Option configures a Reader at construction time. Options are applied in order; the Reader is otherwise immutable.
func AcceptV012 ¶
func AcceptV012() Option
AcceptV012 propagates to format.MetaOptions.AcceptV012, letting the reader open files with format version "0.12" in addition to "0.13".
func IgnoreMissingEOF ¶
func IgnoreMissingEOF() Option
IgnoreMissingEOF downgrades a missing EOF marker from ErrTruncated to clean io.EOF. Intended for use cases where the file is being read while still being written.
func SkipCorruptTx ¶
func SkipCorruptTx() Option
SkipCorruptTx enables resync after a CRC mismatch or unknown-magic byte mid-stream: the cursor scans forward byte-by-byte for the next valid RowMarker / ZRowMarker / EOFMarker and resumes there. Mirrors `xlog_cursor_find_tx_magic` (src/box/xlog.c:1989).
Without this option the cursor surfaces ErrCorruptCRC / ErrUnknownMagic at the moment of failure.
func WithAliasBodies ¶
func WithAliasBodies() Option
WithAliasBodies makes the Scan / ScanTx cursor skip the per-row body copy: Row().BodyRaw aliases the reader's internal read buffer and is only valid until the next Scan / ScanTx (and is clobbered when the next tx block loads). Use it for max-throughput streaming consumers that fully process each row before advancing and never retain rows past the next Scan. Without it the default Scan contract holds — rows are safe to retain until Recycle. It has no effect on Next / NextTx.
Example ¶
ExampleWithAliasBodies is the fastest read path: for streaming consumers that fully process each row before advancing, WithAliasBodies skips the per-row body copy, so Row().BodyRaw aliases the read buffer and is valid only until the next Scan. Do not retain rows (or their BodyRaw) past the next Scan.
package main
import (
"bytes"
"fmt"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
r, err := reader.NewReader(bytes.NewReader(sampleXlog()), reader.WithAliasBodies())
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
total := 0
for r.Scan() {
total += len(r.Row().BodyRaw) // Consumed now, not retained past the next Scan.
}
if err := r.Err(); err != nil {
panic(err)
}
fmt.Printf("scanned 3 records, total body bytes=%d\n", total)
}
Output: scanned 3 records, total body bytes=6
type Reader ¶
type Reader struct {
// contains filtered or unexported fields
}
Reader is a single-file, forward-only cursor over an xlog / snap / vylog / run / index byte stream. Construct via Open (path-shaped, the Reader owns the file) or NewReader (io-shaped, the caller retains ownership).
A Reader is not safe for concurrent use. Create one per consumer; each instance carries its own buffers and zstd decoder handle.
func NewReader ¶
func NewReader(rs io.ReadSeeker, opts ...Option) (*Reader, error)
NewReader wraps an existing io.ReadSeeker (we keep ReadSeeker rather than io.Reader for API symmetry with future seek-based features; the current implementation only uses the Read half). It parses the meta header eagerly so the caller can rely on Meta() being non-nil.
Ownership of rs remains with the caller; (*Reader).Close on a Reader constructed this way is a no-op for I/O.
func NewReaderBytes ¶
NewReaderBytes builds a Reader over a complete in-memory journal image. It is the zero-copy alternative to NewReader for callers that already hold the whole file in a byte slice (preloaded, embedded, or memory-mapped): the block loop slices each fixheader and payload directly out of b instead of copying through a bufio buffer, eliminating the per-block memmove and the read syscalls. For uncompressed (RowMarker) blocks the decoded rows alias b directly, so with WithAliasBodies the row bodies are zero-copy AND safe to retain for as long as b is alive (b outlives the Reader). ZRow blocks still decompress.
Ownership of b remains with the caller; Close is a no-op for I/O. B must not be mutated while the Reader is in use.
Example ¶
ExampleNewReaderBytes reads a journal that is already fully in memory (preloaded, embedded, or memory-mapped). Instead of copying each tx block through an internal buffer, the reader slices blocks directly out of the input — and for uncompressed blocks the decoded row bodies alias that buffer. Combined with WithAliasBodies this is end-to-end zero-copy, and because the input outlives the reader the bodies stay valid even after the reader advances (unlike the streaming WithAliasBodies, where bodies last only until the next Scan).
package main
import (
"bytes"
"fmt"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
data := sampleXlog() // A complete journal image already in memory.
r, err := reader.NewReaderBytes(data, reader.WithAliasBodies())
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
// Retain every row through the whole scan: with NewReaderBytes the aliased
// bodies remain valid because they point into data, which we still hold.
var rows []format.XRow
for r.Scan() {
rows = append(rows, r.Row())
}
if err := r.Err(); err != nil {
panic(err)
}
for _, row := range rows {
fmt.Printf("record: type=%d lsn=%d body=%d bytes\n", row.Type, row.LSN, len(row.BodyRaw))
}
}
Output: record: type=2 lsn=1 body=2 bytes record: type=2 lsn=2 body=2 bytes record: type=3 lsn=3 body=2 bytes
func Open ¶
Open opens path for reading and parses its meta header. The returned Reader owns the file and releases it on Close. This is the path-shaped façade over the io-shaped NewReader.
func OpenAt ¶
OpenAt opens path, parses its meta header, and positions the cursor to begin reading at blockOffset — a byte offset previously reported by Offset (always a clean tx-block boundary past the header). It is the resume primitive behind follow-style tailing: re-open a growing file and continue from where a prior pass stopped, without re-reading the blocks already consumed.
blockOffset is clamped up to the end of the meta header, so 0 (or any value inside the header) is equivalent to Open. The returned Reader owns the file and releases it on Close. It accepts the same Options as Open; pair it with IgnoreMissingEOF when tailing a file the writer has not finalised.
func OpenFS ¶
OpenFS opens name within fsys and parses its meta header. It is the fs.FS analogue of Open: the returned Reader owns the opened fs.File and closes it on Close. Use it to read a journal from an embed.FS, fstest.MapFS, archive filesystem, or any other io/fs.FS. Name follows the io/fs path contract (slash-separated, unrooted).
func OpenMmap ¶
OpenMmap memory-maps path read-only and returns a Reader that slices tx blocks directly out of the mapping — no read syscalls and no per-block copy in the steady state. It is the path-shaped counterpart to NewReaderBytes: the mapped bytes are the Reader's buffer, so uncompressed (RowMarker) blocks decode zero-copy and, with WithAliasBodies, the row bodies alias the mapping and stay valid until Close. ZRow blocks still decompress.
The returned Reader owns the mapping and the file descriptor; Close munmaps and closes the fd. The caller must not use any retained aliasing body after Close (the mapping is gone). Available on unix platforms only; other platforms have a NewReaderBytes-backed fallback.
Example ¶
ExampleOpenMmap memory-maps a journal file and reads it with no per-block copies or read syscalls in the steady state. It is the path-shaped counterpart to NewReaderBytes; Close unmaps the file. On platforms without mmap it transparently falls back to reading the whole file into memory.
package main
import (
"bytes"
"fmt"
"os"
"path/filepath"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
// Stage the sample bytes to a file, since OpenMmap takes a path.
path := filepath.Join(os.TempDir(), "go-xlog-mmap-example.xlog")
if err := os.WriteFile(path, sampleXlog(), 0o600); err != nil {
panic(err)
}
defer func() { _ = os.Remove(path) }()
r, err := reader.OpenMmap(path)
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }() // Unmaps the file.
count := 0
for r.Scan() {
count++
}
if err := r.Err(); err != nil {
panic(err)
}
fmt.Printf("read %d records via mmap\n", count)
}
Output: read 3 records via mmap
func (*Reader) Close ¶
Close releases the underlying file when this Reader owns it (i.e. was constructed via Open). For NewReader-constructed readers Close is a no-op — the caller retains ownership of the stream.
func (*Reader) Err ¶
Err returns the terminal error that stopped Scan / ScanTx, or nil if they stopped at a clean end-of-stream.
func (*Reader) Meta ¶
Meta returns the parsed meta header. The pointer is stable for the lifetime of the Reader; callers must not mutate the returned value.
func (*Reader) Next ¶
Next returns the next xrow in the stream, advancing across tx-block boundaries transparently. Behaviour at EOF:
- After reading the 4-byte EOFMarker, Next returns (nil, io.EOF) on every subsequent call and never reads further.
- If the underlying read returns io.EOF before the EOFMarker is observed, Next returns (nil, ErrTruncated). With IgnoreMissingEOF this is downgraded to (nil, io.EOF) and eofSeen is set.
On a corrupt tx (CRC mismatch / unknown magic) the cursor returns ErrCorruptCRC / ErrUnknownMagic; with SkipCorruptTx it instead scans forward for the next valid magic and resumes.
func (*Reader) NextBlockRaw ¶
NextBlockRaw reads the next physical tx block verbatim — its full on-disk bytes (fixheader + payload) — into a reusable Reader-owned buffer, verifies the CRC32C, and returns the slice without decoding any rows. It is the read half of the verbatim block-copy fast path (pair with writer.WriteRawBlock): a copy/truncate that does not transform rows can forward blocks byte-for-byte, skipping row decode, re-encode, recompression, and the destination's second CRC. A compressed (ZRow) block stays compressed — the bytes are exactly what is on disk.
The returned slice aliases a Reader-owned buffer and is valid only until the next NextBlockRaw call. EOF handling mirrors Next: after the 4-byte EOFMarker it returns (nil, io.EOF) on every subsequent call; an unexpected EOF before the marker is ErrTruncated (or clean io.EOF with IgnoreMissingEOF). A corrupt block surfaces ErrCorruptCRC / ErrUnknownMagic, or — with SkipCorruptTx — resyncs forward to the next valid magic.
NextBlockRaw must not be interleaved with the row cursors (Next / Scan / ScanTx) on the same Reader; pick one read mode per Reader.
Example ¶
ExampleReader_NextBlockRaw walks a journal one physical tx block at a time without decoding any rows. NextBlockRaw returns each block's on-disk bytes (fixheader + payload) verbatim, CRC-verified — the read half of the verbatim-copy fast path (see pipe.CopyRaw). It is useful for tooling that counts, indexes, or forwards blocks without paying for row decoding.
package main
import (
"bytes"
"errors"
"fmt"
"io"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
r, err := reader.NewReader(bytes.NewReader(sampleXlog()))
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
blocks := 0
for {
block, err := r.NextBlockRaw()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
panic(err)
}
_ = block // The verbatim fixheader+payload, ready to forward as-is.
blocks++
}
// The sample has two transactions, so two physical blocks.
fmt.Printf("%d physical blocks\n", blocks)
}
Output: 2 physical blocks
func (*Reader) NextTx ¶
func (r *Reader) NextTx() (*Transaction, error)
NextTx returns the next logical transaction by reading rows until one of them has IsCommit set. A 1-row single-statement tx returns as a Transaction with len(Rows)==1, because format.DecodeXRow infers IsCommit from absent KeyTSN.
At end-of-stream (EOF marker observed with no rows pending) returns (nil, io.EOF). If the underlying read truncates mid-tx (rows have been accumulated but EOF appears before IsCommit), returns ErrTruncated regardless of IgnoreMissingEOF — a half-tx is never a well-formed log.
func (*Reader) Offset ¶
Offset returns the byte offset of the next tx block the cursor will read — a clean block boundary past the meta header, advanced only when a whole block (or the EOFMarker) has been consumed, never mid-block. Pass it to OpenAt to resume a later pass exactly where this one stopped. A partial trailing block does not move it, so re-reading from Offset never double-emits a row.
func (*Reader) Recycle ¶
func (r *Reader) Recycle()
Recycle resets the Scan/ScanTx body arena to empty (keeping its capacity), reclaiming the memory backing the BodyRaw of every row returned since the last Recycle. Those bodies become invalid; do not touch them afterwards (the row structs themselves are caller-owned value copies and stay valid). Streaming or batch consumers call Recycle at their batch boundary; consumers that accumulate rows simply never call it.
Example ¶
ExampleReader_Recycle shows the retain-and-recycle pattern. Rows returned by Scan are safe to retain until Recycle, so a caller can accumulate a batch, process it, then Recycle to reclaim the arena memory before the next batch — bounded memory while still holding many rows at once. After Recycle the previously returned rows (and their BodyRaw) must not be touched.
package main
import (
"bytes"
"fmt"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
r, err := reader.NewReader(bytes.NewReader(sampleXlog()))
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
const batchSize = 2
var batch []format.XRow
flush := func() {
if len(batch) == 0 {
return
}
fmt.Printf("batch of %d (lsn %d..%d)\n", len(batch), batch[0].LSN, batch[len(batch)-1].LSN)
batch = batch[:0]
r.Recycle() // Reclaim the arena; rows above must not be used after this.
}
for r.Scan() {
batch = append(batch, r.Row()) // Safe to retain until Recycle.
if len(batch) == batchSize {
flush()
}
}
if err := r.Err(); err != nil {
panic(err)
}
flush() // Trailing partial batch.
}
Output: batch of 2 (lsn 1..2) batch of 1 (lsn 3..3)
func (*Reader) Row ¶
Row returns the row decoded by the most recent successful Scan. It is only meaningful after Scan returned true. See Scan for the retention contract.
func (*Reader) Rows ¶
Rows returns a Go 1.23 iterator over (format.XRow, error) pairs. The iterator stops at io.EOF without yielding it. Any other error is yielded once and then the iterator stops. The same Reader can also be driven via Next directly — Rows is sugar, not a separate state machine.
Example ¶
ExampleReader_Rows reads an xlog record by record, with no awareness of transaction boundaries. Rows yields every xrow in stream order, crossing (and decompressing) tx blocks transparently.
package main
import (
"bytes"
"fmt"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
r, err := reader.NewReader(bytes.NewReader(sampleXlog()))
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
for row, err := range r.Rows() {
if err != nil {
panic(err)
}
fmt.Printf("record: type=%d lsn=%d\n", row.Type, row.LSN)
}
}
Output: record: type=2 lsn=1 record: type=2 lsn=2 record: type=3 lsn=3
func (*Reader) SawEOFMarker ¶
SawEOFMarker reports whether the cursor consumed the real 4-byte EOFMarker — i.e. the file is finalised on disk. It stays false when end-of-stream was reached only via the IgnoreMissingEOF downgrade (a file still being written). A follower uses it to distinguish "writer finished this file" (stop / hand off to the next file) from "writer paused mid-append" (wait for more bytes).
func (*Reader) Scan ¶
Scan advances the zero-allocation cursor to the next row, returning false at end-of-stream or on error (check Err). It is the alloc-light counterpart to Next: the row decodes into Reader-owned scratch instead of forcing per-row heap traffic, so a steady-state loop allocates nothing per row.
Retention: the row returned by Row is a value the caller owns, so the struct is always safe to keep. By default its BodyRaw is copied into a Reader body arena and stays valid until you call Recycle (call it at a batch boundary to reclaim that memory). With the WithAliasBodies option BodyRaw instead aliases the read buffer and is only valid until the next Scan — for max-throughput streaming consumers that do not retain bodies.
for r.Scan() {
row := r.Row()
// ... use / retain row ...
}
if err := r.Err(); err != nil { ... }
Example ¶
ExampleReader_Scan reads record by record through the zero-allocation cursor. Unlike Rows/Next, Scan decodes each row into reader-owned scratch, so a tight loop performs no per-row heap allocation. Check Err after the loop; Row() is valid until the next Scan (and safe to retain until Recycle — see ExampleReader_Recycle).
package main
import (
"bytes"
"fmt"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
r, err := reader.NewReader(bytes.NewReader(sampleXlog()))
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
for r.Scan() {
row := r.Row()
fmt.Printf("record: type=%d lsn=%d\n", row.Type, row.LSN)
}
if err := r.Err(); err != nil {
panic(err)
}
}
Output: record: type=2 lsn=1 record: type=2 lsn=2 record: type=3 lsn=3
func (*Reader) ScanTx ¶
ScanTx advances the zero-allocation cursor by one logical transaction, reading rows until one has IsCommit set. Returns false at clean end-of-stream or on error (check Err); a tx left incomplete by a truncated stream surfaces as ErrTruncated, matching NextTx. The rows are available via Tx and follow the same retention contract as Scan/Row (bodies safe to retain until Recycle).
Example ¶
ExampleReader_ScanTx reads transactions through the zero-allocation cursor. ScanTx groups rows by their TSN/commit flags exactly like Txs, but decodes into reusable scratch; Tx() returns the rows of the current transaction.
package main
import (
"bytes"
"fmt"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
r, err := reader.NewReader(bytes.NewReader(sampleXlog()))
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
for r.ScanTx() {
rows := r.Tx()
fmt.Printf("transaction: %d row(s), startLSN=%d\n", len(rows), rows[0].LSN)
}
if err := r.Err(); err != nil {
panic(err)
}
}
Output: transaction: 2 row(s), startLSN=1 transaction: 1 row(s), startLSN=3
func (*Reader) Tx ¶
Tx returns the rows of the transaction read by the most recent successful ScanTx. The slice is reused by the next ScanTx; copy it if you need the slice to survive (the row bodies stay valid until Recycle).
func (*Reader) Txs ¶
func (r *Reader) Txs() iter.Seq2[*Transaction, error]
Txs returns a Go 1.23 iterator over (*Transaction, error) pairs. Same stop semantics as Rows.
Example ¶
ExampleReader_Txs reads the same xlog grouped into logical transactions. The reader reconstructs transactions from each row's TSN/commit flags, so the two-row tx and the single-row tx come back distinct.
package main
import (
"bytes"
"fmt"
"github.com/google/uuid"
"github.com/tarantool/go-iproto"
"github.com/vmihailenco/msgpack/v5"
"github.com/tarantool/go-xlog/format"
"github.com/tarantool/go-xlog/reader"
"github.com/tarantool/go-xlog/writer"
)
// sampleXlog builds a small in-memory xlog: one two-row transaction
// (LSN 1, 2) followed by one single-row transaction (LSN 3). It is used by
// the reader examples so they are self-contained.
func sampleXlog() []byte {
meta := &format.Meta{
Filetype: format.FiletypeXLOG,
Version: "example",
InstanceUUID: uuid.MustParse("11111111-2222-3333-4444-555555555555"),
}
var buf bytes.Buffer
w, err := writer.NewWriter(&buf, meta)
if err != nil {
panic(err)
}
body := func(v int) []byte {
b, err := msgpack.Marshal([]int{v})
if err != nil {
panic(err)
}
return b
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_INSERT, LSN: 1, BodyRaw: body(1)},
{Type: iproto.IPROTO_INSERT, LSN: 2, BodyRaw: body(2)},
}); err != nil {
panic(err)
}
if err := w.WriteTx([]format.XRow{
{Type: iproto.IPROTO_REPLACE, LSN: 3, BodyRaw: body(3)},
}); err != nil {
panic(err)
}
if err := w.Close(); err != nil {
panic(err)
}
return buf.Bytes()
}
func main() {
r, err := reader.NewReader(bytes.NewReader(sampleXlog()))
if err != nil {
panic(err)
}
defer func() { _ = r.Close() }()
for tx, err := range r.Txs() {
if err != nil {
panic(err)
}
fmt.Printf("transaction: %d row(s), startLSN=%d\n", len(tx.Rows), tx.StartLSN)
}
}
Output: transaction: 2 row(s), startLSN=1 transaction: 1 row(s), startLSN=3
type Transaction ¶
Transaction is one *logical* tx — a maximal run of consecutive xrow records sharing the same tsn, terminated by IsCommit. The reader assembles these from NextTx by reading rows until one returns true from IsCommit. Single-statement txs land here as 1-row Transactions (the format decoder infers IsCommit from absent KeyTSN).
StartLSN is the LSN of the first row, useful as a stable identifier for the tx within the file.