|
| 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