AsyncAPI 3.x: the driver contract for publishing a message - #1738
AsyncAPI 3.x: the driver contract for publishing a message#1738LautaroPetaccio wants to merge 7 commits into
Conversation
755b0a4 to
513d76a
Compare
513d76a to
45344be
Compare
45344be to
cbaae4c
Compare
cbaae4c to
cbea234
Compare
| /** | ||
| * Headers to publish alongside the body, for a transport that has them. | ||
| */ | ||
| public Map<String, String> headers = new LinkedHashMap<>(); |
There was a problem hiding this comment.
add a description of what the keys and values are in maps. This programming discipline is specified in docs/for_developers.md
| */ | ||
| public Long replyTimeoutMs; | ||
|
|
||
| public static final String CORRELATION_IN_HEADER = "HEADER"; |
There was a problem hiding this comment.
all constants should be at the beginning of the class
| /** | ||
| * The index of the action this answers, echoing what was asked. | ||
| */ | ||
| public Integer index; |
There was a problem hiding this comment.
It can't. It basically holds the same value as ActionDto.index, which newAction has already set by then, so a null never reaches this line. It is Integer to match ActionDto.index and ActionResponseDto.index.
EMController almost never assigns a DTO through constructors and most of the DTOs are plain classes without validations.
WDYT I should do in this case? Should I create this DTO to have a constructor with the integer with a validation?
| * so this is a tuning parameter with no equivalent in a synchronous protocol. It is set | ||
| * generously and reported with the result. | ||
| */ | ||
| public Long replyTimeoutMs; |
There was a problem hiding this comment.
Yes, when no reply is expected (replyAddress null), there is nothing to wait for. I've written it in the javadoc.
| /** | ||
| * The reply's headers, for a transport that has them. | ||
| */ | ||
| public Map<String, String> replyHeaders = new LinkedHashMap<>(); |
There was a problem hiding this comment.
describe what key,value are as specified in docs/for_developers.md
| * How long the driver waited, in milliseconds, whether or not anything arrived. Reported | ||
| * because the verdict on silence is only meaningful alongside how long it was waited for. | ||
| */ | ||
| public Long waitedMs; |
There was a problem hiding this comment.
Yes, when the driver did not wait at all, that is a fire and forget operation, or a message that could not be published. I've written it in the javadoc.
cbea234 to
7750cb6
Compare
b59d655 to
0596618
Compare
61ca390 to
018bf2b
Compare
Everything so far is in core and reads a document. This is the other side: what a driver must provide so that the core can drive an AsyncAPI service without knowing anything about brokers. AsyncApiProblem declares that the SUT is driven by messages. It carries only the document -- a location to fetch it from, or the text itself. The connection to the broker stays in the driver and never crosses, which is what keeps the core free of any broker library. Note it may be declared with the document inline, which REST has no real need for. A REST service usually serves its own contract over HTTP; a service that speaks only Kafka has no endpoint to serve anything from, so its document is far more likely to be a file shipped beside it. SutController.executeAsyncApiAction is where a driver publishes one message and, when a reply is expected, waits for the one that answers it. It is the counterpart of executeAction for RPC. It has a default that throws rather than being abstract, so that adding it does not break every existing driver; the message says what to override. Only publish and await are protocol-specific, and they never leave the driver. The driver is asked to report what happened, not to judge it: AsyncApiReplyDto distinguishes published-with-no-reply-expected, a reply that arrived, silence within the window, and a failure to publish at all. The last of those is a broken setup rather than a finding about the service, which is why it is kept apart from silence. Deciding what an outcome means is the core's job, so that it means the same thing whatever the transport. Also carried in the reply is whether the correlation id came back. Whether correlation works cannot be read off a contract, since echoing the id is the service's own behaviour, so it is established by watching for it. Wiring: a field on SutInfoDto, a field on ActionDto, and two branches in EMController.
Review feedback: CORRELATION_IN_HEADER and CORRELATION_IN_PAYLOAD closed the class, after every instance field. docs/for_developers.md puts constants first.
Review feedback, and docs/for_developers.md: a Map field says what its key and value are. Both header maps go from header name to its text.
A null list failed inside addAll anyway; the check now sits where the method starts, named, as docs/for_developers.md asks of public methods.
Review feedback: replyTimeoutMs is null when no reply is expected, and waitedMs when the driver did not wait at all, which the javadoc left unsaid.
018bf2b to
1a05716
Compare
Review feedback: it echoes ActionDto.index, which newAction has already dereferenced by the time the reply is built, so an int says what the Integer could only promise.
Reverts f4acc61. The DTOs in controller-api are plain carriers: none of them checks nullness on assignment, and index is Integer in both ActionDto and ActionResponseDto. This one matches them; that it is never null is guaranteed by newAction dereferencing dto.index first.
Ninth in the AsyncAPI stack, on top of #7. This one is entirely in
client-java— no core changes, so it reviews as controller-side plumbing on its own.Everything so far reads a document. This is the other side: what a driver must provide so the core can drive an AsyncAPI service without knowing anything about brokers.
Why a driver at all, even for black-box
Unlike REST and GraphQL, there is no universal wire to point at — Kafka, AMQP, MQTT and WebSocket share nothing at the API level. So something has to hold a client and move the bytes, and that something is the driver. This is the one way AsyncAPI is unlike every other black-box mode EvoMaster ships, and it is the same reason RPC needs a driver.
AsyncApiProblemCarries only the document — a location to fetch it from, or the text itself. The connection to the broker stays in the driver and never crosses, which is what keeps core free of any broker dependency.
The inline-text option is worth noting, since REST has little need for it: a REST service usually serves its own contract over HTTP, whereas a service that speaks only Kafka has no endpoint to serve anything from. Its document is far more likely to be a file shipped beside it.
SutController.executeAsyncApiActionWhere a driver publishes one message and, when a reply is expected, waits for the one that answers it — the counterpart of
executeActionfor RPC.It has a default that throws rather than being abstract, so adding it does not break any existing driver; the message names what to override. Only publish and await are protocol-specific, and they never leave the driver.
The driver reports, it does not judge
AsyncApiReplyDtodistinguishes four outcomes, because they mean different things and only one is a fault:Deciding what an outcome means is the core's job, so that it means the same thing whatever the transport.
The reply also carries whether the correlation id came back. Whether correlation works cannot be read off a contract — echoing the id is the service's own behaviour — so it is established by watching for it rather than assumed.
Wiring
A field on
SutInfoDto, a field onActionDto, and two branches inEMController.Testing
Six tests in
AsyncApiProblemTest, including one asserting that a driver pointed at an AsyncAPI service without implementing the hook fails with a message telling it what to override.client-java/controllerruns 740 tests, 0 failures, 34 errors — identical with and without this change. Those 34 are testcontainers suites failing because Docker is not reachable in my environment; I verified the same counts on a pristine tree before claiming that.