Malachi.Consumer.GroupCoordinator (malachi v0.22.7)

View Source

Tracks the members of consumer groups and assigns each topic's ranges across them (via Malachi.Consumer.Assignment), so members consume in parallel: one range per member.

A group is keyed by {group, topic}. Members join, then heartbeat to stay alive; a member that stops heartbeating for session_ms is evicted (a dead consumer). Rebalancing is eager: any membership change: join, leave, eviction - or a change in the topic's ranges recomputes the whole assignment and bumps a generation counter; a member re-reads its assignment on the next heartbeat and sees the new generation, its signal to take over its (possibly changed) ranges. Because the assignment is deterministic and sticky, a rebalance moves few ranges.

This is the coordinator logic as a single GenServer, reached through seams (:clock, :ranges_fun) so it is testable without a cluster; which node coordinates which group (or replicating the membership) is a separate wiring concern. Member state is soft: a coordinator restart just makes members re-join.

Options

  • :ranges_fun - (topic -> [range_id]), the topic's current ranges (default fn _ -> [] end)
  • :clock - (-> integer_ms) monotonic clock (default System.monotonic_time(:millisecond))
  • :session_ms - evict a member silent this long (default 30_000)
  • :tick_ms - reconcile interval: evict + rebalance (default 10_000)
  • :name - optional registered name

Summary

Functions

Reads member's current assignment without refreshing its session.

Returns a specification to start this module under a supervisor.

Refreshes member's session and returns its current {:ok, generation, ranges}, or {:error, :unknown_member} if it was evicted (the caller must join/4 again).

Joins member to {group, topic}, triggering a rebalance. Returns {:ok, generation, ranges}, or {:error, :not_owner} if this node no longer owns the topic's coordination (the caller re-resolves).

Removes member from {group, topic}, triggering a rebalance.

The fetch-path entry point (implicit membership): registers member if new (a rebalance) or just refreshes its session if known (no rebalance), and returns its {:ok, generation, ranges}. Called on every fetch, so a member stays alive by fetching and needs no separate heartbeat. Returns {:error, :not_owner} if this node no longer owns the topic's coordination (the caller re-resolves).

Runs one reconcile (evict timed-out members + rebalance) immediately. Also fired on the timer.

Types

member()

@type member() :: term()

reply()

@type reply() ::
  {:ok, non_neg_integer(), [term()]} | {:error, :unknown_member | :not_owner}

Functions

assignment(server, group, topic, member)

@spec assignment(GenServer.server(), term(), term(), member()) :: reply()

Reads member's current assignment without refreshing its session.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

heartbeat(server, group, topic, member)

@spec heartbeat(GenServer.server(), term(), term(), member()) :: reply()

Refreshes member's session and returns its current {:ok, generation, ranges}, or {:error, :unknown_member} if it was evicted (the caller must join/4 again).

join(server, group, topic, member)

@spec join(GenServer.server(), term(), term(), member()) :: reply()

Joins member to {group, topic}, triggering a rebalance. Returns {:ok, generation, ranges}, or {:error, :not_owner} if this node no longer owns the topic's coordination (the caller re-resolves).

leave(server, group, topic, member)

@spec leave(GenServer.server(), term(), term(), member()) :: :ok

Removes member from {group, topic}, triggering a rebalance.

poll(server, group, topic, member)

@spec poll(GenServer.server(), term(), term(), member()) :: reply()

The fetch-path entry point (implicit membership): registers member if new (a rebalance) or just refreshes its session if known (no rebalance), and returns its {:ok, generation, ranges}. Called on every fetch, so a member stays alive by fetching and needs no separate heartbeat. Returns {:error, :not_owner} if this node no longer owns the topic's coordination (the caller re-resolves).

reconcile_now(server)

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

Runs one reconcile (evict timed-out members + rebalance) immediately. Also fired on the timer.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()