Malachi.BrokerServer (malachi v0.8.13)

View Source

A GenServer that owns a Malachi.Broker (the control-plane router) and a local Malachi.Cluster.ReplicationServer (segment storage/replication), serializing concurrent access. It wires the broker's injected effect functions to the replication server: produce replicates through it and read/stream_history read segments from it.

Writes are durable on return: by default each batch is fsynced on a quorum by the replication server before it commits, so there is no buffering. With group commit enabled (:group_commit, single-node rf=1), a produce instead buffers its batch and the client reply is deferred until the next time-based flush (~:group_commit_interval_ms), so many concurrent producers coalesce into one fsync; the reply is still returned only once the batch is durable. Under group commit the flush is also triggered early once :group_commit_flush_max_records are parked (so each fsync, and so each reply, stays bounded and a produce never waits long enough to time out), and beyond :group_commit_max_inflight parked records new produces are shed with {:error, :overloaded} rather than dropped. Either way sync/1 is a no-op kept for API compatibility.

On the replicated (non-group-commit) path the produce is NON-BLOCKING for this server's loop (the NorthGuard end-to-end pipelined shape): the loop plans the produce (routing, segment opening, optimistic offset commit), fires the replication dispatches as casts, parks the caller, and replies from the replication results, waking consumers only after every dispatch is quorum-durable. So the node accepts the next produce while earlier ones replicate, instead of one produce per replication round trip.

The Broker (and the layers it composes) are pure immutable values; routing all mutations through this single process is what makes concurrent producers/consumers safe.

Supersedes Malachi.TopicServer.

Summary

Functions

The ids of a topic's active ranges.

Rebuilds a sharded broker's metadata routing for a new ring topology (a vnode split adopted via gossip): the local read cache takes the new ring: existing vnodes keep their cached Metadata, a newly-added vnode starts empty until the next refresh from ra, and the write path is re-routed over the topology's %{vnode_id => nodes} placements (server id = {vnode_id, a_member}). Pure: the metadata catch-up for a new/changed vnode is the separate refresh side effect. Returns the new broker.

Applies :set_segment_replicas healing commands to the control plane.

Returns a specification to start this module under a supervisor.

Durably commits a consumer group's position for a topic; returns the control-plane reply.

A consumer group's committed offsets for a topic (empty if it never committed).

Consumes a topic's current ranges from positions, returning {records, next_positions}. When wait_ms > 0 and nothing is available yet, the call blocks (long-poll) until a produce to the topic delivers data or wait_ms elapses (then records is []). With wait_ms == 0 it returns immediately. positions/next_positions map each range id to its Broker.consume_cursor.

Creates a topic (and its root range); returns {:ok, root_range_id} or an error.

Removes a sealed segment from the control plane (retention); returns the control-plane reply.

Merges two buddy ranges; returns {:ok, child_id} or an error.

The current control-plane metadata (e.g. for a healing coordinator to inspect).

Whether this broker has read every metadata vnode at least once since boot. Until it has, a topic whose vnode is still silent is represented by an empty placeholder in the local cache, and every read of it answers a page with no records: durable data reported as a drained topic. A node in that state can accept connections and pass /health while being unable to answer a single read honestly.

Routes, replicates and commits records; returns {:ok, placements} or an error.

Reads up to max_records committed records from a range, starting at offset.

Reads one cross-epoch consume page of a range, tailing the active range (see Malachi.Broker.read_consume/5).

The Malachi.Cluster.ReplicationServer this broker owns, or nil when it was given an external broker set (the clustered shape, where the replication server is supervised and named separately).

Splits a range; returns {:ok, left_id, right_id} or an error.

Starts a server whose segment storage is rooted at directory.

Stops the server (and its replication storage).

Acks count streamed records of topic/group at positions (a decoded cursor): returns that much window credit (unblocking further pushes) and durably commits the group's position. Returns :ok.

Streams one bounded page of a range's cross-epoch history (see Malachi.Broker.stream_history/5).

