Skip to content

Allow a log to start at a non-zero offset - #241

Merged
kjnilsson merged 5 commits into
mainfrom
initial-offset
Sep 4, 2026
Merged

kjnilsson merged 5 commits into
mainfrom
initial-offset

Conversation

@ansd

@ansd ansd commented Sep 3, 2026

Copy link
Copy Markdown
Member

The initial_offset config key was only honoured for the acceptor writer type, where it is derived from the writer's offset range. Honouring it for writers too lets a brand new stream start at an offset other than 0.

Two consequences are worth calling out. An empty log now resumes from the offset its only segment is named after rather than from the configured value, so a restart before the first write is correct even if the configuration has changed since. And the offset counter is initialised to one before the log's first offset, without which
last_offset + 1 - first_offset reports a negative message count for an empty log.

The `initial_offset` config key was only honoured for the acceptor writer
type, where it is derived from the writer's offset range. Honouring it for
writers too lets a brand new stream start at an offset other than 0.

Two consequences are worth calling out. An empty log now resumes from the
offset its only segment is named after rather than from the configured
value, so a restart before the first write is correct even if the
configuration has changed since. And the offset counter is initialised to
one before the log's first offset, without which
`last_offset + 1 - first_offset` reports a negative message count for an
empty log.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Empty replicated logs cannot restart safely after configuration changes, and replication-state queries can crash before the first write.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Enables new streams to begin at a configured non-zero offset and recover that offset from disk.

Changes:

  • Applies initial_offset to writers, readers, acceptors, and counters.
  • Recovers empty-log offsets from segment filenames.
  • Adds single-node, replication, recovery, reader, and counter tests.
File summaries
File Description
src/osiris.erl Adds initial_offset to stream configuration.
src/osiris_log.erl Implements non-zero offset initialization and recovery.
src/osiris_writer.erl Initializes empty-writer commit state.
test/osiris_SUITE.erl Adds end-to-end writer and replication tests.
test/osiris_log_SUITE.erl Adds log-level offset tests.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/osiris_log.erl
Comment thread src/osiris_writer.erl
Comment thread test/osiris_log_SUITE.erl Outdated
An empty writer range carries no offset, so a replica restarting while the
writer is still empty fell back to its own local config for the offset to
resume at. That config can be stale relative to the writer's actual log
(the writer itself always resumes from its segment filename), causing
init_data_reader/2 to reject the replica as out of range and replication
to never restart. osiris_writer:overview/1 now reports the writer's actual
empty-log starting offset alongside the range, and osiris_replica uses it
instead of the replica's own config.

query_replication_state's empty-tail branch also only matched the literal
{0, empty}, so a writer whose empty log starts elsewhere hit a case_clause
as soon as any caller queried it, e.g. rabbit_stream_coordinator:add_replica/2.
Generalised to any {N, empty}.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The changed RPC tuple contract prevents replicas from starting in mixed-version clusters.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/osiris_writer.erl Outdated
Adding a third element to overview/1's return value broke replication
between mixed-version nodes: it is called over RPC, so during a rolling
upgrade an old replica querying a new writer, or a new replica querying
an old writer, would hit a case_clause on the shape mismatch, for every
stream regardless of whether it uses a non-zero initial offset.

overview/1 goes back to its original {Range, EpochOffsets} shape. The
writer's empty-log starting offset is now fetched through a separate
starting_offset/1 call, made only when the range is empty, with the
replica falling back to 0 if the writer doesn't have the function yet
(an old writer can only ever have an empty log starting at 0).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Replica initialization incorrectly treats every starting-offset RPC failure as a valid zero offset.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/osiris_replica.erl Outdated
The catch-all also mapped nodedown, timeouts and a dead writer process to
offset 0. For a writer whose empty log starts at 100, a replica that hit
any of those would initialise its log at 0, have its data reader rejected
as out of range and restart, rather than retrying the query.

Only the remote undef reply, which identifies a writer that predates
starting_offset/1, now implies 0. Every other failure stops the replica
the same way a failed overview/1 call already does.

The offset query moved into its own function, which meant splitting the
tail of handle_continue/2 into start_replica/6. Most of the diff is that
body being re-indented; `git diff -w` shows the actual change.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Distributed log recovery and rolling-upgrade compatibility warrant final human validation.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@ansd
ansd marked this pull request as ready for review September 4, 2026 10:33
@mergify

mergify Bot commented Sep 4, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@ansd
ansd marked this pull request as draft September 4, 2026 10:43
Reverts the replica-side query added in bb8717b, 389b2a7 and e2ba12f.
Every member of a stream is started from the same config, so a replica
can read the initial offset from it directly instead of asking the
writer for it over RPC.

In RabbitMQ that config is built once from the queue's immutable
x-stream-initial-offset argument and replicated to all members by the
stream coordinator, and a non-zero value is gated behind a feature flag,
so no member can be left honouring a different offset. The expectation is
now stated on the config key itself. osiris_replica is untouched again,
which also removes the RPC wire-compatibility and error-handling problems
the query introduced.

Kept from those commits is the empty-tail handling in
query_replication_state/1, which is an unrelated crash: it only matched
{0, empty}, so a writer whose empty log starts elsewhere hit a
case_clause as soon as rabbit_stream_coordinator:add_replica/2 queried
it.
@ansd
ansd marked this pull request as ready for review September 4, 2026 11:05
@kjnilsson
kjnilsson requested a balanced review from Copilot September 4, 2026 11:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation consistently propagates non-zero initial offsets and includes strong regression coverage for affected paths.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@kjnilsson
kjnilsson self-requested a review September 4, 2026 12:57
@kjnilsson
kjnilsson merged commit 3fffc69 into main Sep 4, 2026
6 checks passed
@kjnilsson
kjnilsson deleted the initial-offset branch September 4, 2026 13:03
ansd added a commit to rabbitmq/rabbitmq-server that referenced this pull request Sep 4, 2026
since rabbitmq/osiris#241 was merged into osiris main
branch and the initial-offset branch was deleted
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants