-
Notifications
You must be signed in to change notification settings - Fork 42
RFC for log segmentation #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6110030
RFC for log segmentation
hachikuji 06f6e3d
Merge remote-tracking branch 'origin/main' into log-segment-rfc
hachikuji b8ee4e6
Move public api to potential future work
hachikuji 50e0bea
Consolidate future work
hachikuji 2d9591b
try to make motivation clearer
hachikuji 31f14c6
More compact segment representation
hachikuji bd44af0
remove unclear checkpoint reference
hachikuji File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| # RFC 0002: Logical Segmentation | ||
|
|
||
| **Status**: Draft | ||
|
|
||
| **Authors**: | ||
| - Jason Gustafson <12502538+hachikuji@users.noreply.github.com> | ||
|
|
||
| ## Summary | ||
|
|
||
| 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. | ||
|
|
||
| ## Motivation | ||
|
|
||
| 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. | ||
|
|
||
| ## Goals | ||
|
|
||
| - Enable efficient seeking within a log based on pluggable criteria (time, size, etc.) | ||
| - Provide a foundation for pluggable retention policies | ||
|
|
||
| ## Non-Goals | ||
|
|
||
| - Defining specific retention policies | ||
| - API design for range and prefix queries | ||
| - Cross-key queries or joins | ||
|
|
||
| ## Design | ||
|
|
||
| ### Segment Concept | ||
|
|
||
| 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: | ||
|
|
||
| 1. **Sequential**: Segments are numbered starting from 0 and increment monotonically | ||
| 2. **Non-overlapping**: Each sequence number belongs to exactly one segment | ||
| 3. **Immutable once sealed**: A completed segment's boundaries and metadata don't change | ||
| 4. **Sparse in sequence space**: Segment boundaries don't need to align with sequence allocation blocks | ||
| 5. **Atomic batch placement**: Each write batch lands entirely within a single segment; batches never span segment boundaries | ||
|
|
||
| ### Key Encoding with Segments | ||
|
|
||
| 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 | ||
|
|
||
| ### Segment Metadata | ||
|
|
||
| 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. | ||
|
|
||
| #### Metadata Lifecycle | ||
|
|
||
| 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. | ||
|
|
||
| ### Segment Triggers | ||
|
|
||
| Segments are "bumped" (a new segment is started) automatically based on configured triggers. | ||
|
|
||
| #### Time-Based Trigger | ||
|
|
||
| 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. | ||
|
|
||
| ### Configuration | ||
|
|
||
| Segment configuration is part of the main `Config` struct: | ||
|
|
||
| ```rust | ||
| 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. | ||
|
|
||
| ## Alternatives | ||
|
|
||
| ### Time in the Key (No Segments) | ||
|
|
||
| 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) | ||
|
|
||
| ### Fixed-Size Segments | ||
|
|
||
| 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 | ||
|
|
||
| ## Open Questions | ||
|
|
||
| None at this time. | ||
|
|
||
| ## Potential Future Work | ||
|
|
||
| ### Size-Based Triggers | ||
|
|
||
| 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. | ||
|
|
||
| ### Segment-Based Deletion | ||
|
|
||
| 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. | ||
|
|
||
| ### Key Listing API | ||
|
|
||
| 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. | ||
|
|
||
| ### Public Segment Sealing API | ||
|
|
||
| A future enhancement could expose manual segment sealing to applications: | ||
|
|
||
| ```rust | ||
| 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) | ||
|
|
||
| ## Updates | ||
|
|
||
| | Date | Description | | ||
| |------------|-------------| | ||
| | 2026-01-07 | Initial draft | | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.