Status: Draft
Authors:
- Jason Gustafson 12502538+hachikuji@users.noreply.github.com
This RFC introduces logical segmentation as a partitioning mechanism for log data. Segments are logical boundaries in the sequence space that can be triggered by wall-clock time or manual intervention. They enable efficient seeking within the log and provide a foundation for future range-based query APIs.
RFC 0001 defined a log entry key encoding that supports efficient scans for a specific key's entire history or a sequence number range. However, for operations which naturally span multiple keys, it would be simpler and more efficient to operate on a higher-level unit which spans the entire keyspace of the log.
This proposal introduces the notion of a log segment to address this gap. A log segment corresponds to the entries spanning a range of sequence numbers across the full keyspace. This is the same notion as the time bucket that we use in the OpenData-Timeseries design, but generalized to allow for segments marked by other factors than time. The segment provides a natural anchor point for cross-key operations and metadata maintenance. In principle, this makes it possible to manage metadata tied to that operation at the level of the segment, rather than level of keys. For example:
Range queries naturally span multiple keys. For example, a prefix scan over /sensors/* needs to seek to a particular point in time across all matching keys. Without segments, this requires either scanning from the beginning or maintaining a separate index mapping time to sequence numbers for every key.
Retention is simpler to reason about at the segment level. Rather than tracking expiration per key, we can drop entire segments when they age out. This also avoids coherence issues when maintaining metadata about keys. For example, a key listing API (see future work) would tie listing records to segments—when a key is no longer used, it naturally falls out of scope when its segments are removed.
- Enable efficient seeking within a log based on pluggable criteria (time, size, etc.)
- Provide a foundation for pluggable retention policies
- Defining specific retention policies
- API design for range and prefix queries
- Cross-key queries or joins
A segment is a logical boundary in the log's sequence space. Each segment represents a contiguous range of sequence numbers and carries metadata about its contents. Segments enable readers to efficiently skip portions of the log that don't match their query criteria.
Key properties of segments:
- Sequential: Segments are numbered starting from 0 and increment monotonically
- Non-overlapping: Each sequence number belongs to exactly one segment
- Immutable once sealed: A completed segment's boundaries and metadata don't change
- Sparse in sequence space: Segment boundaries don't need to align with sequence allocation blocks
- Atomic batch placement: Each write batch lands entirely within a single segment; batches never span segment boundaries
The log entry key format is extended to include the segment ID:
Log Entry:
| version (u8) | type (u8) | segment_id (u32 BE) | key (TerminatedBytes) | relative_seq (varint u64) |
The segment_id is a 32-bit identifier, allowing up to ~4 billion segments. Future versions can expand this to 64 bits if needed.
The relative_seq is the entry's sequence number relative to the segment's start_seq (i.e., it resets to 0 at the start of each segment). Using variable-length encoding keeps keys compact since most relative offsets within a segment are small.
This encoding ensures:
- All entries within a segment are contiguous in storage
- Within a segment, entries are grouped by key
- Within a key, entries are ordered by sequence number
- Prefix scans can be constrained to specific segments
Each segment has associated metadata stored in a separate record:
SegmentMeta Record:
Key: | version (u8) | type (u8=0x03) | segment_id (u32 BE) |
Value: | start_seq (u64 BE) | start_time_ms (i64 BE) |
The metadata tracks:
- start_seq: The first sequence number in this segment
- start_time_ms: Wall-clock time when the segment was created
End boundaries (end sequence, end time) are derived from the next segment's start values, or from the current log state for the active segment.
When a new segment is created, a SegmentMeta record is written with start_seq and start_time_ms. This ensures the segment is immediately discoverable.
Segments are "bumped" (a new segment is started) automatically based on configured triggers.
The built-in trigger starts a new segment after a configurable wall-clock duration. Example: With a 1-hour interval, a new segment starts every hour. This provides predictable time-based partitioning similar to timeseries buckets.
Time-based triggering is simple to implement because it only requires comparing the current wall-clock time against the segment's start_time_ms. No internal state tracking is needed beyond what's already stored in the segment metadata.
Segment configuration is part of the main Config struct:
struct Config {
storage: StorageConfig,
segmentation: SegmentConfig,
}
struct SegmentConfig {
/// Interval for automatic segment sealing based on wall-clock time.
/// If `None`, automatic sealing is disabled and all entries are written
/// to segment 0.
///
/// Default: `None` (disabled)
seal_interval: Option<Duration>,
}With the default configuration (seal_interval: None), the log writes to segment 0 indefinitely. Users who want time-based partitioning can enable it by setting an interval. This keeps the default behavior simple while allowing opt-in to automatic segment management.
An alternative is to embed timestamp directly in the key:
| version | type | timestamp (i64) | key (TerminatedBytes) | sequence |
Rejected because:
- Forces time-based ordering as the primary dimension
- Doesn't support size-based or manual partitioning
- Complicates key-based access patterns (all times for a key are scattered)
Using fixed sequence ranges per segment (e.g., 1M sequences each):
segment_id = sequence / 1_000_000
Rejected because:
- No flexibility for different workloads
- Time-based queries still require scanning segment metadata
- Doesn't naturally align with time boundaries
None at this time.
Size-based triggers (e.g., seal after N entries or N bytes) require tracking entry counts or byte sizes, which adds complexity. Once the count API from RFC 0001 is implemented, size-based triggers could leverage it.
Segments provide a natural unit for data lifecycle management. APIs for deleting entire segments would enable efficient retention policies. Rather than scanning and deleting individual entries, retention can be enforced by dropping segments older than a threshold.
Currently the log provides no simple way to inspect which keys are present—users would have to scan the full log. A key listing API would introduce listing records tied to each segment. When a key first appears in a segment, a listing record is written. To enumerate all keys, users iterate the listing records across segments. When segments are deleted, their listing records are removed as well, so keys that are no longer present naturally fall out of scope.
A future enhancement could expose manual segment sealing to applications:
impl Log {
/// Seals the current segment and starts a new one.
///
/// # Arguments
/// * `user_meta` - Optional user-defined metadata to attach to the sealed segment
///
/// # Returns
/// The sealed segment ID
async fn seal_segment(&self, user_meta: Option<Bytes>) -> Result<u64, Error>;
/// Returns an iterator over all segments in the log.
///
/// Segments are returned in order from oldest (segment 0) to newest.
/// Each segment includes its metadata (start sequence, start time, user metadata).
fn segments(&self) -> SegmentIterator;
}This would enable use cases such as:
- Creating segment boundaries aligned with application logic
- Attaching metadata accumulated during writes (checksums, record counts, correlation IDs)
- Aligning segments with external events (e.g., end of business day)
| Date | Description |
|---|---|
| 2026-01-07 | Initial draft |