Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ jobs:
- name: Build and test
run: mvn --batch-mode --no-transfer-progress verify

# The JMH module is a standalone build against the installed
# library snapshot (see benchmarks/README.md). Compiling it here -
# not running it - keeps the regression instrument from rotting
# silently when an internal seam it reaches changes shape.
- name: Compile the benchmark module against the freshly built library
run: |
mvn --batch-mode --no-transfer-progress -DskipTests -Djacoco.skip=true -Dktlint.skip=true -Dcyclonedx.skip=true install
mvn --batch-mode --no-transfer-progress -f benchmarks/pom.xml compile

# Repo-local script instead of a third-party reporting action (this
# workflow pins actions to SHAs; a summary needs no new supply-chain
# surface). Runs on failure too, so a red build still shows which
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,13 @@ Three resilience mechanisms run independently per topic class:

1. **Per-class circuit breaker.** A Resilience4j `CircuitBreaker` is
instantiated per active topic class. A stuck audit-topic broker does
not throttle technical-log delivery, and vice versa. Default
thresholds (tuned for logging volume, canonical in the
[configuration guide](docs/config/kafka-appender-config-guide.md)):
50% failure rate over a sliding window of 20 calls, 30 second
cooldown in open state, 10 probe calls in half-open, probes spread
5 ms apart. These thresholds are fixed in code - the breaker
registry is an internal seam (ADR-0002), so there is currently no
supported way to tune them per deployment; if a real tuning need
comes up, open an issue so it can become an XML-bindable property
with a follow-up ADR instead of an ad-hoc hook.
not throttle technical-log delivery, and vice versa. The
thresholds are tuned for logging volume and fixed in code; their
canonical values live in the configuration guide's
[defaults quick reference](docs/config/kafka-appender-config-guide.md#12-defaults-quick-reference),
which a test keeps in step with the constants. Why they are not
configurable - and what else is deliberately fixed - is explained in
the guide's [section on fixed behavior](docs/config/kafka-appender-config-guide.md#13-what-is-deliberately-not-configurable).

2. **Asynchronous delivery with callback-driven outcome tracking.**
Kafka's `producer.send` is invoked with a callback that feeds the
Expand Down Expand Up @@ -790,7 +787,10 @@ user id, account id) — most deployments use the trace-id default. Per
[ADR-0002](docs/adr/ADR-0002-public-api-is-the-operator-surface.md),
such an override would be added as an XML-bindable `KafkaAppender`
property (e.g. a partitioning-key MDC name), not by exposing the
internal enricher — open an issue if your deployment needs it.
internal enricher — open an issue if your deployment needs it. The
configuration guide lists
[everything that is deliberately not configurable](docs/config/kafka-appender-config-guide.md#13-what-is-deliberately-not-configurable)
and the reason for each item.

## Future work

Expand Down
50 changes: 50 additions & 0 deletions docs/adr/ADR-0004-appender-instances-are-not-restartable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ADR-0004: Appender instances are not restartable

- **Status:** accepted
- **Date:** 2026-09-07
- **Context:** The 2026-09-07 defect analysis
(`docs/assessment/CODE_ANALYSIS-2026-09-07T19-09-00.md`, finding 4)
found that `start()` after `stop()` silently lost the fallback
appender. Its remediation chose the symmetric direction - restart the
fallback, reset the breakers, re-arm the error guard, rebind the
metrics - and the same day's follow-up pass (`.R2.md`, findings R2-4
and R2-5) and architecture review
(`docs/assessment/ARCHITECTURE_REVIEW-2026-09-07T20-23-00.md`,
finding 3) showed what that buys: every per-appender resource now
needs a "what happens on restart?" answer, and no consumer, issue or
README passage asks for a programmatic restart.

## Decision

**A `KafkaAppender` instance is started once.** `start()` after
`stop()` is refused with an `addError` naming this ADR; the operator
or program creates a new instance instead.

Rationale: this is Logback's own lifecycle. A reconfiguration
(`<configuration scan="true">`, `LoggerContext.reset()` plus Joran,
Spring Boot's logging-system re-initialization) stops and detaches
every appender and builds **new instances**; `LoggerContext.stop()` is
terminal; Logback's `AsyncAppender` detaches its appenders on `stop()`
and is not restartable in practice. A same-instance restart therefore
only ever originates in application code that holds a reference and
toggles it - a path for which there is no known user. Supporting it
means keeping fallback, circuit-breaker state, metrics binding,
one-shot error report and every future stateful component symmetric
across a second life; refusing it removes those branches and the
interactions between them.

## Consequences

- `KafkaAppender.start()` checks the stop guard first and refuses with
a message that names the alternative (a new instance); the guard is
never reset. The Joran round trip and Logback reconfiguration are
unaffected - they never restart an instance.
- The restart-symmetry code introduced on 2026-09-07 (fallback
restart in `start()`, breaker reset in `buildPipeline`, error-guard
re-arm) is removed; the `KafkaAppenderMetricsBinding` keeps deciding
on the appender's bound state, which is simpler than its former
identity set regardless of restart.
- Reversal is a conscious API addition: if a consumer documents a need
for stop/start toggling, a follow-up ADR reinstates the symmetric
lifecycle as a supported contract - with the per-resource answers
written down, not rediscovered.
Loading
Loading