spec
spec copied to clipboard
Add a View property to the info section to change the perspective of subscribe and publish operations.
The specification regards publishing and subscribing from the point of view of what a client is allowed to do. For example, if the spec has a publish operation, then applications will actually be subscribing. This is consistent with OpenAPI server side code, where if one sees a GET, it is not the server that does the GET but rather other applications.
However some users of AsyncAPI find it more intuitive to consider things from the opposite perspective. If they have a document with a publish operation and want to generate code, they expect that the code will publish, not subscribe.
I propose adding a parameter called 'view' that can have two values:
'client' - this is consistent with the current specification. It means that the document says what a client of the application can do. 'provider' - this means that the document describes what the provider of the service is doing, i.e. a subscribe operation means that the application is subscribing.
Can't it be tackled using specification extensions? It can be done using specification extensions, but so far there are three generator templates using their own extensions and terminology - it would be nice to make this consistent.
Describe the solution you'd like I would like to see an addition to the info section that supports this parameter, e.g.
info:
view: provider
Describe alternatives you've considered The java-spring-cloud-stream and paho-python templates currently support this through the use of specification extentions and parameters. The java-template also has this feature but uses different terminology.
Welcome to AsyncAPI. Thanks a lot for reporting your first issue.
Keep in mind there are also other channels you can use to interact with AsyncAPI community. For more details check out this issue.
Hey Michael, it's an interesting and at least for me a confusing point too. I did indeed notice you had the switch in the paho-python and thought it was a good idea.
IMHO, The ability to define the point of view is a MUST. In my day to day job most of my conversations are around how a client can interact with a service, rather than the actual server definition so being available in the core spec and therefore consistent would be a great solution.
Thanks for your support. I put the same flag in the java-spring-cloud-stream template, and it was recently added to the java-spring-template with the name 'inverseOperations.'
Hey Michael, i'm agree with the idea, but as i explained i other issues\PR I would be very careful with client
and provider
words.
I had a worry, that when someone will read client
he could mix up it with client-server
paradigm. But for sure, in AsyncAPI we couldn't have terms like server
or client
in common understanding, since AsyncAPI could describe what application produces and what it consumes.
Let's consider following case, my AsyncAPI consist with only subscribe
operations. So, in your terms we will name application that only supply messages like provider
, and application that will actually creates messages like client
. A little bit confusing, from my point of view.
So, maybe counterpart
or partner
instead of provider
and source
instead of client
?
...And regarding view
, maybe perspective
? Just a though.
I agree the terms aren't perfect, those are the terms that our group in Solace came up with. I'll take this back to them for further discussion. Ultimately it's up to Fran to decide, should he agree to put this in the spec.
Hi Michael, I agree with the idea. Couple of questions w.r.t "provider" perspective:
- Since "provider" view describes the usage of the channel but not the channel itself, it would benice to refer to the channel (described in a "channel view" spec) instead of defining the channel again in the "provider" view. If the same channel is used in different "provider" views, we can avoid duplicate ambiguous channel definitions in their respective "provider" view specs.
+1 to this feature. What if we use Consumer / Provider instead of Client / Provider?
I would rather not have two versions of a spec based on a field (in this case, 'view' is being proposed to indicate which perspective you are viewing).
Has anyone considered a shift in the language for the spec from being "application-centered" to being channel-centered? In that way, the channel defines what it accepts and what it publishes, leaving the spec to describe the message structures and assoc bindings for generators to leverage based on if the generator is designed to support publishing, consuming, or both.
I would rather not have two versions of a spec based on a field (in this case, 'view' is being proposed to indicate which perspective you are viewing).
Has anyone considered a shift in the language for the spec from being "application-centered" to being channel-centered? In that way, the channel defines what it accepts and what it publishes, leaving the spec to describe the message structures and assoc bindings for generators to leverage based on if the generator is designed to support publishing, consuming, or both.
I understand where you are coming from and this is where I started as well. My fear was we needed something for the way the spec is written today (possibly a band-aid) and really think this through (in my opinion) for the next major release. But, to your point, if it was channel-centered, we could say a channel has a 1..* relationship with a schema and not use a verb (because to me, a channel is a pipe and is worthless unless someone put something into it, and someone pulls it out ;) ). I think then you could have "Application" specifications that reference channels and use verbs that say "what it does" (super helpful for internal enterprise ICD's and code gen) and "what you could do" which would be useful for B2B (external) use cases.... So my only point is that I like your proposal to have something channel centered, but could leverage that for "Application-centric" modeling as well. -- I will also in another reply, explain in better detail what we were thinking around view for the community to review as a near-term solution since we get tons of questions from our customers on this aspect/view (no pun intended).
I like the debate this is generating. In my opinion, this flaw of the spec raises deeper intrinsic problems that need to be solved in a solid way. Let me expose my point of view here:
View property on the info section
I like this proposal for what it represents, however, we should take into account possible side-effects it can cause. This flag does not only change the meaning of the publish/subscribe verbs but the way protocol bindings are interpreted. For instance, HTTP, WebSockets, NATS, and other protocols supporting the concept of client and server or the concept of request and reply will be affected by this flag too. As an example, it's not the same to say that a publish
operation is a NATS request the application is performing than saying the publish
operation is a NATS reply. The same applies to HTTP.
Not going to say this is impossible to solve but definitely is not just about adding a flag on the info object. It has a lot of side-effects. And that's precisely what I would try to avoid in the spec at all costs: side-effects.
Why avoiding side-effects is key?
The spec is built with reusability in mind, even though it has its flaws on this area too. Let me explain this by example. Given the following AsyncAPI document:
asyncapi: 2.0.0
info:
title: My user management service
version: 1.0.0
channels:
user/registered:
description: In this channel, we get a message every time a user registers on our platform.
subscribe:
description: Subscribe to this channel to get a message every time a user signs up.
message: ...
If we add the view
flag, suddenly some of the things don't make sense anymore:
asyncapi: 2.0.0
info:
title: My user management service
version: 1.0.0
view: provider
channels:
user/registered:
description: In this channel, we get a message every time a user registers on our platform.
subscribe:
description: Subscribe to this channel to get a message every time a user signs up.
message: ...
The description message Subscribe to this channel to get a message every time a user signs up.
doesn't make sense now. It should be "Publish to this channel every time a user signs up" or "This application is subscribing to messages about user signups" because the point of view changed. It is an inconsistency, and might not be a big deal if you're the owner of the whole file but it gets worst if we extract the channel definition for others to use it:
asyncapi: 2.0.0
info:
title: My user management service
version: 1.0.0
view: provider
channels:
user/registered:
$ref: 'shared.yaml#/channels/userRegistered'
Suddenly, it's not that obvious. And it gets worse as more files depend on the shared.yaml#/channels/userRegistered
definition.
What's the purpose of this flag?
If I understood correctly, the purpose is to band-aid the spec to solve a common misconception regarding the publish
and subscribe
meanings. I agree it's utterly weird and should be addressed. That said, I don't think this flag is really providing any "aid" but instead, it's bringing more confusion to the table, as you saw in my examples above.
Don't get me wrong, I'm all for fixing it but the more I think about a solution the less likely it seems to me it's going to happen in a minor version. This deserves an elegant solution and we shouldn't be afraid of releasing version 3.0.0 soon.
Channel-centered vs Application-centered
I think @jhigginbotham pointed us in a great direction. Not only with the comment above but in this tweet about OpenAPI newest version (mate, you got me thinking).
The problem of channel reusability and this problem can actually be addressed by going channel-centered. However, we'd lose the value of an application-centered definition. So, why not do both? And why not making operations first-class citizens instead-of/as-well-as channels? Maybe this discussion is out of the scope of this issue. Or maybe not, that really depends on how much do we want to change.
Everything is better with examples, so here goes some food for thought. Please, don't treat it as a proposal. I just want everyone's mind to be as open as possible to solve this problem:
Application (asyncapi.yaml)
asyncapi: 3.0.0
kind: application # Optional. Defaults to "application". Can also be "library".
info:
title: Trains API
description: Real-time train updates
version: 1.0.0
operations:
publishTrainUpdate:
type: publish
channel:
$ref: 'library.yaml/channels/trainUpdates'
subscribeTrainUpdate:
type: clientSubscribe
channel:
$ref: 'library.yaml/channels/trainUpdates'
Library (library.yaml)
asyncapi: 3.0.0
kind: library
channels:
trainUpdates:
name: trains/updates # Not sure about "name". Maybe "path"? "address"?
message:
payload: ...
I'll elaborate on this idea as soon as I find some time but please don't hesitate to leave your thoughts and/or concerns here.
I think your last examples are what I was thinking in that what you have with a "Library" actually becomes sort of a catalog of available channels. In the kind "application" what does the verb represent? We know that in the kind "library" that you could publish or subscribe to any channel and that it would be access control rules that would limit what a client would actually be able to do (thus not described in the spec, just like openapi). Therefore the operations represent what the application does or the operations available for another application to perform? I think that brings us back to the crux of the discussion in that the Library is list of what channels are available and what you could do, but I would like the kind "Application" to be the operations performed by an application. Thoughts? I think this discussion is super interesting and good!
In the kind "application" what does the verb represent?
Yes, sorry, I should have clarified this point. In this example, publish/subscribe means what the application is doing (opposite as now). Just so we don't screw the other point of view that's sometimes necessary, we should add something like clientPublish
and clientSubscribe
. Then it means we'll end up having, at least, 4 verbs: publish
, subscribe
, clientPublish
, and clientSubscribe
.
@jhigginbotham and other people are questioning if we should have operation verbs at all. I think we should but let's keep our minds open and understand when is this information valuable and why.
Good explanation on the potential additional confusion for using a view band aid. Couple of points that occur to me.
First a question, what is the value of retaining publish, subscribe as is ? rather than having 4 verbs: applicationPublish, applicationSubscribe, clientPublish and clientSubscribe or something similar. Would continuing to use publish & subscribe still lead to confusion when you first pick up the spec?
And actually taking a step back, does going down the route of adding verbs run the risk of making it to verbose? i.e. if I understood the point made about NATS in the community call, would you potentially need more verbs to support its req/response via the same channel? or did I misunderstand the point being made.
I have a question: I was under the impression that one would define a yml file at the application level to document what the application does and allow for code generation at the application level. At the system level with many applications, the various yml files would point to a "library" of message definitions to support the publishing and consuming of the defined messages. My vote would be to keep current publish & subscribe verbs but document them as being from the application's perspective... aka messages application publishes and/or subscribes to. Great spec and very useful for document our current system. Many thanks to the group. Great work.
This issue has been automatically marked as stale because it has not had recent activity :sleeping: It will be closed in 30 days if no further activity occurs. To unstale this issue, add a comment with detailed explanation. Thank you for your contributions :heart:
@fmvilas
By separating channel from operation, it looks like message
is the same regardless of the operation type. The discussion above regarding NATS (and https://docs.nats.io/developing-with-nats/tutorials/reqreply) suggests that to support req/reply there is a definite need for the message
to vary per operation.
Ignoring NATS - what if the message
varies between publish
and consume
on the same channel? For example a chat application - your user ID is added by the server as a header or message field when you publish (but it's not part of the publish message payload).
My vote would be to keep current publish & subscribe verbs but document them as being from the application's perspective... aka messages application publishes and/or subscribes to.
@cappelaere I've been (incorrectly?) treating AsyncAPI as a way to describe the broker/event backbone - e.g "This channel contains weather data from IOT sensors - publish to it if you're a new device, subscribe to it if you want to process the data".
However, I think the existing paradigm as documented (https://www.asyncapi.com/docs/tutorials/streetlights) makes sense from an application view too - you're stating "to use this application publish new messages here, and if you want to see output you need to subscribe here".
Also, this means that you can continue (if acceptable) to use the spec to also describe the broker/event backbone - the inversion would be even more confusing - "this is a Kafka broker, any new messages will be published to the channel, but if you want to add a message you need to use the subscribe operation"
To me, while I think "subscribe to the published messages" makes sense, "produce to the subscribe endpoint" is harder to wrap your head round as a new user:
asyncApi: 2.0.0
channels:
newOrders:
subscribe:
summary: 'Send new orders here'
orderResult:
publish:
summary: 'Results are output here'
What use cases have you seen where the inverse is needed for applications?
Or - is the problem fundementally regarding code generation - that tools need to be able to understand the perspective (i.e generating mocks of applications, vs generating consumers)?
In my case, I have dozens of applications. Each one of them has a unique YML file describing what each application generates (publish) or ingest (subscribe). So the YML file is application centric. It allows for discovery of the API's (description of the services) and generation of code for applications. As a developer, this helps me keeping track of who does what and who needs what. I am the user of those YAML files :)
I, too, see an AsyncAPI document as formalizing the (message-based) contract of an application, and i think it is wise not to depart from this clear application-centric perspective almost accidentally, as a side-effect of clarifying the meaning of publish
and subscribe
. Maybe all that is needed is to replace publish
and subscribe
with more unequivocal terminology, maybe even emphasizing the application-centric nature of an AsyncAPI document, such as
- replace
subscribe
withapplicationSent
- replace
publish
withapplicationReceived
.
(This could work well with a solution to #415 .)
Code generation has a lot to do with why I opened this issue. We usually don't use these files to generate message brokers, we use them to generate client applications. So if I were thinking about what I want the application to do, I'd hope that the app would publish messages in the publish operations.
I can understand why the spec was written this way, there have been numerous discussions, my understanding (and Fran will I'm sure correct me because I'm probably wrong) is that they were trying to adopt the same perspective that OpenAPI does, which is to say, when OpenAPI exposes a GET, and you're generating server code, it's not the server doing the GET.
But the thing is, in EDA there's no server doing server-side code, there are just applications. The brokers are just plumbing. So does the document say what one application is doing in relation to another application, one publishes and one subscribes, but we're looking at it from the other application's perspective? Or is it the broker's point of view?
Thank you for helping me understand the challenge of the different perspectives. I'm interested in the "socialising" side of AsyncAPI - the ability to describe "data sources" rather than "applications".
Example use case: I have a series of different types of sensors and I want to provide access externally to a combined stream of data.
From an (internal) application perspective - I would write a spec that says "this application publishes sensor data to channel x
with payload y
". And then application developers for each sensor type implements the applications with a common payload. In that case, the spec would be "provider" view.
But what I want to broadcast is the availability of the data source - "Channel x
contains a stream of data from ALL the different sensors" so that others can then write their own applications that subscribe to that data.
I could externally publish a "client" view application that describes the pseudo application - but my worry is that going application-centric means that this use case becomes more fiddly to articulate - the "it's not an application, but instead a collection of data from many applications".
I guess the bigger question is whether this is an acceptable use case for AsyncAPI at all?
My suggestion is to keep it simple. To my thinking, the subscribe:
attribute should relate to behavior that happens when a service/app/person subscribes to the given channel. In other words, it should receive messages off of the subscription. The 'publish:
attributes should relate to what happens when a service/app/person publishes a message into a channel. Right now I add suffixes to my operationId
values to reflect this intention like so:
channels:
wisesayingNeeded:
subscribe:
operationId: onWiseSayingNeededSub
message:
$ref: "#/components/messages/wiseSayingNeeded"
publish:
operationId: onWiseSayingNeededPub
message:
$ref: "#/components/messages/wiseSayingNeeded"
Presently, the current result is that after the code is generated I need to actually switch the generated functions calls around in order for incoming messages to be routed to the subscribe handler. In terms of the publish handler, I'm still not getting any behavior.
Here my case that's motivating me to ask for the simple approach I describe: https://github.com/asyncapi/nodejs-template/issues/44
@reselbob : what you describe is the current meaning of publish
and subscribe
: https://github.com/asyncapi/spec/blob/master/spec/asyncapi.md#channelItemObject
Thanks, @GeraldLoeffler . The problem for me is that when I auto-generate the code using the Node-template incoming messages from the subscription go to the function I associate with the operationId
in the publish
attribute. Thus, I have to twiddle with the generated code to make things line up. The impact for me is that I was planning to demonstrate the code autogeneration feature in an article I am writing. But, until this gets fixed, the article is on hold. Same for my demonstration project in Katacoda. If you need me to provide a video that demonstrates the problem, please let me know.
I have run into the confusion about publishing or subscribing with code generation and been thinking a lot about this lately, and I am ready to chime into the discussion. I am mostly working with broker architectures (Kafka, I've done some MQTT) but I think I also have enough of a handle on some of the other models (serverless applications, websockets) that are in use. I've also been bothered by the limitation that any asyncapi spec can only describe one component's point of view, even though event-driven systems are usually a bit of a spider's web when you see their architecture diagrams.
As I see it, payloads need to be decoupled from channels, and we're missing a piece of vocabulary that I'll call "actor". (It's missing because OpenAPI doesn't have it, there's only a client and a server, we don't need to introduce a load of different players. It also doesn't really have much of a concept of who can do what, it describes all the operations available in the system.)
Each "actor", so a client/application/whatever thing, then has payloads that it sends or receives to other actors. This covers the brokerless setups (such as websockets, which really will just be a client and a server and they are the inverse of one another but that's a special case IMO) or microservices, where the order service might receive payloads from another component and then send specific payloads to the stock and billing systems.
What this does not cover is the brokered setups, like Kafka or MQTT. To describe these systems, our actors need to be sending to or receiving from channels rather than other actors. Let's keep the channel descriptions but without the publish/subscribe stuff attached (this needs to be different per-actor), and without the payloads being attached. Then each actor has data associated with it that describes which payload it can send where - either to a channel, or to an actor.
What's either neat or really confusing about this is that now we can have one AsyncAPI specification that describes a whole system, and a full picture of which actors do what with which data. It removes the confusion about publish and subscribe (this is difficult with the brokers especially because "both" is always the answer) and means that things like generator tooling probably need to take an actor as a parameter so that they again only represent one point of view.
So to recap:
payloads describe the data packets, with whatever binding-specific context is needed, but they're independent.
channels the topics/channels and other broker-hosted pipes to put data into and out of but no other information like pub/sub or payload.
actors :sparkles: new addition :sparkles: represent the various system components (microservice, client, application), have their own metadata and then have a collection of items that combine:
- direction (publish/subscribe)
- connection (id of an actor, or a channel)
- payload
This makes it clear which thing does what, in which direction, for as many components as we want to include. It does kind of add a third dimension to a two-dimensional existing idea but, well, this is my suggestion.
Our customers (Solace) continue to struggle on this topic, and I am seeing a proliferation of different ways people are using the spec in unintended ways. While on the surface this shows flexibility, the problem is that they end up with unforeseen consequences when it comes to tooling. I think many people in this thread have brought up reasonable approaches, but it feels like a working group should be formed to determine the best approach to move forward? How can we collectively drive this forward? Thank you @lornajane for your proposal!
This is the result of the discussion between @lornajane and me during the first episode of Thinking Out Loud.
Key takeaways
- A distributed architecture is a web of nodes. Each AsyncAPI file must describe only a single node.
- We shouldn't try to guess what a client can or can not do based on the definition of a server. A client is its own node and should be defined separately.
-
publish
would mean the application is sending a message andsubscribe
would mean the application is receiving a message. - Alternatively to point 1, an AsyncAPI file should allow us to define the whole architecture and not just a single application/node. Here come the
actors
. - Actors would allow us to define the nodes of our architecture and their relationship with the channels or other actors.
- In the special case of a client/server relationship, clients would publish to or subscribe from a server actor, and vice-versa.
Example of AsyncAPI file
asyncapi: '3.0.0'
actors:
website-client:
description: This is the web application.
publish:
- user_signup_intent
website-server:
description: This is a WebSockets server.
subscribe:
- user_signup_intent
publish:
- user/signedup
email-service:
description: Sends welcome emails to users.
subscribe:
- user/signedup
channels:
user/signedup:
message:
$ref: '#/components/messages/UserSignedUp'
user_signup_intent:
message:
$ref: '#/components/messages/UserSignUpIntent'
components:
messages:
UserSignedUp:
description: This message is sent from the WebSockets server to the broker.
payload:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
UserSignUpIntent:
description: This message is sent from the browser to the WebSockets server.
payload:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
First off, I thoroughly enjoyed the thinking loud conversation between @lornajane and @fmvilas It was an honest and open dialogue.
I have had a lot of confusion understanding and trying to adopt the async api spec refer to my slack conversation https://asyncapi.slack.com/archives/CQVJXFNQL/p1625749552465500
I liked @lornajane's idea of a single file to define the full aysnc api architecture
My Proposal
Basis
My proposal for the spec is based on the fact that channel
is core communication mechanism that enables the actors publish
or subscribe
to message payloads.
I envision the spec as a list of channels
, which have a message payload schema described and can have actors that can publish or subscribe or do both (websockets use case).
My idea is a slight modification to Lorna's idea where I propose actors / roles
be tied to the specific channel.
I propose we introduce two attributes called publishers
and subscribers
that are arrays and are nested as part of the channel
attribute.
An example of an async api spec would be:
asyncapi: '3.0.0'
channels:
user/signedup:
message:
$ref: '#/components/messages/UserSignedUp'
publishers:
- website-client
subscribers:
- website-server
- email-server
order/placed:
message:
$ref: '#/components/messages/OrderPlaced'
publishers:
- website-server
subscribers:
- logistics
- shipping
- customer-service
components:
messages:
UserSignedUp:
description: This message is sent from the WebSockets server to the broker.
payload:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
OrderPlaced:
description: This message is sent from server when an order is placed.
payload:
type: object
properties:
orderId:
type: string
description: Name of the user
amount:
type: float
format: currency
description: Order amount
Advantages
Channel as the core concept
As a consumer of this spec when I look at a channel, I know the following a. Name of the channel b. Schema of the messages that are going to be published / subscribed on this channel c. Actors who are going to subscribe / publish on this channel
Infrastructure as code use case
A spec like this can be used by to standup the required messaging infra structure. With infrastructure as code gaining popularity, I would like to generate my kafka brokers, sqs queues, sns topics, rabbitmq queues on the basis of this spec. Going a step further we could have a terraform tool that can generate the infrastructure as code based on the async api spec.
Solution for Code generation / tooling.
For a tool to generate the necessary code , the tool can be passed parameters like the channel name and actor name and based on this the tool can generate code based on the role played by the actor
Example.
Consider the above file as an example. website-server
is a consumer of the UserSignedUp
messages but is also the publisher of the OrderPlaced
messages.
Let's say we want to generate the code for website-server
, our tooling can take the asyncpi file and the actor name, in this case the website-server
as arguments.
<code-generator-tool> --file asycnapi.yaml --actor website-server
Since in the above file website-server
is listed as the subscriber
for user/signedup
channel, the tool will know to generate the subscriber
code.
Similarly since website-server
is listed as the publisher
for the order/placed
channel, the tool will know to generate the publisher
code.
The advantage is that using the actor
passed as an argument to the tool, one can generate all the necessary code for that particular actor
even though the actor
is a publisher
in certain cases and a subscriber
in certain cases
Code organization
@fmvilas mentioned that sometimes it is not manageable to have a huge asyncapi file. We can get around that problem as well, by breaking down the spec file into the grain of a channel A channel, along with its message contract and list of subscribers and publishers array can be defined in a single file. So for an organization with a large number of channels would have multiple files and each application uses the file(s) it is interested in based on the channels.
It however does not solve the problem of where to locate these files. Again it is upto the team adopting the spec. They could create duplicates of it have copies of it on the subscribers and publishers code repo OR They could house it in a single repo and use it as a git submodule
An even simplified version of the spec
The heart of the spec's aim in my mind is to define the channels
and the contract of the message payloads
being published on them.
The perspective of the actor
is more from a tooling standpoint of the spec.
With that in mind what if we removed the concept of actors
and just have the channels and message payloads.
From a tooling perspective when I pass it the asycn api spec , i need to pass in the channel
and the role
<code-generator-tool> --file asycnapi.yaml --channel user/signedup --role=publisher
From this the tooling now knows that it needs to generate a publisher
code for the channel user/signedup
A simplified spec would then look like
asyncapi: '3.0.0'
channels:
user/signedup:
message:
$ref: '#/components/messages/UserSignedUp'
order/placed:
message:
$ref: '#/components/messages/OrderPlaced'
components:
messages:
UserSignedUp:
description: This message is sent from the WebSockets server to the broker.
payload:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
OrderPlaced:
description: This message is sent from server when an order is placed.
payload:
type: object
properties:
orderId:
type: string
description: Name of the user
amount:
type: float
format: currency
description: Order amount
Again these are my initial thoughts, open to feedback
I think this is great progress. I agree that a purely logical Channel
concept, independent of the actors that use the Channel or how they use them is key to improving the spec.
To that end, I would argue:
- Nesting actors within channels, and explicitly naming them creates a tight coupling that I feel like could be handled better using actor -> channel references (as suggested by @lornajane ). The actions of the actors would be defined within the actors themselves . This leaves Channels to be just logical pipes.
This setup slightly reduces the ability to see "Actors who are going to subscribe / publish on this channel". You would instead have to walk the dependencies to see the actors. However, in a large enterprise, there are multiple files anyways, and that advantage is minimized.
-
publishers
andsubscribers
are associated with a pub-sub model of interaction. Whereas there are many async interactions that are not pub-sub (point to point, queueing through brokers). I like @geraldloeffler suggestion (slightly modified by me) of changing this toactorSends
andactorReceives
. By genericized the terms to only signify who is putting data into the pipe and taking data out of the pipe, we can accommodate many more use cases without twisting the meaning of publish and subscribe.
- 2021.07.21 - Updated to use @iancooper suggested
endpoint
, rather thanactor
Root of the Issues
In v2.1 an AsyncAPI spec describes how other components may interact with the focal component. The focal component is assumed to implement the inverse of what is listed in the spec. In other words, if interacting components can subscribe from the focal component, the spec assumes that focal component publishes those messages. The 1:1 view of the world is a result of AsyncAPI’s efforts to strongly parallel OpenAPI. The single point of view doesn’t cause issues in OpenAPI because in a sync system, as there's only a client and a server, assuming the server is the inverse of the client is generally accurate.
However, event-driven systems are not exclusively (or even predominantly) 1:1 interaction. In fact, async architectures that use event brokers are typically a network of decoupled nodes interacting in complex ways to achieve desired outcomes. Here the assumption of inverse relationships fails, as components can be:
- Not strictly clients or servers, but rather both.
- Not interact 1:1 with other components, but rather in a one-to-many-fashion, serving as information provider to multiple components, while simultaneously serving as information consumer from multiple components.
- Selectively choose which events to receive from information providers
Addressing the issue by adding Endpoint
Both channel reuse and the perspective issues can be addressed by:
- Removing the tight coupling between components and channels that is implicitly created by including “publish” and “subscribe” information in the channel declaration.
- In its place, introduce a concept of an “endpoint” to AsyncAPI. Endpoint represents either a single component, or a generic stereotype (interface) of a class of components. The endpoint concept explicitly defines, from the perspective of the endpoint:
- What operation (channel + interaction + binding) does/can the Endpoint implement? o Using a reference, what channels does the endpoint interact with? o How does this endpoint interact with the channel?
- For each endpoint-specific operation, which endpoint-specific bindings are present? o For example, if an endpoint get messages from a queue subscribes to a topic, what is the name of the queue, and the quality of service?
The resulting AsyncAPI structure looks like:
The introduction of “endpoint” and the resulting loose coupling addresses both the perspective and the channel reuse issue. Because endpoints are defined from their own perspective, using intuitive verbs, the new user experience is vastly improved and code generation becomes less problematic. And by decoupling endpoints from channels by using references, channels can be reused by multiple endpoints with different styles of interaction.
Further Abstract the Channel concept
The current verbs in v2.1 (publish and subscribe) are tightly coupled to the pub-sub model of async interaction. Queues, mailboxes, websocket endpoints all can serve as asynchronous channels, but do not require either publishing or subscribing. This proposal
- Replaces the
subscribe
verb withendpointSends
- Replaces the
publish
verb withendpointReceives
This change further abstracts the “Channel” concept away from any implementation choice. The implementation details can in turn be specified as bindings, either attached to the channel or the endpoint-operation, depending if they apply to the channel, or to an individual endpoint’s use of the channel.
On a practical level, further abstraction of Channel allows point-to-point asynchronous communication, either brokered or unbrokered, to be represented by Channels. This in turn allows the spec to remain consistent across a wider range of interaction styles.
Emphasize multiple physical realizations of underlying logical model
In the following examples, the endpoint, channel and message entities are presented as separate files. This is solely for the sake of consistency. The use of references allows for a wide range of implementation options including
- Separating endpoints, channels and messages into separate files
- Combining all entities into a single file
- Dividing up entities by business domain
- Utilizing a schema registry or event management portal in lieu of files The spec has no opinion on what the best way to organize. In fact, a single organization may use different physical AsyncAPI representations of the same underlying set of logical async interactions. And, depending on use case, an organization may choose to present only a piece of the overall architecture to an endpoint for reasons of security, simplicity, etc.
Differentiate Interface and Implementation
Currently AsyncAPI defines how a component MAY interact with the focal component. This reflects the intention of the document to serve as a true API. For consistency, this convention is continued in this proposal.
That said, a separate and legitimate use of AsyncAPI is generating and documenting a particular endpoint’s implementation. OpenAPI (and REST) is tightly coupled to the HTTP/S protocol, so that verbs, URL, etc. can be assumed to be implemented using HTTP/S. In contrast, the asynchronous world uses a wide variety of protocols. Evidence of this is seen by provision of the specific binding information present in many AsyncAPI files and the growing number of code generation options. The need to explicitly provide implementation information in the asynchronous world forces a more explicit spec that allows for implementation details to be recorded in a systematic way. To accommodate this reality, this proposal:
- Adds an optional field called intent to Endpoint
o Indicates whether the intent of this Endpoint definition is an interface (in essence defining of class of Endpoint, rather than the implementation of a particular Endpoint), or documenting a particular implementation.
o If the intent is implementation, all operations present in the file will be assumed to be implemented. o If the tag is not present, the assumption will be that the intent is an interface. - Adds an optional field called required tag to operations
o If true, endpoints which implement the interface MUST implement the operation.
o If not present, required is assumed to be false.
Clarify Inheritance rules when using references
To achieve decoupling, the proposed spec relies heavily on references. This (and the introduction of separate implementation and interface concepts) requires the spec to formalize the requirements with regards to inheritance when referencing another entity. In the case where Entity A references another entity B (endpoint, channel, message), Entity A MUST inherit all the properties of Entity B (and likewise all the Entities that have been previously referenced). Entity A MAY modify a property of Entity B (and previously referenced entities) ONLY IF ONE OR MORE OF THESE CONDITIONS ARE TRUE:
- Endpoints may choose exclude optional operations (those not marked as required:true)
- There are parameters within the property (e.g. parameterized topic string). If parameter is restricted via type, enumeration or pattern, Entity A MUST follow the restrictions.