Skip to content

Commit a583c19

Browse files
committed
docs: rework README and add AGENTS contributor guide
1 parent 4ac0689 commit a583c19

2 files changed

Lines changed: 672 additions & 454 deletions

File tree

AGENTS.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# AGENTS.md
2+
3+
Coding rules and project conventions for AI agents and human contributors working on
4+
**spring-cloud-stream-starter-request-reply** (artifactId `spring-cloud-stream-starter-request-reply`,
5+
groupId `community.solace.spring.cloud`). This file applies to this repository only.
6+
7+
The library adds synchronous request/reply and request/multi‑reply semantics on top of Spring Cloud
8+
Stream, primarily for the Solace binder. When in doubt about behaviour, read the source under
9+
`src/main/java/community/solace/spring/cloud/requestreply` — it is the source of truth, not this file.
10+
11+
## 1. Technology stack
12+
13+
- **Language:** Java 17 (`<java.version>17</java.version>`). Do not use language features beyond Java 17.
14+
- **Build:** Maven, with `spring-boot-starter-parent` as the parent POM. It is a **library / Spring Boot
15+
starter**, not an application: the `spring-boot-maven-plugin` repackage goal is disabled and the POM is
16+
flattened for publishing.
17+
- **Frameworks:** Spring Boot, Spring Cloud Stream, Spring Cloud Function, Spring Integration, Spring
18+
Messaging.
19+
- **Reactive:** Project Reactor (`Flux`, `Mono`, `FluxSink`) for multi‑reply streaming.
20+
- **Observability:** Micrometer (`micrometer-core` for metrics, `context-propagation` for tracing/MDC).
21+
- **Solace:** Solace `sol-jcsmp` types (`SDTStream`, `Destination`) are used **optionally** — the Solace
22+
header parser is only registered when `com.solacesystems.jcsmp.Destination` is on the classpath.
23+
- **Testing:** JUnit 5, Spring Boot Test, `spring-cloud-stream-test-binder`, Mockito, Reactor Test,
24+
Awaitility.
25+
- Do not add heavyweight dependencies. Solace types must stay optional so the library keeps working with
26+
other binders (e.g. the test binder).
27+
28+
## 2. Project structure
29+
30+
```
31+
src/main/java/community/solace/spring/cloud/requestreply/
32+
├── service/
33+
│ ├── RequestReplyService.java # public API interface (requester side)
34+
│ ├── RequestReplyServiceImpl.java # requester implementation (correlation, timeout, executor)
35+
│ ├── ResponseHandler.java # per-request bookkeeping, dedup, completion latch
36+
│ ├── RequestReplyFunctionRegistrar.java # registers a reply consumer per bindingMapping
37+
│ ├── RequestReplyAutoConfiguration.java # main auto-configuration
38+
│ ├── MessageConverter.java
39+
│ ├── header/
40+
│ │ ├── RequestReplyMessageHeaderSupportService.java # responder-side wrap/wrapList/wrapFlux helpers
41+
│ │ └── parser/ # ordered Message/MessageHeaders parsers (correlationId,
42+
│ │ │ # destination, replyTo, replyIndex, totalReplies, errorMessage)
43+
│ │ ├── SolaceHeaderParser.java SpringHeaderParser.java HttpHeaderParser.java ...
44+
│ ├── logging/ # RequestReplyLogger + DefaultRequestReplyLogger
45+
│ └── messageinterceptor/ # RequestSendingInterceptor, ReplyWrappingInterceptor
46+
├── config/
47+
│ ├── RequestReplyProperties.java # @ConfigurationProperties("spring.cloud.stream.requestreply")
48+
│ ├── BinderMappings.java # a single bindingMapping entry
49+
│ ├── logging/LoggerAutoConfiguration.java
50+
│ └── messageinterceptor/ # Noop*Interceptor beans + MessageInterceptorAutoConfiguration
51+
├── env/ # ${replyTopicWithWildcards|...} property source
52+
├── util/ # MessageChunker, CheckedExceptionWrapper
53+
└── exception/ # RequestReplyException
54+
src/main/resources/META-INF/
55+
├── spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports # auto-config registration
56+
└── spring.factories # EnvironmentPostProcessor registration
57+
examples/ # standalone runnable example apps (own POMs)
58+
doc/ # protocol diagrams and AsyncAPI spec
59+
```
60+
61+
- Auto‑configuration classes are registered in
62+
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`. Any new
63+
auto‑configuration must be added there (never rely on component scan alone from a starter).
64+
- The `${replyTopicWithWildcards|...}` `EnvironmentPostProcessor` is registered in `spring.factories`.
65+
- Modules under `examples/` are independent Maven projects; keep them compiling but do not couple library
66+
code to them.
67+
68+
## 3. Coding standards & conventions
69+
70+
Formatting is defined in `.editorconfig` (IntelliJ scheme). Key rules:
71+
72+
- **Indent:** 4 spaces, no tabs. **Line ending:** LF. **Encoding:** UTF‑8. Trailing whitespace trimmed.
73+
Files do **not** end with a final newline (`insert_final_newline = false`).
74+
- **Import order:** `java.**`, then `javax.**`, then everything else, then `org.springframework.**`, then
75+
static imports (`$*`), each group separated by a blank line. No wildcard/on‑demand imports (threshold
76+
999) — use single‑class imports.
77+
- **Braces:** always use braces, even for single‑statement `if`/`for`/`while` (`*_brace_force = always`).
78+
- **Naming:** implementation classes end in `Impl` (e.g. `RequestReplyServiceImpl`); test classes end in
79+
`Tests`.
80+
- Prefer `@Nullable` (Spring) on nullable returns/params, as existing parser code does.
81+
- Public API methods carry Javadoc; keep it accurate when you change signatures or behaviour.
82+
- Do not introduce Lombok in `src/main` (it is not a main dependency here; the examples use it, main code
83+
does not).
84+
- Update `CHANGELOG.md` (Keep a Changelog format, semantic versioning) for any user‑visible change.
85+
86+
## 4. Request/reply model & public API contract
87+
88+
The two public entry points are `RequestReplyService` (requester) and
89+
`RequestReplyMessageHeaderSupportService` (responder). Treat their signatures and semantics as a stable
90+
contract; breaking changes require a major version bump and a CHANGELOG entry.
91+
92+
**Requester (`RequestReplyService`):** every method is generic in request `Q` and response `A`, takes an
93+
`expectedClass` and a `Duration timeoutPeriod`, and has an overload accepting
94+
`Map<String, Object> additionalHeaders`. Flavours:
95+
96+
- `requestAndAwaitReplyTo{Topic,Binding}` → blocking, returns `A`, declares `InterruptedException`,
97+
`TimeoutException`, `RemoteErrorException`.
98+
- `requestReplyTo{Topic,Binding}` → non‑blocking, returns `CompletableFuture<A>` (single response).
99+
- `requestReplyTo{Topic,Binding}Reactive` → returns `Flux<A>` (zero to N responses).
100+
101+
`…Topic` variants resolve the binding by matching the destination against
102+
`bindingMapping[].topicPatterns` (first match wins; no match → `IllegalArgumentException`). `…Binding`
103+
variants send to the binding's `-out-0` destination.
104+
105+
**Responder (`RequestReplyMessageHeaderSupportService`):**
106+
107+
- `wrap(fn, applicationExceptions...)` → single `Message` response. Returning `null` drops the message.
108+
- `wrapList(fn, bindingName, applicationExceptions...)``List<Message>` of known size.
109+
- `wrapFlux(fn, bindingName[, groupTimeout])` → streaming `Flux<Message>` of unknown size.
110+
111+
Invariants the wrappers must preserve:
112+
113+
- copy the **correlationId** onto every reply,
114+
- set the reply **destination** (`BinderHeaders.TARGET_DESTINATION`) from the request's reply‑to header,
115+
applying `variableReplacements`,
116+
- set `totalReplies` and `replyIndex` headers for multi‑reply, and always emit a terminal (empty) message
117+
to close the stream,
118+
- forward a matching `applicationException` as an error reply (`errorMessage` header) which the requester
119+
surfaces as `RemoteErrorException` (or an errored `Flux`),
120+
- copy any headers listed in `copyHeadersOnWrap`.
121+
122+
**Reply routing:** for each `bindingMapping`, `RequestReplyFunctionRegistrar` registers a
123+
`Consumer<Message<?>>` bean named after the binding, wired to `-in-0`, delegating to
124+
`RequestReplyServiceImpl.onReplyReceived`. Never register this consumer in a way that runs in sliced test
125+
contexts (see §6). If a bean with the binding name already exists, do not override it.
126+
127+
## 5. Testing
128+
129+
- Run the full suite with `mvn verify`. Do not use `--offline` unless dependencies are already cached.
130+
- Tests live under `src/test/java/...` mirroring the main package layout. Unit tests end in `Tests`;
131+
Spring‑context / sample‑app integration tests live under `sampleapps/` and `integration/` and use the
132+
`spring-cloud-stream-test-binder`.
133+
- Use JUnit 5 + AssertJ/Mockito; use Awaitility (already a dependency) for async assertions rather than
134+
`Thread.sleep`. Use Reactor Test (`StepVerifier`) for `Flux`/`Mono` behaviour.
135+
- Any change to header parsing, deduplication (`ResponseHandler`), grouping (`MessageChunker`), or the
136+
wrap helpers must come with tests — these are the correctness‑critical parts.
137+
- The starter must remain loadable **and** excludable: keep the tests that verify
138+
`@ImportAutoConfiguration(exclude = RequestReplyAutoConfiguration.class)` and
139+
`spring.autoconfigure.exclude` behaviour green.
140+
- Do not require a live Solace broker for `src/test`; use the test binder. (The `examples/` apps may point
141+
at a public broker, but they are not part of the unit test suite.)
142+
143+
## 6. Project-specific patterns & invariants
144+
145+
- **Solace is optional.** Guard Solace‑specific beans/parsers with `@ConditionalOnClass` (as
146+
`RequestReplyAutoConfiguration` does for `SolaceHeaderParser`). Never make core request/reply logic hard‑
147+
depend on `sol-jcsmp`.
148+
- **Reply consumers are contributed by an `ImportBeanDefinitionRegistrar`, not an
149+
`ApplicationContextInitializer`.** Spring Boot applies initializers to *every* context (including sliced
150+
test contexts) and they cannot be excluded, which previously broke `@JsonTest`/`@WebMvcTest` slices. Keep
151+
this registration inside `RequestReplyFunctionRegistrar` imported by the auto‑configuration so that
152+
excluding the auto‑configuration also disables the consumers.
153+
- **Header access goes through the ordered parser chain**, never by reading a hard‑coded header key inline.
154+
To support a new binder or header convention, add a parser bean and order it with `@Order` (lower =
155+
higher priority). Header name constants live in `SpringHeaderParser` (`totalReplies`, `replyIndex`,
156+
`groupedMessages`, `groupedContentType`, `errorMessage`) — reuse them.
157+
- **Correlation ids** are generated as a random UUID unless the request already carries one.
158+
- **Reply‑to uniqueness:** reply topics should be process‑unique. Use `${replyTopicWithWildcards|uuid}`
159+
(generated once at process start) — not `${random.uuid}` (new value per reference). The requester listens
160+
on the wildcarded topic via `${replyTopicWithWildcards|<binding>|<wildcard>}`.
161+
- **Deduplication** (`ResponseHandler`) is by `replyIndex`, backed by a `BitSet`; terminal (finish/error)
162+
messages bypass dedup. The unknown/streaming growth bound is read as a **JVM system property**
163+
`spring.cloud.stream.requestreply.dedup.maxBitsWhenUnknown` (via `Integer.getInteger`, default 100000) —
164+
it is **not** a Spring `@ConfigurationProperties` value, so it is set with `-D`, not `application.yaml`.
165+
- **Grouping thresholds** are load‑bearing constants: flush at 1 MB, at 10 000 messages, or after the group
166+
timeout (default 200 ms). Changing them affects broker behaviour and interop — do so deliberately and
167+
document it.
168+
- **Threading & context:** requests run on a shared executor wrapped with
169+
`ContextExecutorService`/`ContextSnapshotFactory` so Micrometer context (trace id, MDC) propagates across
170+
every pipeline stage. Preserve the executor‑wrapping approach; do not replace it with per‑task snapshots.
171+
- **State is in memory** (`PENDING_RESPONSES`), so the library is not fail‑safe or horizontally scalable
172+
for a single request. Do not silently introduce assumptions that a reply may be handled by a different
173+
instance than the one that sent the request.
174+
- **Configuration property namespace** is `spring.cloud.stream.requestreply`. Add new options to
175+
`RequestReplyProperties` / `BinderMappings` with getters/setters and document them in the README property
176+
table. Unknown YAML keys under a binding mapping are silently ignored by relaxed binding — do not rely on
177+
keys that have no field.
178+
179+
## 7. Example
180+
181+
A minimal requester + responder pair. Configuration ties the binding name across
182+
`spring.cloud.function.definition`, `requestreply.bindingMapping`, and the `-in-0`/`-out-0` bindings.
183+
184+
```yaml
185+
spring:
186+
cloud:
187+
function:
188+
definition: temperatureQuery
189+
stream:
190+
requestreply:
191+
bindingMapping:
192+
- binding: temperatureQuery
193+
replyTopic: reply/temperature/@project.artifactId@_${HOSTNAME}_${replyTopicWithWildcards|uuid}
194+
topicPatterns:
195+
- request/temperature/.*
196+
bindings:
197+
temperatureQuery-in-0:
198+
destination: ${replyTopicWithWildcards|temperatureQuery|*}
199+
contentType: "application/json"
200+
binder: solace
201+
temperatureQuery-out-0:
202+
binder: solace
203+
```
204+
205+
```java
206+
// Requester
207+
SensorReading reading = requestReplyService.requestAndAwaitReplyToTopic(
208+
request,
209+
"request/temperature/celsius/livingroom",
210+
SensorReading.class,
211+
Duration.ofSeconds(30)
212+
);
213+
214+
// Responder (Spring Cloud Function bean)
215+
@Bean
216+
public Function<Message<SensorRequest>, Message<SensorReading>> temperatureQuery(
217+
RequestReplyMessageHeaderSupportService headerSupport
218+
) {
219+
return headerSupport.wrap(req -> {
220+
SensorReading r = new SensorReading();
221+
r.setTemperature(21.5);
222+
return r; // return null to drop; throw a forwarded exception to send an error reply
223+
}, MyBusinessException.class);
224+
}
225+
```
226+
227+
## Related projects
228+
229+
Only public ecosystem projects are relevant here — do not reference private/internal systems:
230+
231+
- [Spring Cloud Stream](https://spring.io/projects/spring-cloud-stream) — the messaging abstraction this
232+
library builds on.
233+
- [Spring Cloud Function](https://spring.io/projects/spring-cloud-function) — the functional model used for
234+
responders and reply consumers.
235+
- [Spring Boot](https://spring.io/projects/spring-boot) — auto‑configuration and starter mechanics.
236+
- [Solace Spring Cloud / PubSub+ binder](https://github.com/SolaceProducts/solace-spring-cloud) — the
237+
primary target binder.
238+
- [Project Reactor](https://projectreactor.io/) — reactive types for multi‑reply.
239+
- [Micrometer Context Propagation](https://docs.micrometer.io/context-propagation/reference/) — tracing/MDC
240+
propagation across threads.

0 commit comments

Comments
 (0)