Skip to content

AsyncAPI 3.x: the driver contract for publishing a message - #1738

Open
LautaroPetaccio wants to merge 5 commits into
feature/asyncapi-action-individualfrom
feature/asyncapi-driver-contract
Open

AsyncAPI 3.x: the driver contract for publishing a message#1738
LautaroPetaccio wants to merge 5 commits into
feature/asyncapi-action-individualfrom
feature/asyncapi-driver-contract

Conversation

@LautaroPetaccio

@LautaroPetaccio LautaroPetaccio commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

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.

AsyncApiProblem

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

Where a driver publishes one message and, when a reply is expected, waits for the one that answers it — the counterpart of executeAction for 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

AsyncApiReplyDto distinguishes four outcomes, because they mean different things and only one is a fault:

  • published, no reply expected — a fire-and-forget operation did what it could
  • published, and a reply arrived — the only case with something to classify
  • published, and nothing arrived within the window — the contract promised a reply and did not deliver, though a slow service and a stuck one look alike from outside
  • could not be published at all — a broken setup, not a finding about the service, which is why it is reported separately from silence

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 on ActionDto, and two branches in EMController.

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/controller runs 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.

@arcuri82
arcuri82 force-pushed the feature/asyncapi-driver-contract branch from 755b0a4 to 513d76a Compare September 8, 2026 11:35
@arcuri82
arcuri82 force-pushed the feature/asyncapi-driver-contract branch from 513d76a to 45344be Compare September 8, 2026 11:41
@arcuri82
arcuri82 force-pushed the feature/asyncapi-driver-contract branch from 45344be to cbaae4c Compare September 8, 2026 11:42
@arcuri82
arcuri82 force-pushed the feature/asyncapi-driver-contract branch from cbaae4c to cbea234 Compare September 8, 2026 11:45
/**
* Headers to publish alongside the body, for a transport that has them.
*/
public Map<String, String> headers = new LinkedHashMap<>();

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.

add a description of what the keys and values are in maps. This programming discipline is specified in docs/for_developers.md

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done!

*/
public Long replyTimeoutMs;

public static final String CORRELATION_IN_HEADER = "HEADER";

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.

all constants should be at the beginning of the class

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done!

/**
* The index of the action this answers, echoing what was asked.
*/
public Integer index;

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.

can this value be null?

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

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.

can this value be null?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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<>();

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.

describe what key,value are as specified in docs/for_developers.md

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done!

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

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.

can this value be null?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-driver-contract branch from cbea234 to 7750cb6 Compare September 8, 2026 16:28
@LautaroPetaccio
LautaroPetaccio force-pushed the feature/asyncapi-driver-contract branch from b59d655 to 0596618 Compare September 8, 2026 17:19
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.
@arcuri82
arcuri82 force-pushed the feature/asyncapi-driver-contract branch from 61ca390 to 018bf2b Compare September 9, 2026 10:52
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