Subscribes the calling process as a streaming consumer of topic for consumer group: records are pushed to it as {:log_records, topic, records, next_positions} as they are produced, resuming from the group's committed position and bounded by a credit window (at most window records in flight, at most max per push). Ack with stream_ack/5 to return credit and commit progress. :ok.

No-op: writes are already durable on return. Kept for API compatibility.

The per-topic overview (Malachi.Metadata.overview/1) annotated with each topic's failure-domain violation count (Malachi.Broker.domain_violations/2), computed from a single merged-metadata view.

Removes the calling process's streaming subscription to topic. Returns :ok.

Functions

active_range_ids(server, topic)

The ids of a topic's active ranges.

adopt_topology(broker, topology)

Rebuilds a sharded broker's metadata routing for a new ring topology (a vnode split adopted via gossip): the local read cache takes the new ring: existing vnodes keep their cached Metadata, a newly-added vnode starts empty until the next refresh from ra, and the write path is re-routed over the topology's %{vnode_id => nodes} placements (server id = {vnode_id, a_member}). Pure: the metadata catch-up for a new/changed vnode is the separate refresh side effect. Returns the new broker.

apply_heal(server, commands)

@spec apply_heal(GenServer.server(), [Malachi.Metadata.command()]) :: :ok

Applies :set_segment_replicas healing commands to the control plane.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

commit_offset(server, group, topic, offsets)

Durably commits a consumer group's position for a topic; returns the control-plane reply.

committed_offsets(server, group, topic)

A consumer group's committed offsets for a topic (empty if it never committed).

consume(server, topic, positions, max_records, wait_ms, ranges \\ nil)

@spec consume(
  GenServer.server(),
  Malachi.Metadata.topic_name(),
  map(),
  pos_integer(),
  non_neg_integer(),
  [term()] | nil
) :: {[Malachi.Log.Record.t()], map()} | {:error, :metadata_unavailable}

Consumes a topic's current ranges from positions, returning {records, next_positions}. When wait_ms > 0 and nothing is available yet, the call blocks (long-poll) until a produce to the topic delivers data or wait_ms elapses (then records is []). With wait_ms == 0 it returns immediately. positions/next_positions map each range id to its Broker.consume_cursor.

create_topic(server, name, keyspace_bits)

Creates a topic (and its root range); returns {:ok, root_range_id} or an error.

delete_segment(server, segment_id)

@spec delete_segment(GenServer.server(), Malachi.Metadata.segment_id()) :: term()

Removes a sealed segment from the control plane (retention); returns the control-plane reply.

merge_ranges(server, range_id_a, range_id_b)

Merges two buddy ranges; returns {:ok, child_id} or an error.

metadata(server)

@spec metadata(GenServer.server()) :: Malachi.Metadata.t()

The current control-plane metadata (e.g. for a healing coordinator to inspect).

metadata_ready?(server, timeout \\ 1000)

@spec metadata_ready?(GenServer.server(), timeout()) :: boolean()

Whether this broker has read every metadata vnode at least once since boot. Until it has, a topic whose vnode is still silent is represented by an empty placeholder in the local cache, and every read of it answers a page with no records: durable data reported as a drained topic. A node in that state can accept connections and pass /health while being unable to answer a single read honestly.

produce(server, topic, records)

@spec produce(GenServer.server(), Malachi.Metadata.topic_name(), [
  Malachi.Log.Record.t()
]) ::
  {:ok,
   %{
     required(Malachi.Metadata.range_id()) =>
       {non_neg_integer(), non_neg_integer()}
   }}
  | {:error, term()}

Routes, replicates and commits records; returns {:ok, placements} or an error.

read(server, range_id, offset, max_records)

@spec read(
  GenServer.server(),
  Malachi.Metadata.range_id(),
  non_neg_integer(),
  pos_integer()
) ::
  {:ok, [Malachi.Log.Record.t()]} | :eof | {:error, term()}

Reads up to max_records committed records from a range, starting at offset.

read_consume(server, range_id, cursor, max_records)

