Skip to content

Fail clearly on bad storage config, lake-without-registry, and unauthenticated clients - #850

Merged
solace-aross merged 5 commits into
mainfrom
fix/sol-155256-fail-clearly
Oct 7, 2026
Merged

solace-aross merged 5 commits into
mainfrom
fix/sol-155256-fail-clearly

Conversation

@solace-aross

@solace-aross solace-aross commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Updated after review (0766f20): Builder::storage stays infallible in both crates; the maintenance interval options are parsed and stripped by the broker only. Intervals parse with human_units then humantime; bare numbers, zero and values over 365 days are rejected. The pre-auth rejection is logged at ERROR with the client address in broker.rs, and the frame.rs lines are debug!. Details below that disagree with this predate the update.

Summary

The broker used to panic or hang on bad input instead of failing clearly. This PR makes it fail fast with a specific message in every case, and logs a clear warning instead of silently hanging when a client sends a request before authenticating.

Problems and fixes

  1. A maintenance_interval/transaction_maintenance_interval storage-URL option that doesn't parse, or parses to zero, used to be silently ignored (defaulting), or later panicked in tokio::time::interval (which panics on a zero period). Builder::storage now validates both options up front and returns an error naming the option and value; the CLI prints a specific message and exits non-zero instead of a panic or a silent wrong default.

  2. Every storage engine now rejects a query option it doesn't recognise (a typo like maintainance_interval, or an option that belongs to a different engine, e.g. vacuum_into on a non-sqlite engine) instead of silently passing it through. Postgres already rejected unknown options natively via tokio-postgres's own parser; added a regression test for it instead of new code.

  3. A data lake subcommand (iceberg, delta, parquet) without --schema-registry used to panic on .unwrap(). --schema-registry is now validated synchronously in Arg::build() before the broker starts, so the error comes from argument parsing, not a panic.

    The first design for this made --schema-registry a subcommand-local argument duplicated under each lake subcommand. That's wrong: clap only parses a subcommand-local flag positioned after the subcommand name, and this repo's own usage (justfile, compose.yaml, docs) puts --schema-registry before the subcommand. A reproduction confirmed the ordering bug. The fix instead keeps --schema-registry as a single top-level arg (global = true), which parses correctly in both positions. schema_registry_before_subcommand_parses is a regression test for this specific ordering.

  4. A client that sends a request before completing authentication used to have its connection silently dropped, with no indication of why, and in testing this hung for the test harness's full retry window. Checked this against Apache Kafka at the 3.9.0 source level (not a live comparison): Kafka's own SASL-handshake enforcement behaves the same way (closes the connection if the first request isn't a SASL handshake), and the original "hang" was the test harness's own timeout being shorter than the client's default retry window, not a Nisshi bug. The fix: log one specific warn! (client address via the existing tracing span, api_key, api_name) at the point of rejection, instead of two redundant generic error! dumps further down the stack.

Duration grammar change (disclosure)

Validating maintenance_interval/transaction_maintenance_interval required picking a parser. These moved from humantime's grammar to human_units's, to match the grammar already used by sibling options (batch_max_delay, busy_timeout) in the same URL query-string context. human_units's grammar is narrower:

  • Before (humantime): compound durations (1h30m) and more unit spellings (min, sec, us) parsed.
  • After (human_units): only <integer><ns|μs|ms|s|m|h|d> or a bare integer (seconds).

Concretely: maintenance_interval=5min and maintenance_interval=1h30m parsed fine before this change and now produce a startup error. This is a real, user-visible behavior change beyond the ticket's literal ask (which wanted validation added, not necessarily a narrower grammar). It's likely an improvement regardless — anyone currently using a compound duration got a silent wrong default before, and now gets an immediate, clear startup error — but it's a different outcome for the exact same input, so reviewers should know about it going in.

Other known, accepted gaps (disclosed, not fixed here)

  • nisshi-service/src/stream.rs:816 still logs a generic connection-ending error! in addition to the new, specific warn!. This is pre-existing, not introduced by this ticket, and isn't being touched: that layer's error type is fully generic/opaque, and specializing it would need a broader change affecting the proxy and client error paths too. Net result as shipped: one new specific WARN plus the pre-existing generic ERROR, down from two generic ERRORs before this change.
  • nisshi-storage-sql/src/limbo.rs:1075 (and :4070) call .unwrap() on a non-UTF-8 database path. Low severity (experimental, turso-backed engine); flagged here rather than split into a separate ticket.

Follow-up tickets filed

  • Follow-up — a recognised storage option with a value that fails to parse still falls back to a default silently on dynostore and the sqlite engine (separate, already-scoped-out gap from this ticket's validation).
  • Follow-up — --listener-url with a non-IP hostname silently falls back to the unspecified (wildcard) address instead of resolving the hostname or failing clearly, in nisshi-broker/src/broker.rs's Broker::listen. An operator who configures a specific listen interface silently gets the wildcard address instead, which is a misconfiguration-tolerated-as-network-exposure-surface issue.

Review history

This is round 2 of 2 (capped). Round 1 found a dead UnsupportedStorageUrl(Url) error variant in nisshi-storage; round 2 found a second, separate dead variant of the same name in nisshi-broker's own Error enum (also from the tansu rename, zero constructors) — deleted in this round. Round 2 also found the two new storage error variants were falling through to the CLI's generic "Unknown error occurred" catch-all instead of a specific message — added two match arms following the existing LakeRequiresSchemaRegistry pattern.

Test output

Targeted suites (nisshi-cli, nisshi-storage + nisshi-storage-sql/-dynostore/-null/-slatedb, nisshi-broker including unauthenticated::*, nisshi-service), run via cargo nextest run --workspace --all-features --all-targets filtered to these packages: 573 passed, 0 failed (1 slow, 39 skipped by backend/feature gating).

unauthenticated::* specifically:

PASS [0.110s] nisshi-broker::it unauthenticated::api_versions_exempt_before_authentication
PASS [0.111s] nisshi-broker::it unauthenticated::request_before_authentication_closes_connection
Summary: 2 tests run: 2 passed, 0 skipped

cargo clippy --workspace --all-features --all-targets -- -D warnings: clean (0 errors).
cargo fmt --all -- --check: clean.

The new unauthenticated.rs test was mutation-tested twice: once reproducing the original "hangs" bug (replacing the rejection with a never-resolving future) — confirmed the test correctly fails after a bounded timeout instead of hanging forever; once reproducing a "silently bypasses auth" bug (disabling the check entirely) — confirmed the test correctly fails because it gets back a real, unexpected successful response instead of a closed connection. Both mutations correctly left api_versions_exempt_before_authentication passing, since ApiVersions is supposed to work regardless of authentication state.

Live binary runs confirmed both new CLI error messages print clearly and exit non-zero, e.g.:

$ nisshi broker --storage-engine 'memory://nisshi/?maintenance_interval=0s'
ERROR nisshi: storage option maintenance_interval=0s is not a valid non-zero duration (expected e.g. 10m, 90s, 500ms)

$ nisshi broker --storage-engine 'memory://nisshi/?typo_option=1'
ERROR nisshi: storage option typo_option is not recognised by the memory engine

🤖 Generated with Claude Code

https://claude.ai/code/session_01D2qdgPhMyLGZN5dLR8CVHs

@sgamelin sgamelin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this. The lake-without-registry fix and the global = true reasoning look right, and the pre-authentication change matches Kafka. I checked that at 3.9.1, the version our descriptors pin, rather than 3.9.0: SaslServerAuthenticator.handleKafkaRequest throws IllegalSaslStateException for any request other than ApiVersions or SaslHandshake, and the connection closes (SaslServerAuthenticator.java#L520-L523).

Main points, inline:

  1. None of the new storage-option validation has a test.
  2. nisshi_storage::Builder::storage accepts the two interval options, and then every engine rejects them.
  3. The duration grammar change breaks configs that start today. The description's reason for it isn't accurate.
  4. The default log filter hides the new warn!.

Also:

  • CHANGELOG: [Unreleased] is empty. This PR changes startup in three ways users will notice: unknown storage options are fatal, the duration grammar is narrower, and a bare integer now means seconds instead of being ignored. It also makes Builder::storage fallible in two published crates. Could you add ### Changed entries?
  • Postgres: tokio-postgres rejects the unknown option, so the error reaches the generic "Unknown error occurred" arm in nisshi.rs, not the new message. That's fine, but the new test in pg.rs passes on any error from Postgres::builder. Asserting that the error names vacuum_into would pin what it claims.
  • --schema-registry: schema_registry_before_subcommand_parses also passes without global = true, because the flag was already top-level. What global = true adds is the flag after the subcommand (parquet --location ... --schema-registry ...), and a test for that order would pin it.

Merge coordination with other open PRs:

  • #844 adds its own nisshi_cli::Error::Server(..) arm to the same match in nisshi.rs. Git merges both cleanly. But #844's arm comes first and ends in _ =>, so this PR's arm becomes unreachable, and unreachable_patterns then fails clippy with -D warnings. Whichever PR merges second has to fold both inner matches into one Server arm and choose one fallback: #844 prints the inner error, and this PR prints err.
  • #845 and #826: #845 (tests/it/produce_acks_zero.rs) and #826 (tests/it/common/wire.rs) add Broker::builder()...storage(...) calls without ?. Git won't flag a conflict there, so whichever lands second has to add the ?.
  • #849 conflicts in the nisshi_storage::Error enum. Keep both sets of new variants and drop UnsupportedStorageUrl. #849 also deletes get_telemetry_subscriptions.rs, whose doctest this PR edits, so take the deletion. #835 rewrites delete_records.rs and removes the test this PR edits, so take #835's file. All three resolutions are mechanical.

Comment thread nisshi-storage/src/lib.rs
Comment thread nisshi-storage/src/lib.rs Outdated
Comment thread nisshi-storage/src/lib.rs Outdated
Comment thread nisshi-service/src/frame.rs Outdated
@solace-aross
solace-aross force-pushed the fix/sol-155256-fail-clearly branch from a7aaae2 to 0766f20 Compare October 6, 2026 09:30
@solace-aross

Copy link
Copy Markdown
Collaborator Author

Summary points, in 0766f20:

@solace-aross
solace-aross force-pushed the fix/sol-155256-fail-clearly branch from 7fa5dfd to ed229a9 Compare October 7, 2026 12:30
@solace-aross
solace-aross requested a review from sgamelin October 7, 2026 12:31
solace-aross and others added 5 commits October 7, 2026 10:05
…enticated clients

SOL-155256: the broker used to panic or hang instead of failing fast.

- Builder::storage (nisshi-broker and nisshi-storage) now rejects a
  maintenance_interval/transaction_maintenance_interval that doesn't parse
  or is zero, naming the option and value, instead of silently defaulting
  or later panicking in tokio::time::interval.
- Every storage engine factory (memory, S3, GCS, sqlite/libsql, turso,
  slatedb, null) now rejects a storage URL query option it doesn't
  recognise, naming the key and scheme. Postgres already rejected unknown
  options natively (tokio-postgres's own parser); added a regression test
  instead of new code.
- A data lake subcommand (iceberg/delta/parquet) without --schema-registry
  now fails with a clear error from a synchronous, directly unit-tested
  check in Arg::build(), instead of panicking on .unwrap(). Keeps
  --schema-registry as a single, non-duplicated field (global = true) so
  it still parses before or after the subcommand.
- A request before authentication now logs one specific warn! (api_key,
  api_name) at the point of rejection, instead of two redundant generic
  error! dumps; added a real-socket test proving the connection actually
  closes promptly rather than hanging.

Filed SOL-155358 to track the separate, already-scoped-out gap: a
recognised storage option with a value that fails to parse still falls
back to a default silently, on dynostore and the sqlite engine.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2qdgPhMyLGZN5dLR8CVHs
Signed-off-by: Andrea Ross <168456375+solace-aross@users.noreply.github.com>
…LI messages

- Delete nisshi-broker::Error::UnsupportedStorageUrl(Url), a second dead
  variant from the tansu rename with zero constructors, left over after
  round 1 only caught the nisshi-storage copy.
- Add CLI match arms for InvalidStorageOptionValue and
  UnrecognizedStorageOption so both surface a clear, specific message
  instead of falling through to the generic "Unknown error occurred"
  Debug dump, matching the existing LakeRequiresSchemaRegistry pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2qdgPhMyLGZN5dLR8CVHs
Signed-off-by: Andrea Ross <168456375+solace-aross@users.noreply.github.com>
Signed-off-by: Andrea Ross <168456375+solace-aross@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2qdgPhMyLGZN5dLR8CVHs
… with the peer

The broker reads and removes maintenance_interval and
transaction_maintenance_interval from the storage URL in build(), so
nisshi_storage::Builder::storage and nisshi_broker::Builder::storage stay
infallible and no engine sees a key that the broker accepted. An interval
parses with human_units, then humantime; a bare number, zero, an
unparseable value or a value over 365 days is rejected.

The broker logs a pre-authentication rejection at ERROR with the client
address. Adds tests for both helpers, broker startup on memory:// and
sqlite://, --schema-registry after the subcommand, and the Postgres
error naming the option. Adds CHANGELOG entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011P97dHLhTRdJ35fMJFpYPg
Signed-off-by: Andrea Ross <168456375+solace-aross@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011P97dHLhTRdJ35fMJFpYPg
Signed-off-by: Andrea Ross <168456375+solace-aross@users.noreply.github.com>
@solace-aross
solace-aross force-pushed the fix/sol-155256-fail-clearly branch from ed229a9 to dc5bb6d Compare October 7, 2026 14:05

@sgamelin sgamelin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this round. Every earlier point is addressed in 7d37ca0:

  • Tests: each of the four mutations I listed now fails a test, apart from part of the last one (inline). The new tests pass at dc5bb6d.
  • Interval options: they now live only in nisshi-broker, and both Builder::storage methods are infallible again, as on main.
  • Duration grammar: 1h30m and 5min parse as on main, a bare number fails, and the 365-day cap stays well below tokio's Instant overflow.
  • Logging: a rejected pre-authentication connection now logs one ERROR line with the peer address under the default filter.
  • The rest: the CHANGELOG entries, the pg assertion and the after-subcommand test are in.

Approving. Three optional points, inline.

Merge coordination with other open PRs:

  • #844: still needs the two Server arms in nisshi.rs folded into one, as in my earlier review.
  • #845: conflicts in the connection-error match in broker.rs. Keep both new arms.
  • #827: conflicts in the pg.rs tests. Keep both tests.
  • #849: still conflicts in nisshi_storage::Error. Keep both sets of variants.
  • #826 and #835: now merge cleanly.

Comment thread CHANGELOG.md
Comment thread nisshi-broker/src/broker.rs
Comment thread nisshi-storage-sql/src/lite/factory.rs
@solace-aross
solace-aross added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 786eb0d Oct 7, 2026
24 checks passed
@solace-aross
solace-aross deleted the fix/sol-155256-fail-clearly branch October 7, 2026 18:05
dennis-brinley pushed a commit to dennis-brinley/nisshi that referenced this pull request Oct 9, 2026
Closes nisshi-io#886.

Keeps secrets and record data out of logs, at every log level.

## Changes

**Generated `Debug` hides secret fields.** `SENSITIVE_FIELDS` in
`nisshi-sans-io/build.rs` lists fields by message, struct and field
name. For a struct with a listed field, the generator leaves `Debug` out
of the derive and writes a `Debug` impl that matches the derived output,
except that the listed field reads `[hidden]`. This covers the public
and the internal (mezzanine) structs, so logging a message, a `Body` or
a `Frame` hides it. Field types, serde, `PartialEq` and `Hash` don't
change.

| Message | Struct | Field |
|---|---|---|
| `SaslAuthenticateRequest` | same | `AuthBytes` |
| `SaslAuthenticateResponse` | same | `AuthBytes` |
| `AlterUserScramCredentialsRequest` | `ScramCredentialUpsertion` |
`SaltedPassword` |
| `CreateDelegationTokenResponse` | same | `Hmac` |
| `DescribeDelegationTokenResponse` | `DescribedDelegationToken` |
`Hmac` |
| `ExpireDelegationTokenRequest` | same | `Hmac` |
| `RenewDelegationTokenRequest` | same | `Hmac` |
| `AlterConfigsRequest` | `AlterableConfig` | `Value` |
| `IncrementalAlterConfigsRequest` | `AlterableConfig` | `Value` |

The build fails when an entry matches no field. It also fails on a
`bytes` field that is in neither `SENSITIVE_FIELDS` nor
`LOGGABLE_BYTES_FIELDS`, which lists the eight `bytes` fields that may
be logged (the SCRAM salt, group protocol metadata and assignments, and
client telemetry). A descriptor update that adds a `bytes` field
therefore needs a decision before it builds. The build also fails if a
listed field is tagged, because the internal struct keeps a tagged
field's bytes in `tag_buffer`, which the generated `Debug` can't hide.

**Raw bytes are logged by length.** Frame dumps in `nisshi-service`,
`nisshi-client` and `Frame::request`/`Frame::response`, and the
`serialize_bytes` spans (which recorded every bytes field at INFO), now
log a length. The decoder no longer logs each `u8` it reads, which wrote
a compressed record's key and value one byte per line, and the snappy
inflator no longer logs its input. String fields are logged by length
when they're encoded or decoded, because a string can hold a config
value. When the broker decodes a request it logs the api key, version
and correlation id read from the frame header, so a frame that fails to
decode can still be identified.

**Records are logged by length.** `deflated::Batch` writes
`record_data_len` in place of `record_data`, so a logged produce or
fetch message doesn't carry record contents. `Record` writes `key_len`
and `value_len`, and a record `Header` writes its key and `value_len`.
Record decoding, the dynostore batch decode and the SQL record reads log
lengths. A failing test that compares records now shows lengths for keys
and values; the other fields still show.

**Storage.** `ScramCredential`'s `Debug`, and slatedb's stored form of
it, show `salt` and `iterations` and hide `stored_key` and `server_key`.
The libSQL and Turso query helpers log the SQL without its parameters,
which hold record data and credentials. The SQL produce paths log key
and value lengths when an insert fails, and the libSQL compaction spans
skip the record key. The null backend's SCRAM upsert span skips the
credential.

**Schemas and lake tables.** Schema validation and Avro, JSON and
protobuf conversion log the field, the schema and the kind of value, not
the value. The error variants that held a value (`AvroToJson`,
`InvalidValue`, `JsonToAvro`, `JsonToAvroFieldNotFound`,
`UnsupportedSchemaRuntimeValue`) now hold its kind, and the
`todo!`/`unimplemented!` messages for unsupported Avro types name the
kind. An Avro record that fails to read or write against its schema
becomes the new `Error::AvroRecord`, which holds no detail, because the
`apache_avro` error for a record writes the record's values into its
message. These are breaking changes to `nisshi_schema::Error`, listed in
the CHANGELOG.

## Comparison with Kafka 3.9.1

Logs aren't visible to clients, so this changes nothing a client can
observe.

- **SASL auth bytes:** matches. Kafka handles SASL frames in the
authenticator and never passes them to the request logger
([SaslServerAuthenticator.java#L425-L504](https://github.com/apache/kafka/blob/3.9.1/clients/src/main/java/org/apache/kafka/common/security/authenticator/SaslServerAuthenticator.java#L425-L504)).
- **`SaltedPassword` and `Hmac`:** stricter than Kafka. Kafka's request
logger redacts only config values
([RequestChannel.scala#L186-L212](https://github.com/apache/kafka/blob/3.9.1/core/src/main/scala/kafka/network/RequestChannel.scala#L186-L212)),
so it logs these. We follow Kafka's own `DelegationToken.toString`,
which writes `hmac=[*******]`
([DelegationToken.java#L74-L79](https://github.com/apache/kafka/blob/3.9.1/clients/src/main/java/org/apache/kafka/common/security/token/delegation/DelegationToken.java#L74-L79)).
- **Config values:** stricter than Kafka. Kafka's request logger hides
an `AlterConfigs` or `IncrementalAlterConfigs` value only when the
config it names is sensitive
([RequestChannel.scala#L186-L212](https://github.com/apache/kafka/blob/3.9.1/core/src/main/scala/kafka/network/RequestChannel.scala#L186-L212)).
We keep no list of sensitive config names, so we hide every value, topic
configs included.
- **Raw frames:** matches. Kafka logs request and response sizes, not
their bytes
([RequestChannel.scala#L436-L450](https://github.com/apache/kafka/blob/3.9.1/core/src/main/scala/kafka/network/RequestChannel.scala#L436-L450)).
- **Records:** matches. Kafka's generated JSON converters write a
records field as its size
([JsonConverterGenerator.java#L404-L420](https://github.com/apache/kafka/blob/3.9.1/generator/src/main/java/org/apache/kafka/message/JsonConverterGenerator.java#L404-L420)).
- **Placeholder:** `[hidden]`, as Kafka's `Password.HIDDEN`
([Password.java#L24](https://github.com/apache/kafka/blob/3.9.1/clients/src/main/java/org/apache/kafka/common/config/types/Password.java#L24)).

## Tests

- `nisshi-sans-io/tests/it/redact.rs`:
- For each hidden field, formats the message, its `Body` and a `Frame`
with `{:?}`, and checks that a marker secret is absent as text, as a
decimal byte list, as hex and as the decoder's one-line-per-byte form,
that `[hidden]` is present, and that the other fields still show.
- Round-trips `SaslAuthenticate` (request and response, every version),
`AlterUserScramCredentials`, `AlterConfigs`, `IncrementalAlterConfigs`
and a produce request through encode and decode under a TRACE subscriber
with span events on, and checks that the captured log doesn't hold the
marker. The SASL request test also checks that the `serialize_bytes`
span ran, so it can't pass by capturing nothing; with the old span
restored it fails.
- Inflates a batch with each compression type under the same subscriber,
and checks the `Debug` of a record and of an inflated and a deflated
batch.
- `nisshi-sans-io` unit test: the internal (mezzanine) `Debug` hides
each listed field.
- `nisshi-broker/tests/it/log_redaction.rs`: against the libSQL, SlateDB
and PostgreSQL backends, creates a SCRAM user with
`AlterUserScramCredentials`, logs in with SCRAM-SHA-256, creates a
topic, produces an uncompressed and a gzip batch and fetches them back,
all under a global TRACE subscriber. The in-memory backend has no leg,
because it keeps no SCRAM credentials. It checks that the log holds none
of the password, the salted password, the SASL messages, or the record
key, value and header value.
- `nisshi-storage`: `ScramCredential`'s `Debug` hides both keys.
- Misspelling a `SENSITIVE_FIELDS` entry fails the build with:
`SENSITIVE_FIELDS: no field SaltedPasswd in struct
ScramCredentialUpsertion of message AlterUserScramCredentialsRequest;
...`

## Overlap

nisshi-io#849 also touches `nisshi-service/src/frame.rs`. This PR changes the
`debug!` lines around the request decode there and leaves the
`debug!(?request)` line that nisshi-io#849 rewrites, since the generated `Debug`
covers it. The branch is rebased on `main` after nisshi-io#850.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
IgorOffline pushed a commit to IgorOffline/tansu-io-main that referenced this pull request Oct 10, 2026
…uce, consume, offsets and restarts (nisshi-io#902)

Stacked on nisshi-io#862: review that one first. This PR adds the next set of
smoke tests on top of its harness.

## What this adds

Tests that run the Kafka CLI tools the way a user does, on every storage
engine:

- `broker`: ApiVersions names node 111, and the cluster id is the one
the broker started with.
- `topics`: describe, auto-create on produce, delete and re-create.
- `configs`: describe, add and delete topic configs, and broker
defaults.
- `produce`: every `acks` setting, compression codecs, tombstones,
`max.message.bytes`, idempotent and concurrent producers,
`LogAppendTime`.
- `consume`: reading from an offset, a group resuming where it stopped,
a pattern subscription that picks up a topic created later.
- `offsets`: lookups by time.
- `restart`: topics, records, committed offsets, topic configs and SCRAM
users survive a SIGTERM restart on PostgreSQL and SQLite, and an
in-memory broker starts again empty.
- `storage_url`: an unparsable or invalid storage URL stops the broker
with an error that names it.

The harness now has one module per Kafka tool. Each command whose output
a test reads has its own `Output<Marker>` type, so a reader can only be
called on its own command's output. Brokers can restart on the same
storage, and tests can start isolated brokers.

The CI change gives a smoke leg that fails before `just smoke` runs
(setup, toolchain) a FAIL row, so it still shows in the report. `run.sh`
also fails the leg if no nextest status line parses, instead of showing
a green leg with no tests.

`.claude/rules/smoke-tests.md` sets the rules for writing a smoke test.

## Ignored tests

A test that fails because of a broker bug is ignored with the bug in its
reason, so CI stays green and the fix enables it. Every reason names the
issue or PR that fixes it, e.g. nisshi-io#798 for the five produce tests that
read the latest offset on in-memory storage, and nisshi-io#849 for the
`max-timestamp` lookup.

## Testing

- `just clippy`-equivalent for `nisshi-smoke-test` (all features, and
each engine feature alone), `cargo fmt`, the crate's unit tests, rustdoc
with warnings denied, ShellCheck on `run.sh` and actionlint on `ci.yml`
all pass.
- `just smoke postgres`, `sqlite` and `memory` on this tree, rebased on
main: every test that isn't ignored passes, and the shared broker passes
its checks. Local runs also run the ignored tests: postgres 55 of 64
pass, sqlite 58 of 67, memory 45 of 58, and every failure is an ignored
test.
- The `maintenance_interval`, timestamp-lookup and `acks=0` tests were
ignored for bugs that nisshi-io#850, nisshi-io#838 and nisshi-io#845 fixed, and are now enabled.
`acks=0` stays ignored on in-memory storage for nisshi-io#798, like the other
produce tests.
- `s3` is unchanged: it still runs only `topic_lifecycle`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: William Kourlas <156007774+solace-wkourlas@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants