Expand description
Derive macros for the krabka broker.
krabka-macros turns one annotated declaration into code that the broker
would otherwise write out by hand two or three times. It is built on
[moxy], which supplies the token, syntax-tree, attribute-parsing and
template layers.
§RegisterMetrics
#[derive(RegisterMetrics)] goes on a struct of prometheus-client metric
handles. It adds two private methods to the struct:
fn unregistered() -> Selfconstructs every field: withDefault::default(), with a histogram overbuckets, or with thenewexpression.fn register(&self, registry: &mut Registry)registers a clone of every field that is notskip, in field order. The order of the fields is the order of the families in the text exposition.
Each field takes one optional #[metric(...)] attribute:
help = "..."— the help text. Every registered field needs one.name = "..."— the registered name. The default is the field name with a trailing_totalremoved, becauseprometheus-clientappends_totalto the name of a counter itself.buckets = EXPR— construct aHistogramoverEXPR. On aFamily<_, Histogram>field, construct the family so that each of its histograms usesEXPR.new = EXPR— construct the field withEXPR.skip— construct the field, but do not register it.
#[derive(krabka_macros::RegisterMetrics)]
struct Metrics {
#[metric(help = "Records received")]
records_total: Counter,
#[metric(help = "Request latency in seconds", buckets = [0.001, 0.01, 0.1])]
latency_seconds: Family<ApiLabel, Histogram>,
}§human_units
#[human_units] goes on a file-config struct, above its
#[derive(Deserialize, Serialize, JsonSchema)]. Each field typed
Option<Time>, Option<ByteSize> or Option<Ratio> (under any path
prefix) gets two attributes:
#[serde(default, with = "krabka_units::serde_units::human::option_X")], so that the TOML file writes the value as a string with a unit, such as"5s"or"1MiB".defaultis left out when the field already has a serdedefault.#[schemars(with = "Option<crate::file_config::schema_units::X>")], so that the JSON Schema names the unit in itsformat.
A field with its own serde with, deserialize_with or serialize_with
keeps its codec and gets neither attribute. Every other field passes through
unchanged.
#[krabka_macros::human_units]
#[derive(Deserialize, Serialize, JsonSchema)]
struct FileTimeouts {
request_timeout: Option<Time>,
segment_bytes: Option<ByteSize>,
}§dispatch_table!
dispatch_table! { ... } goes in the broker’s dispatch registry module. It
reads sections of CamelCase Kafka api names, each a label, a colon, and
comma-separated entries closed by ;:
krabka_macros::dispatch_table! {
context: Metadata, AddPartitionsToTxn => crate::txn::handlers::add_partitions_to_txn::handle;
sync_context: DescribeConfigs;
typed: ListGroups;
typed_group: Heartbeat;
typed_sync: DescribeAcls;
typed_infallible: ListConfigResources;
auth: CreateDelegationToken;
telemetry: PushTelemetry;
}From each name, such as CreateAcls, it derives the adapter
create_acls_adapter, the ApiKey::CreateAcls variant, the request module
krabka_protocol::owned::create_acls_request and its CreateAclsRequest
type. The handler is crate::handlers::create_acls::handle unless the entry
names another with => path.
contextandsync_contextadapters callhandler(broker, version, body, ctx)on the raw body; async_contexthandler returns its result instead of a future. A handler that needs the request’s correlation id reads it from the context.typedadapters decode the request, awaithandler(broker, request, version, ctx)for aResult<Response, BrokerError>, and encode the response withcrate::handlers::encode_response.typed_groupadapters do the same but decode throughcrate::handlers::decode_group_request.typed_syncadapters decode the request, callhandler(broker, &request, version, ctx)for aResult<Response, BrokerError>without awaiting it, encode the response, and wrap the result in a ready future.typed_infallibleadapters do the same for a handler that returns theResponseitself.telemetryadapters pass aTelemetryContextand wrap a synchronous result.authentries generate no adapter: the hand-written<name>_adaptermust be in scope.typed_own_spangenerates thetypedadapter without the span below, for a handler that keeps a#[tracing::instrument]of its own.
Every other generated adapter runs the decode, the handler and the encode
inside an info span named handle_<snake_name>, with api = "<Name>",
version and, for the context, sync_context and telemetry sections,
req_bytes = body.len(). On Err it emits, inside that span, the event
#[tracing::instrument(err)] would: ERROR with error set to the
error’s Display. The expansion calls ::tracing, so the calling crate
depends on it, and BrokerError implements Display.
It then emits fn register_dispatch_table(registry: &mut DispatchRegistry),
which registers every entry at the request schema’s FLEXIBLE_MIN and
panics on a duplicate registration. The expansion names Broker,
ApiVersion, CorrelationId, RequestContext, TelemetryContext,
BoxFuture, Bytes, BrokerError, ApiKey, DispatchEntry and
DispatchRegistry unqualified, so the calling module imports them.
§throttle_probes!
throttle_probes! { ... } is an expression for the KIP-219 throttle-echo
audit: an array of (API_KEY, probe as Probe), one per entry, keyed by the
response module’s generated API_KEY. Its sections are throttled (the
response has throttle_time_ms, which the probe sets to SENTINEL),
unthrottled (it has none), and legacy_split: Name = version, whose
probe encodes the kafka_3_6_2 response below version. The probes call
position, ApiVersion, ThrottlePosition, SENTINEL and Probe from
the calling module.
let probes: BTreeMap<_, _> = krabka_macros::throttle_probes! {
throttled: Metadata, ListOffsets;
unthrottled: SaslHandshake;
legacy_split: Produce = 3, Fetch = 4;
}
.into_iter()
.collect();§RuntimeOverlay
#[derive(RuntimeOverlay)] goes on a clap argument group whose fields
overlay the same-named fields of a config struct. The struct names that
config with #[overlay(target = Type)]. The derive adds
pub(crate) fn copy_into(&self, target: &mut Type), which assigns every
field in field order:
- by default,
target.f = self.f, so the field must beCopy; - with
#[overlay(refined)],target.f = self.f.map(|value| value.into_value()), for anOptionof a refined newtype; - with
#[overlay(clone)],target.f.clone_from(&self.f); - with
#[overlay(skip)], not at all.
A field that the target lacks, or whose type differs, fails to compile.
#[derive(clap::Args, krabka_macros::RuntimeOverlay)]
#[overlay(target = RuntimeFileConfig)]
struct RuntimeArgs {
cleaner_interval: Option<Time>,
#[overlay(refined)]
num_partitions: Option<PositiveI32>,
}§krabka_env
#[krabka_env] goes on a clap argument struct, above its
#[derive(clap::Args)]. Each field without an #[arg(...)] of its own gets
#[arg(long, env = "KRABKA_<FIELD>", ...)], where <FIELD> is the field
name in upper case and the rest follows from the field type:
| Field type | Added argument |
|---|---|
Option<Time> | value_parser = krabka_units::parse::positive_time |
Option<ByteSize> | value_parser = krabka_units::parse::positive_byte_size |
Option<Ratio> | value_parser = krabka_units::parse::positive_ratio |
Option<PositiveCount> | value_parser = krabka_broker::config_value::parse_positive_count |
Option<PositiveI16>, I32, I64 | the matching parse_positive_* |
Option<u32> | value_parser = clap::value_parser!(u32).range(1..) |
Option<i64> | value_parser = clap::value_parser!(i64).range(0..) |
Option<bool> | action = clap::ArgAction::Set |
Option<i16>, Option<i32>, Option<String>, Option<PathBuf> | none |
The type is matched as written, so a field of any other type, or of one
of these that needs another parser, keeps its own #[arg(...)]. Doc
comments stay where they are and remain the help text.
#[krabka_env(prefix = "BENCH_")] puts every variable under that prefix in
place of KRABKA_, so the field tls_ca_path reads BENCH_TLS_CA_PATH.
A field whose variable is not its name under the prefix keeps its own
#[arg(...)].
§RefinedNewtype
#[derive(RefinedNewtype)] goes on a tuple struct with one field, the value
a refined_type rule checks. It adds new(value) -> Result<Self, E>, which
validates through the rule, and a #[must_use] const getter that returns
the value. Both take the struct’s visibility. One #[refined(...)]
attribute configures it:
rule(<type>)— therefined_typerule, such asGreaterU32<0>. Required. It goes in parentheses because a generic type does not parse after=.getter = name— the getter’s name. The default isinto_value.string_error—newreturnsString, the rule’s error text, rather thanrefined_type::result::Error<T>.label = "..."— withstring_error,newreturns"<label>: <rule error>".default = EXPR— implementDefaultasSelf::new(EXPR), which panics whenEXPRbreaks the rule.from_str— implementFromStrwithErr = String: the field type’s ownparse, thennew, each error as its text.display— implementDisplayas the field’s.parse_fn = name— add a freefn name(&str) -> Result<Self, String>that parses the wayfrom_strdoes.quantity = ByteSize,TimeorFrequency— the field is a whole number of bytes, milliseconds or Hz, andnewtakes thekrabka_unitsquantity. It refuses a fraction, an infinity, or a count the field type cannot hold with"<label>: must be a whole number of <unit> that fits in <field type>", then hands the count to the rule. It impliesstring_error, addsTryFrom<quantity>throughnew, makesfrom_strparse withkrabka_units::parseanddisplayprint the quantity’shuman()text, and makesdefaulttake a quantity.quantity_getter = name— withquantity, add a getter that returns the value as its quantity.
#[derive(Clone, Copy, krabka_macros::RefinedNewtype)]
#[refined(rule(GreaterU32<0>), string_error, label = "fetch miss limit", getter = get,
default = 3, from_str, display)]
pub struct FetchMissLimit(u32);
#[derive(Clone, Copy, krabka_macros::RefinedNewtype)]
#[refined(rule(GreaterI32<0>), quantity = ByteSize, label = "fetch max", getter = bytes,
quantity_getter = size, default = mebibytes(8), from_str, display)]
pub struct FetchMax(i32);§PrimitiveCmp
#[derive(PrimitiveCmp)] goes on a tuple struct with one field. It
implements PartialEq and PartialOrd between the struct and the field’s
type in both directions, so that Seq(3) == 3_u64 and 2_u64 < Seq(3)
compile.
§EnumStr
#[derive(EnumStr)] goes on an enum whose variants each stand for one
fixed text, such as a metric label value or a config value. It adds a
#[must_use] const fn as_str(self) -> &'static str with the enum’s
visibility. The method takes &self when any variant has fields, which
its arm matches with { .. } or (..). One optional #[enum_str(...)]
attribute on the enum configures it:
case = "..."— how a variant name becomes its text:"snake_case","kebab-case","lowercase"or"UPPERCASE". Without it the text is the variant name unchanged.as_str = name— the method’s name.parseorparse = name— addfn parse(&str) -> Option<Self>, under that name, over the unit variants.all— addconst ALL: [Self; N], every variant in declaration order. Every variant must be a unit variant.label_value— implementprometheus_client’sEncodeLabelValueas the text. The crate must depend onprometheus-client.
A variant’s own #[enum_str(...)] takes name = "...", its text in place
of the cased name, and alias = "..." or alias("...", "..."), further
text that parse accepts.
#[derive(Clone, Copy, krabka_macros::EnumStr)]
#[enum_str(case = "snake_case", as_str = config_name, parse = from_config_name)]
pub enum AssignorKind {
Auto,
#[enum_str(alias = "highly-available")]
HighlyAvailable,
}§FieldDefaults
#[derive(FieldDefaults)] goes on a struct with named fields in place of a
hand-written impl Default that only fills each field with a fixed
expression. Each field takes one optional #[default(EXPR)] attribute and
starts as EXPR; a field without one starts as Default::default(). The
expression is evaluated each time default() runs, so it may call
functions, and it is written out as given, so it must name what it uses in
the scope of the struct. The derive adds no bounds: a generic struct
writes the bounds its fields need on the struct itself.
The name differs from Default because the standard derive owns the bare
name and its own #[default] attribute on enum variants. Do not derive
both on one struct.
#[derive(krabka_macros::FieldDefaults)]
pub struct FreezeConfig {
#[default(Duration::from_secs(30))]
pub poll_interval: Duration,
#[default(true)]
pub enabled: bool,
pub owners: Vec<String>,
}§cli_main!
cli_main!(path) goes at the top level of an operator binary’s
main.rs, in place of its main. It expands to a #[tokio::main]
async fn main() that installs a tracing_subscriber::fmt() subscriber
filtered by RUST_LOG, or at info when RUST_LOG is unset or does not
parse, and writes to stderr. stdout stays the binary’s own, so a report that
a script parses, such as krabka-restore --report json, holds no log lines.
It then awaits path::run_from_args(std::env::args_os()) and exits the
process with the i32 it returns. Any arguments after the path, such as
flavor = "multi_thread", go to #[tokio::main(...)] unchanged. The
binary depends on tokio with its macros feature and on
tracing-subscriber.
krabka_macros::cli_main!(krabka_restore, flavor = "multi_thread");§Group timings and epoch values
#[derive(GroupTimingConfig)] adds for_group and the test-only
assigning_at_once constructor to a membership config. Its named timing
fields and bounds remain visible on the struct. Select the stored keys with
#[group_timing(session = KEY, heartbeat = KEY, assignment = KEY)].
The implementation uses the broker’s existing group timing parser and bounds.
#[derive(GroupLifecycle)] adds epoch changes, session expiry queries, and
member eviction to a group state. Select its existing policy with
#[group_lifecycle(protocol = "consumer" | "share" | "streams")].
Consumer and share groups get assignment delay checks; consumer and streams
groups get rebalance fencing. A successful streams epoch change marks the
group dirty. All removals call the state’s own remove_member method.
#[derive(FlexibleEpochValue)] adds the version-zero epoch codec to a
named struct with an i32 epoch followed by an i64 tagged value.
#[epoch_value(record = "RecordName")] supplies the decoding error name.
A zero tagged value is omitted on encode and restored on decode.
#[flexible_value(record = "KafkaRecord")] goes on an inherent impl with
private encode_fields(self or &self, buf: &mut BytesMut) and
decode_fields(buf: &mut &[u8]) -> Result<Self, BrokerError> methods.
It retains those methods and adds the public framed codecs. The field
methods keep wire order explicit; the encoder retains its receiver ownership.
#[derive(KafkaUuid)] adds display, parsing, and string serde to a UUID
wrapper. It calls the declaration’s existing encode, decode, and
KafkaUuidError helpers. Display retains the formatter’s padding rules.
§Handler admission
#[voter_admission(request = TYPE, api = KEY, gate = FUNCTION, refusals = EXPR)]
goes on an ordinary async handle(broker, version, req_bytes, ctx) function.
It runs the shared voter prelude before the operation body and returns early
for an encoded refusal. The body retains its original tokens and scopes.
share_state_handler!(Request, Response, TopicRow, PartitionRow, serve)
declares a share-state handler. A denied request returns an error row for
every requested partition in request order. An allowed request calls serve
with the share coordinator and a fresh metadata snapshot.
§Storage methods
#[derive(SparseIndex)] adds the existing loader and file operations to a
sparse index. #[sparse_index(kind = "offset" | "time")] selects its raw
entry layout and I/O target. The loader retains its monotonic-entry checks.
#[index_open] adds the same tracing span to other ordinary index loaders.
#[remote_operation] traces an ordinary remote-storage SPI method. Its
existing method name selects the span level and fields. It retains the
native method body, parameter names, and synchronous signature.
#[model_type] attaches the existing proof and runtime derives to an
ordinary struct or enum. proof(...) and runtime(...) replace only the
selected derive list, preserving its path order and the declaration tokens.
#[sendfile_cfg] gates one ordinary item with the fetch path’s shared
platform predicate. Apply it separately to imports, exports and methods.
#[decode_dispatch(request = TYPE, binding(req))] decodes a request before
the handler’s native body. binding(mut req) retains an owned mutable
request. decoded_dispatch! accepts the same explicit function, with
[request = TYPE, binding(req)] after its parameters and before its return type.
#[extension_wire(name = "SPAN", api = "API", level = "info")] adapts an
ordinary operation with broker, owned request, version and context parameters.
It emits the raw five-argument entry point and traces both decoding and the
native operation. "debug" retains the marker API’s tracing level.
worm_object_set! emits one ordered admission rule for its Model and
Runtime declarations. Function headers and count observations stay native;
proof projections remain inside the caller’s Pearlite scope.
§Direct runtime and response tables
set_runtime! applies optional fields in table order. Each group names a
validator. plain assigns directly; duration validates positive time
and converts it to Duration. A mapping selects a different target field,
and in supplies the validator’s range argument. An absent field retains
the target value. A validation error stops later assignments.
krabka_macros::set_runtime! {
runtime => config;
plain: enabled;
duration: heartbeat_interval;
in_range: max_size => group.max_size in 1..=1000;
}response_errors! implements the caller’s error traits for external
protocol types. Its four sections are response, code_response, row
and resource_row. Each section contains type paths separated by commas
inside braces. A resource row also implements ErrorRow, so
each resource type is listed once. The traits remain in the caller’s scope.
metric_registry_fixture!(metrics, output, guard; expect("encode"))
renders directly into the named bindings. The caller retains the registry
lock until it drops guard. Use unwrap() to select that result policy.
broker_config! { pub struct BrokerConfig { ... } } constructs the broker
config in one expansion. The fields stay in declaration order with their
original tokens, documentation and attributes. It adds Debug and Clone.
#[membership_defaults] goes before FieldDefaults on a membership config.
It assigns the shared defaults to the six named session and heartbeat
fields. An existing default on one of those fields is an input error.
#[derive(ShareStateValue)] writes scalar fields in declaration order,
followed by state batches and the delivery-count tag. The existing batch,
version and tag helpers retain their byte layouts and validation. Supply
method documentation with #[share_state(encode_doc = "...", decode_doc = "...")].
#[derive(FlexibleArrayValue)] wraps one Vec field in version-zero
framing. #[array_value(record = "RecordName", encode = writer, decode = reader)]
selects the existing array callbacks and decoding error name.
tagged_wire! and unthrottled_wire! construct exhaustive struct values.
Both supply an empty tagged-field set; the second supplies zero throttle.
Every remaining field stays required. Struct updates with .. are refused,
and field expressions retain their original tokens and evaluation order.
Macros§
- acknowledgement_
batches - assignment_
dirs_ fixture - async_
write_ delegate - authorization_
refusal - bench_
failures - bench_
main - benchmark_
latency_ fixture - benchmark_
scenario_ fixture - bind_
retry_ fixture - bootstrap_
parser_ fixture - bound_
start_ fixture - bounded_
bfs - bounded_
unit_ modules - break_
glass_ golden_ id - broker_
config - broker_
test_ setup - capture_
layer_ fixture - cli_
main - compacted_
batch - compaction_
record_ fixture - compression_
bytes_ fixture - config_
alter_ handler - connect_
cli_ client - connection_
policy - consumer_
fetch_ fixture - control_
marker_ fixture - controller_
request - create_
topic_ fixture - decoded_
dispatch - delete_
topic_ request - described_
group_ rows - directory_
tree_ fixture - dispatch_
capacity_ parser - dispatch_
table - duration_
cases_ fixture - duration_
parser_ fixture - empty_
endpoint_ voters - empty_
match_ acl_ filter - ephemeral_
port_ fixture - epoch_
checkpoint_ failure - epoch_
log_ view - epoch_
millis_ fn - epoch_
record_ batch_ fixture - evicted_
local_ segments - export_
segment_ data_ fixture - file_
region_ fixture - finalized_
feature_ fixture - fixture_
constructors - flag_
metadata_ fixture - format_
directories_ fixture - forwardable_
requests - frame_
max_ parser - frame_
prefix_ fixture - freeze_
refusal_ rules - guard_
response - heartbeat_
status - hydrate_
member_ epochs - index_
floor_ lookup - index_
seed_ helpers - java_
string_ producer_ source - jvm_
checkpoint_ dump_ fixture - jvm_
principal_ golden - keyed_
record_ batch_ fixture - legacy_
policy_ fixture - member_
identity_ fixtures - member_
key_ boundaries - metric_
registry_ fixture - minio_
fixture - model_
properties - model_
runner - model_
transition - named_
acl_ gates - offset_
fetch_ rows - otlp_
exporter - parsed_
cli_ entrypoint - partition_
gate - patterned_
bytes_ fixture - private_
dispatch_ table - producer_
batch_ fixture - projection_
identity - quorum_
epoch_ encoder - quorum_
epoch_ requests - reassignment_
fields - rebuild_
reconciliation_ group - reconciliation_
actions - reconciliation_
next_ state - record_
batch_ fixture - record_
header_ text - record_
limit_ fixture - record_
strings - records_
payload_ match - refused_
fetch_ partition_ fixture - registration_
feature_ projection - remote_
metadata_ missing - remote_
segment_ check - remote_
segment_ fixtures - remote_
segment_ transition_ matrix - remote_
started_ segment - replication_
target_ guard - request_
frame_ fixture - request_
test_ identity - required_
topics - response_
errors - s3_
archive_ config_ fixture - schedule_
commit_ changes - scram_
client_ first - scram_
client_ proof_ fixture - scram_
users_ fixture - sendfile_
match - sendfile_
platform - set_
runtime - share_
consumption_ driver - share_
dlq_ meters - share_
first_ fetch - share_
fixture_ schemas - share_
group_ denial - share_
partition_ results - share_
refusal_ matrix - share_
state_ handler - share_
state_ request - share_
state_ results - simulation_
actions - simulation_
earliest_ timer - simulation_
heartbeat - simulation_
step - simulation_
transport - single_
partition_ produce_ fixture - single_
replica_ partition_ fixture - snapshot_
member_ maps - snapshot_
node_ fixture - snapshot_
topic_ fixture - sorted_
index_ cursor - splitmix64_
step - string_
mutations - supported_
feature_ fixture - supported_
features_ fixture - sweep_
loop - tagged_
wire - test_
authorizer - throttle_
probes - timer_
hooks - timestamped_
batch - token_
refusal_ result - topic_
record_ fixture - unix_
millis_ fixture - unthrottled_
wire - validate_
archive_ rows - vector_
request_ fixture - voter_
endpoints - wal_
capture_ topic_ fixture - wire_
codec_ helpers - wire_
codes - wire_
test_ driver - worm_
object_ set - worm_
segment_ fixture
Attribute Macros§
- argv_
entrypoint - bootstrap_
cli_ fields - config_
table - decode_
dispatch - delegate_
authorizer - extension_
wire - finished_
selection - flexible_
value - flush_
group_ records - gcs_
fields - hex_
bytes_ identity - human_
units - index_
open - krabka_
env - membership_
defaults - metadata_
log_ delegate - missing_
remote_ reads - model_
type - object_
ops - object_
store_ config - object_
store_ delegate - partition_
spawn_ parameters - reaper_
task - reconciliation_
fields - remote_
operation - retained_
timestamp_ bounds - round_
robin_ site_ lemma - runtime_
policy_ fields - s3_
fields - scalar_
id - sendfile_
cfg - share_
produce_ parameters - share_
request_ identity - share_
state_ operation - test_
scalar - token_
dispatch - topic_
filter - transport_
errors - voter_
admission
Derive Macros§
- Background
Controllers - EnumStr
- Field
Defaults - Flexible
Array Value - Flexible
Epoch Value - Group
Lifecycle - Group
Record Key - Group
Timing Config - Kafka
Uuid - Model
Epoch Ledger - Node
Endpoints - Partition
Message - Primitive
Cmp - Reconciliation
Action - Reconciliation
Group - Reconciliation
Member - Refined
Newtype - Register
Metrics - Replay
Member Maps - Runtime
Overlay - Scheduled
Loads - Share
State Value - Sparse
Index - Unit
Schema - Wire
State Batch