Reads one cross-epoch consume page of a range, tailing the active range (see Malachi.Broker.read_consume/5).

replication_ref(server)

@spec replication_ref(GenServer.server()) :: pid() | nil

The Malachi.Cluster.ReplicationServer this broker owns, or nil when it was given an external broker set (the clustered shape, where the replication server is supervised and named separately).

A single-node broker starts its own, unnamed, so this is the only way to name it as a replica: the integrity scrub needs it to tell which stored segments are its own. Ask per use rather than caching it, since a broker restart replaces the process.

split_range(server, range_id)

Splits a range; returns {:ok, left_id, right_id} or an error.

start_link(directory, opts \\ [])

@spec start_link(
  Path.t(),
  keyword()
) :: GenServer.on_start()

Starts a server whose segment storage is rooted at directory.

Options

  • :brokers - references of the Malachi.Cluster.ReplicationServers that segments are placed on. When given, this server uses them and does not start its own; when omitted, it starts a single local replication server rooted at directory (single-node default).
  • :live_brokers - a (-> [broker]) (e.g. from membership); when given, the placement broker set is refreshed from it every :brokers_refresh_interval ms, so new segments land on currently-alive brokers. An empty result is ignored (the last non-empty set is kept).
  • :brokers_refresh_interval - refresh period in ms (default 1000).
  • :metadata_cluster - a Raft cluster name (atom). When given, the metadata is made authoritative via that ra cluster (mutations go through the log; reads come from a local cache); ra must already be running. When omitted, metadata is in-memory (single node).
  • :metadata_nodes - the nodes the metadata Raft cluster spans (default [node()]); several nodes make the control plane HA (the metadata survives losing a member).
  • :replication_factor - replicas per segment (default 1; clamped to the broker count).
  • :group_commit - when true, produce buffers its batch and defers the client reply to the next flush so concurrent producers coalesce into one fsync (default from app env; only active at rf=1). See the moduledoc.
  • :group_commit_interval_ms - group-commit flush period in ms (default 5, or app env).
  • :group_commit_flush_max_records - flush eagerly once this many records are parked, bounding the per-flush fsync (default 8000, or app env).
  • :group_commit_max_inflight - past this many parked records, shed produces with :overloaded instead of dropping the connection (default 200000, or app env).
  • :segment_max_bytes - byte threshold at which the active segment seals and rolls.
  • remaining options are forwarded to a started Malachi.Cluster.ReplicationServer (segment log options such as :max_bytes, :flush_bytes, :index_interval); ignored with :brokers.
  • standard GenServer options (:name, etc.) are honored.

stop(server)

@spec stop(GenServer.server()) :: :ok

Stops the server (and its replication storage).

stream_ack(server, topic, group, positions, count, ranges \\ nil, coordinator \\ nil)

Acks count streamed records of topic/group at positions (a decoded cursor): returns that much window credit (unblocking further pushes) and durably commits the group's position. Returns :ok.

stream_history(server, range_id, cursor \\ :start, max_records \\ 1000)

Streams one bounded page of a range's cross-epoch history (see Malachi.Broker.stream_history/5).

subscribe(server, topic, group, window, max, group_opts \\ [])

Subscribes the calling process as a streaming consumer of topic for consumer group: records are pushed to it as {:log_records, topic, records, next_positions} as they are produced, resuming from the group's committed position and bounded by a credit window (at most window records in flight, at most max per push). Ack with stream_ack/5 to return credit and commit progress. :ok.

sync(server)

@spec sync(GenServer.server()) :: :ok

No-op: writes are already durable on return. Kept for API compatibility.

topics_overview(server)

The per-topic overview (Malachi.Metadata.overview/1) annotated with each topic's failure-domain violation count (Malachi.Broker.domain_violations/2), computed from a single merged-metadata view.

unsubscribe(server, topic)

@spec unsubscribe(GenServer.server(), Malachi.Metadata.topic_name()) :: :ok

Removes the calling process's streaming subscription to topic. Returns :ok.