Closed fmvilas closed 10 months ago
@fmvilas Awesome proposal! I love it β€οΈ However, I have some problems with some parts of this proposal, maybe some things are obvious for you and for other people, but some things surprise me and I think their assumptions are wrong.
Why do you want to define the message(s) in the channel, not as currently in the operation level? Consider the first example you gave:
asyncapi: 3.0.0
channels:
userSignedUp:
address: user/signedup
message:
payload:
type: object
properties:
email:
type: string
format: email
operations:
sendUserSignedUpEvent:
channel: userSignedUp
action: send
sendUserSignedUpEvent
operation? This problem is more pronounced in the case of Override channel messages with operation-specific messages
case.servers
(and remotes
field) with names of the existing servers. I guess that given channel is only available for particular server/remote. Ok... but what in case when someone want to define multiple operations (with receive
kind) for the different servers?send
and receive
operation (we have still possibility in the 2.X.X
AsyncAPI). If you define the message as oneOf
, then how to guess which message is for send
and which for receive
?I think that in this case, message(s) should be only defined on the operation, not on the channel. Channels should be then treated only as "interface", which you must "implement" by the operations (also if you need by multiple operations). For me, in the current proposal, the operation is "hardcoded" to the given channel by the message. If I'm not right, please clarify the message
field on the channel, because it really doesn't fit for me.
I like idea to clarify that given server is remote and application is connecting to it and another server "runs" the app, but... did you consider having kind
in the Server object? Then we don't need the remotes
Object. Also as I see, the remotes have this same shape as servers, so kind: remote
(or remote: true
) is enough for me.
I mentioned about it the first section:
... In other examples you defined on the channel the
servers
(andremotes
field) with names of the existing servers. I guess that given channel is only available for particular server/remote...
but I think that you should clarify this thing in the proposal, because I know that you copied this from existing PR, but people can be surprised π
Here I'm just going to think out loud because I'm not an expert in this topic. Maybe instead of an extra action type like request
and the replay
field we should reuse receive
action and add the word operations
or then
, that would determine what should happen when a given operation is processed and whether it "produces" subsequent operations? Something like, if the payment in the e-shop was successful, send (reply to the broker) an operation that saves record in the database - otherwise create another operation that does rollback etc. This way we could support many more techniques than just request/reply.
Example:
operations:
checkPayment:
channel: payment
action: receive
then:
2XX: ... # "reply" operation when given operations success.
4XX:... # "reply" operation when given operations fails.
In the example above, I use HTTP statuses to describe the state of the operation: success or failure. Probably something better can be invented.
It seems more generic to me. The idea itself is taken from CQRS.
ππΌ for the proposal. If we go along with this (and I think we will after a dozen or so revisions), then every tool will have to be re-written from scratch π
Thanks for the quick feedback, Maciej! π
Why do you want to define the message(s) in the channel, not as currently in the operation level?
At least 2 reasons come to my mind:
Channels should be then treated only as "interface", which you must "implement" by the operations (also if you need by multiple operations).
Precisely. Interfaces let you define the "shape" of the implementation and that's what we're doing here.
For me, in the current proposal, the operation is "hardcoded" to the given channel by the message. If I'm not right, please clarify the message field on the channel, because it really doesn't fit for me.
How is that hardcoded? We're just standardizing what messages can go through a channel and I think this is actually one of the biggest values. It's exactly how it is today. In v2, the message is tied to an operation and a channel at the same time in the same place.
How can I guess that by sending a given message the application underneath will process the sendUserSignedUpEvent operation? This problem is more pronounced in the case of Override channel messages with operation-specific messages case.
Mind clarifying this? I'm not sure I understand your question/concern.
Go ahead. In other examples you defined on the channel the servers (and remotes field) with names of the existing servers. I guess that given channel is only available for particular server/remote. Ok... but what in case when someone want to define multiple operations (with receive kind) for the different servers?
Sorry, that was just an experiment I was doing and forgot to delete it before publishing. Thanks for the heads up.
What in case when for given channel you wanna define send and receive operation (we have still possibility in the 2.X.X AsyncAPI). If you define the message as oneOf, then how to guess which message is for send and which for receive?
You can define as many operations as you want in a given channel :)
operations:
mySendOperation:
channel: myChannel
...
myReceiveOperation:
channel: myChannel
...
myOtherReceiveOperation:
channel: myChannel
...
I like idea to clarify that given server is remote and application is connecting to it and another server "runs" the app, but... did you consider having kind in the Server object?
I did, and instantly discarded it π When you're referencing a server or a remote, you can clearly know by the URL if it's either a server o a remote. E.g.:
$ref: 'common.asyncapi.yaml#/components/servers/production' # I know it's a server just by looking at the URL.
$ref: 'common.asyncapi.yaml#/components/remote/production' # I know it's a remote just by looking at the URL.
In general, just because two things have the same shape, it doesn't mean they have to be merged. Structurally they look the same but semantically they're completely different and subject to evolve in different directions in the future.
Maybe instead of an extra action type like request and the replay field we should reuse receive action and add the word operations or then, that would determine what should happen when a given operation is processed and whether it "produces" subsequent operations?
Not a concern of this proposal (my example was just to showcase that something can be done) but I'll bear with you. It seems what you're trying to define there is workflow language. And that's a totally different thing altogether π
Thanks a lot, Maciej! I hope this clarifies things a bit more :)
I think this is a very positive proposal and helps with a lot of the issues around re-use of channels in typical microservice documentation etc.
A quick initial question. My understanding is server is something we have and remote is something we use. With that assumption, should there be an explicit connection between server/remote and channel? If i a channel becomes a reusable asset that I can define in a central file, where it is shared by producer and consumer, where is that channel hosted? Do I want to define the remote that hosts my channel in every file, even if I don't re-declare the channel there to somehow indicate they are hosted there?
On Identifiers, I think there may be a generally useful concept here. Having been working with $ref whilst I try and put together an SNS/SQS binding, it struggles where you want to imply resource uses resource b - defined elsewhere - instead of pull in definition of resource b.
@fmvilas Thanks for explanation. Some things are clear but message
at the channel level doesn't fit me π
In v2, the message is tied to an operation and a channel at the same time in the same place.
Yes, I agree but currently you define given message(s) for particular action/operation (publish/subscribe), even if you have defined explicit operation in the channel level. I can understand the reason for having messages in the channel level, but that's the biggest problem to understand: whether a given message(s) by default is/are associated with send
or receive
operation?
Also you wrote:
You can define as many operations as you want in a given channel :)
Yep, but you may end up with a message defined at the channel level that fits only one operation, and for the rest you have to define the message at the operation level. So what it value for the message in the channel level? This can only cause problems in understanding what message an operation takes.
If an operation will have the ability to define messages which will override those on the channel level then I can accept this, but I opt for having messages only on the operation level. Can you give me an episode of ThinkingOutLoud
or a discussion in which a channel-level message was proposed? Thanks!
A quick initial question. My understanding is server is something we have and remote is something we use. With that assumption, should there be an explicit connection between server/remote and channel? If i a channel becomes a reusable asset that I can define in a central file, where it is shared by producer and consumer, where is that channel hosted? Do I want to define the remote that hosts my channel in every file, even if I don't re-declare the channel there to somehow indicate they are hosted there?
There's an ongoing proposal for that. I didn't want to include it here on purpose to avoid bloating this issue so much: https://github.com/asyncapi/spec/pull/531
Yes, I agree but currently you define given message(s) for particular action/operation (publish/subscribe), even if you have defined explicit operation in the channel level. I can understand the reason for having messages in the channel level, but that's the biggest problem to understand: whether a given message(s) by default is/are associated with send or receive operation?
That's the whole point. Not everyone is interested in operations. Many just want a menu of channels and messages and they don't care who sends or receives them.
So what it value for the message in the channel level? This can only cause problems in understanding what message an operation takes.
The value is the one I described above. Those who want a menu of channels and their associated messages. Many people don't want to describe applications but their sources of data (topics aka channels).
If an operation will have the ability to define messages which will override those on the channel level then I can accept this
Yes. An operation has the ability to override a channel's message
definition. Now the question is, should we allow to completely override it? Or should the operation's message
be a subset of the channel's message
? π€ The first gives you more freedom but potentially overcomplicates things. The second is more restrictive but gives you consistency.
Can you give me an episode of ThinkingOutLoud or a discussion in which a channel-level message was proposed? Thanks!
It wasn't proposed like this explicitly but here's a conversation with @dalelane about it: https://youtu.be/Qsu_yC-5YYM?t=535. I recommend you watch the whole episode.
Is there a reason we have a channels
field in the root AsyncAPI object, would it not be enough to define it inside the operation object and components? π€
The reason behind this is that I hate you have to manually match channel id's, if it can be avoided.
Gonna go with your examples to show the changes, remember in this case $ref
can be switched out completely with the channel object itself if you don't want to use that feature.
This will help us in tooling, as:
And it still enables all the same things, but without complexity.
common.asyncapi.yaml
filebackend.asyncapi.yaml
filefrontend.asyncapi.yaml
filenotifications-service.asyncapi.yaml
filecomments-service.asyncapi.yaml
fileDo you see any reason we would not do this? π€
Is there a reason we have a channels field in the root AsyncAPI object, would it not be enough to define it inside the operation object? π€
Because many people don't want to use AsyncAPI to define applications but a menu of channels, messages, servers, etc. Systems like event gateways are not interested in operations at all but in which channels and messages are available.
Because many people don't want to use AsyncAPI to define applications but a menu of channels, messages, servers, etc. Systems like event gateways are not interested in operations at all but in which channels and messages are available.
@fmvilas forgot to add components
as well. Components still give you the menu? Of course, the channels now become secondary, instead of one of the primary attributes of the AsyncAPI file but it still enables the menu.
We can of course just keep the channels
field, to enable the primary behavior. But I see no reason why operations must manually define the matching of channel id's. I just really want to remove this (manual) runtime matching of channels
if possible π¬
@jonaslagoni You can read my comment about matching channels to the servers and why operating on the id (names) of the server/channel is better option rather than refs (and plain Server/Channel Object) -> https://github.com/asyncapi/spec/pull/531#issuecomment-887738308
@fmvilas Thanks, I'm starting to see that. With the proposal, we have an options:
Additionally, if messages were not defined at the channel level, but only at the operation level, we would have to have a tool that would retrieve these messages from operations - most likely we will have to have such a tool due to the fact that operations will be able to override messages from the channel.
Yes. An operation has the ability to override a channel's message definition. Now the question is, should we allow to completely override it? Or should the operation's message be a subset of the channel's message? π€ The first gives you more freedom but potentially overcomplicates things. The second is more restrictive but gives you consistency.
The second option is better and more consistency. I don't know how to handle situation when you have defined on the channel level the 5 messages (by oneOf
) and in operation you wanna say that only second operation is handled, or all other without this second...
@jonaslagoni you're right. Actually, the channels
field in the root object is not for "menu" purposes but to define which channels this application is making use of, so forget my previous comment. I see not blocking reason for what you're saying. One reason I can think of is that, to know which channels are being used in an application, we'd have to scan all the operations first. Also, by having this matching we make it mandatory to reuse channel definitions, making it more difficult for people to duplicate stuff. An idea that comes to my mind is that we can disallow dereferencing there and instead you can only use $ref
but that complicates things on the tooling side. And another reason is that we'd lose the channel id after dereferencing. Not sure if it's a problem though.
In general, I'd not worry about tooling so much. Let's put the focus on the user experience. If it's better for the user, cool let's do it, if it's not, let's change it. Our decisions should be driven by users and not tooling difficulty/complexity. That said, there may be some good for UX in what you're proposing, especially that it would work with existing JSON Schema tools in editors.
Additionally, if messages were not defined at the channel level, but only at the operation level, we would have to have a tool that would retrieve these messages from operations - most likely we will have to have such a tool due to the fact that operations will be able to override messages from the channel.
@magicmatatjahu Yes, we'd need such a tool but some people might not need it. E.g., those who don't use the operations
keyword.
The second option is better and more consistency. I don't know how to handle situation when you have defined on the channel level the 5 messages (by oneOf) and in operation you wanna say that only second operation is handled, or all other without this second...
Maybe we should make the channel's message
work with message ids? This way we enforce messages having ids, which is another feature request: https://github.com/asyncapi/spec/issues/458. It could look something like this:
channels:
myChannel:
address: my/channel
messages:
- myMessage
- myMessage2
- myMessage3
operations:
sendMyMessage2:
channel: myChannel
messages:
- myMessage2
messages:
myMessage:
payload:
...
myMessage2:
payload:
...
myMessage3:
payload:
...
This would make it possible to check if myMessage2
is part of the channel's messages
definition.
@fmvilas Yes, it's a solution. However I see in this solution and with referencing the channels by $ref
(not by id/names) some problems (@jonaslagoni).
If I have a situation when a given broker has in "menu" some channel and then I connect this channel in operation (in some app connected to broker), how do I know if a channel is unique in the system? To describe this in more detail, I will use an example. I have one broker and one application that is connected to the broker:
broker.yaml
fileapp1.yaml
fileHow can I tell that a particular channel is unique in the system and that is what it describes operations to? Even if we use channel identifiers, we still have a problem, because the identifier itself in application 1
may be different than the identifiers in the broker, where it is defined:
broker.yaml
fileapp1.yaml
fileIt seems to me that the operation and the channel in its object should have something like operationID
and channelID
and these values should be unique in the whole system - then we have an easy way to refer to channels.
broker.yaml
fileapp1.yaml
fileThe same problem occurs with the messages, how do I know if a message is unique in the system?
I deliberately did not use the channel's address
because it can change (as was mentioned by Fran) and the channelID itself should not.
Maybe I'm talking crap and it's not necessary π
EDIT: Ok π on second thought, address can be treated as a channelID
and references make sense here.
@magicmatatjahu I think you're getting a few things wrong here. Let me recap:
# broker.yaml
asyncapi: 3.0.0
info:
title: Broker
version: 1.0.0
servers:
brokerServer:
...
channels:
someChannel1:
...
someChannel2:
...
This definition is not semantically valid. This is not a menu, it's an application definition. Menus can only use components
. That's the reason we now have components.channels
. This is how you'd define a menu:
# broker.yaml
asyncapi: 3.0.0
info:
title: Broker
version: 1.0.0
components:
servers:
brokerServer:
...
channels:
someChannel1:
...
someChannel2:
...
Also, your broker is not a server of your applications but a remote so it should be like this:
# broker.yaml
asyncapi: 3.0.0
info:
title: Broker
version: 1.0.0
components:
remotes:
broker:
...
channels:
someChannel1:
...
someChannel2:
...
Now we got a broker menu. Let's define app1.yaml:
asyncapi: 3.0.0
info:
title: App1
version: 1.0.0
remotes:
broker: # Notice how I'm using a remote and it's referencing a remote too.
$ref: 'broker.yaml#/components/remotes/broker'
channels:
channel1: # Notice this Id is different than the one in broker.yaml. Ids are local to the AsyncAPI file.
$ref: 'broker.yaml#/components/channels/someChannel1'
channel2:
$ref: 'broker.yaml#/components/channels/someChannel2'
operations:
someOperation1:
channel: channel1
someOperation2:
channel: channel2
This is how you know that App1 is using channel1
and channel2
, because they're being used in the channels
object. You don't need unique ids in the whole system because their ids are local to the file and that's ok. channel1
and channel2
are the same as someChannel1
and someChannel2
respectively. I just chose to use a different id to illustrate that ids are local. Does it make sense?
@fmvilas Thanks for explanation, it makes sense π
Even if you are using the local id of the channels, you should have information (especially in tooling like our cupid) that the given channel is unique and for this you can use the channel's address
and then we have connections:
channel1 -> broker.yaml#/components/channels/someChannel1 -> channel's address
I edited my previous comment, maybe you read it too late.
Yes, the address can be the global id.
I think this is a very positive proposal and helps with a lot of the issues around re-use of channels in typical microservice documentation etc.
A quick initial question. My understanding is server is something we have and remote is something we use.
I agree with @iancooper on both points, it's a positive proposal and I have concerns about the definition of server and remote. Because depending on the perspective, the same underlying object is a both a server
and a remote
.
For example: a websocket client connecting to a node.js application that spawns an internal websocket server. The node.js application considers the websocket server an server
. The client considers the same websocket server to be an remote
.
Concerns
server
and an remote
even though its the same underlying entity remote
and server
need to be updated in lockstep. remote
and aserver
, which is not entirely obvious, along the lines of the current publish/subscribe perspective issue.This seems contrary our aims of addressing the perspective issue, the re-use issue, and moving implementation specific information out of core objects.
Alternative
It also seems that given that this is largely a protocol-specific issue, the answer lies in protocol-specific bindings. @fmvilas points out there are code generation implications here--websocket implementations don't know whether to generate an internal websocket server or a client-connection to an external broker. As an alternative, I would suggest that there be separate websocket bindings (call it websocket-client
and websocket-server
) that hint to the code generator how to implement the particular scenario. This would seem to address the concerns raised in the most minimal way possible.
This extends the (key in my mind) effort to make the core objects of AsyncAPI as close to purely logical as possible. Servers
house node-level connection information and can be grouped together into Environments
. These concepts are applicable to all async interactions.
Is it backward-compatible?
Absolutely not.
Concerns I think this deserves a lot of thought and discussion. As @GeraldLoeffler states, as a 2.1 specification, there is an expectation from both our community and general public that the spec is relatively stable. And many find the current spec to be perfectly suitable for their use cases. My feeling would be incompatible changes should be the last resort.
Internally I know a lot of large (=> less agile) enterprises that have coded to the 2.1 spec. To have that code be incompatible with 3.0 would likely decrease AsyncAPI's momentum.
And yes, we can maintain separate 2.x and 3.x branches, but we all know the challenges of maintaining two versions of code, even in the medium term. This is especially true when there's a young spec with an imbalance between tooling needs and developers to implement them.
Alternative Given that v2.1 makes very specific assumptions about the world (e.g. it describes the perspective of the application, the client is the mirror image of the application), in the absence of v3.0 terminology, the new version "falls back" to the v2 interpretation of objects. I feel like this is similar in spirit to @lornajane OpenAPI's introduction of websockets--if websockets aren't included, the typical behavior of OpenAPI is observed.
Because depending on the perspective, the same underlying object is a both a server and a remote.
Alright. You gave me an idea and I changed the proposal. Now components
only have servers
and not remotes, since a remote is a server in the end. We still keep the root remotes
object because βfrom the point of view of the applicationβ it still makes sense. And let's not forget that an AsyncAPI file defines an application unless it only has a components
object.
The websocket server needs to be defined twice in AsyncAPI as both a server and an remote even though its the same underlying entity
That's not true. A remote can reference a server object. It was also like this before I made the change described above. Actually, my "microservices" example shows this possibility when the frontend is using a remote that's referencing a WebSocket server definition.
As we migrate through environments, the definitions of remote and server need to be updated in lockstep.
That should be solved by my latest change π
We need to explain to new developers what the difference is between a remote and a server, which is not entirely obvious, along the lines of the current publish/subscribe perspective issue.
We needed this change anyway, even in version 2.
We've introduced a top level element that is not applicable to a large subset of async implementations.
Which one? π€
It also seems that given that this is largely a protocol-specific issue, the answer lies in protocol-specific bindings.
I'm using WebSockets as examples but it has nothing to do with WebSockets only. HTTP servers, HTTP2 SSE, GraphQL subscriptions, and more will have the same problem. It has nothing to do with the protocol but with the architecture design. Some interactions are client/server and some are broker-based. When it's client/server, we need to make it clear if the server is "something we're exposing" or "something we're connecting to". I don't think it's practical nor elegant to have bindings like websockets-cilent
, websockets-server
, http-server
, http-client
, graphql-client
, graphql-server
, and so on. That actually sounds like a huge hack/patch to me.
And yes, we can maintain separate 2.x and 3.x branches, but we all know the challenges of maintaining two versions of code, even in the medium term.
I don't think we'd even need to maintain two branches of the spec but instead, we can and have to maintain tooling compatibility with the two major versions. Until v2.x gets deprecated and we don't support it anymore but that can be in 1 or 2 years (or whatever we decide). If someone wants to stay in v2, cool! we'll keep supporting it. If you want to get the most out of the spec and tooling, migrate to v3. I think it's a fair and common thing.
As @GeraldLoeffler states, as a 2.1 specification, there is an expectation from both our community and general public that the spec is relatively stable.
This is especially true when there's a young spec with an imbalance between tooling needs and developers to implement them.
Are we young or stable? π I think we're still young and it's true we're gaining momentum but that shouldn't stop us from evolving without ending up with a Frankenstein specification with tons of band-aids just not to break backward compatibility. The users would love to have something that's clear and beautiful to use. IMHO, it's a problem of tooling vendors (including us) to migrate the tools to give the best experience to the users.
Regarding imbalance between tooling needs and developers to implement them, we're working on this. At Postman we're hiring a bunch of people to work exclusively on AsyncAPI. Maybe other companies should follow. Also, the community is growing super fast so that imbalance we'll soon be equilibrated.
I'm still digesting all of this, so this isn't a very considered response from me.
One initial thought jumped out at me though: How do we represent distributed multi-broker systems like Kafka?
e.g. If I have a Kafka cluster with three brokers I might start with:
remotes:
broker1:
url: broker1.myhost.com
protocol: kafka
broker2:
url: broker2.myhost.com
protocol: kafka
broker3:
url: broker3.myhost.com
protocol: kafka
That gives me three brokers to have to refer to elsewhere/in other specs, which feels clunky.
I know we've discussed this idea of groups of servers before, so I think it would be good to resolve this issue as part of a jump to 3.0
It doesn't necessarily have to be over-engineered. Kafka uses bootstrap addresses which are made of combining the broker URLs into a list, so we could do something like that.
remotes:
mycluster:
url: broker1.myhost.com,broker2.myhost.com,broker3.myhost.com
protocol: kafka
or
remotes:
mycluster:
urls:
- broker1.myhost.com
- broker2.myhost.com
- broker3.myhost.com
protocol: kafka
It sort of breaks the idea of this really being a "url", but it does simplify things:
Alright. You gave me an idea and I changed the proposal. Now
components
only haveservers
and not remotes, since a remote is a server in the end. We still keep the rootremotes
object because βfrom the point of view of the applicationβ it still makes sense. And let's not forget that an AsyncAPI file defines an application unless it only has acomponents
object.
That improves re-use, but harms the user experience. I can envision explaining the difference between a server
and a remote
to a new developer, and then having to then explain why remote
then refers to a server
component. It's very reminiscent of the perspective issue.
As we migrate through environments, the definitions of remote and server need to be updated in lockstep.
That should be solved by my latest change π
Agreed, but I don't think in an optimal way.
We need to explain to new developers what the difference is between a remote and a server, which is not entirely obvious, along the lines of the current publish/subscribe perspective issue.
We needed this change anyway, even in version 2.
I agree that AsyncAPI needs a way to indicate client or server code implementation. But that does not require the introduction of server
and remote
verbiage.
We've introduced a top level element that is not applicable to a large subset of async implementations.
Which one? π€
More accurate wording would be βredefinesβ. This proposal redefines server
from βhigh-level connection informationβ which is widely applicable to all async use cases to βconnection information, but only for internally spawned servers, but only when the application acts as server for that particular functionβ. The redefined object is applicable to a greatly reduced subset of async use cases.
I don't think it's practical nor elegant to have bindings like
websockets-cilent
,websockets-server
,http-server
,http-client
,graphql-client
,graphql-server
, and so on. That actually sounds like a huge hack/patch to me.
I would say its practical and common to separate server and client implementations. For example, many jars don't contain both the client and server for a particular technology. And keeping separation of between logical objects and implementation seems like an elegant, non-hacky concept that allows the spec to be vendor neutral and extensible, and allows end users the ability to switch implementations without altering the logical structure of their architecture. But if a proliferation of bindings is a concern, perhaps a third option would be to add a flag to applicable bindings indicating whether the binding is acting as a client or a server.
And yes, we can maintain separate 2.x and 3.x branches, but we all know the challenges of maintaining two versions of code, even in the medium term.
I don't think we'd even need to maintain two branches of the spec but instead, we can and have to maintain tooling compatibility with the two major versions. Until v2.x gets deprecated and we don't support it anymore but that can be in 1 or 2 years (or whatever we decide). If someone wants to stay in v2, cool! we'll keep supporting it. If you want to get the most out of the spec and tooling, migrate to v3. I think it's a fair and common thing.
I hope that v2 continues to receive support, but that my experience is that shiny new things get all of the attention to the detriment of those who committed to earlier versions of the spec.
Are we young or stable? π
"v2.1" projects stability. Radical changes from version to version projects "young". The dissonance between the two needs to be considered.
How do we represent distributed multi-broker systems like Kafka?
@dalelane I think your proposal #465 makes sense and should be included in v3. Actually, everything that's a breaking change should land in v3 and we should be aware of all of them before we release v3, just so we don't release v4 some months later π Let's keep this issue focused on publish/subscribe and its associated problems.
That improves re-use, but harms the user experience. I can envision explaining the difference between a server and a remote to a new developer, and then having to then explain why remote then refers to a server component. It's very reminiscent of the perspective issue.
I understand your concern but I think this remote/server difference is way easier to understand than the problem with publish/subscribe. We could argue the same about channels
. We have to explain it because some people call them "topics", some "event name", some "routing keys", etc. We also had to explain it and it's ok so far. As long as what we're explaining is easy to grasp and reason about, I don't see a problem.
I agree that AsyncAPI needs a way to indicate client or server code implementation. But that does not require the introduction of server and remote verbiage.
I'm sorry but I think introducing verbiage like [protocol]-client
and [protocol]-server
in bindings is actually worse because you still have these concepts but replicated among a bunch of protocols. @magicmatatjahu proposed having a kind
attribute that differentiates both and I think it's starting to make more sense now that components/remotes
is not a thing anymore. Something like:
servers:
wsBackendServer:
url: ...
kind: local # Defaults to "remote" if not specified.
mosquitto:
url: ...
My only argument against it was that it was easier to read something like $ref: 'common.asyncapi.yaml#/components/remotes/mosquitto'
and automatically know it's a remote because it's in the URL. But since components/remotes
is not a thing anymore this argument doesn't apply anymore.
The redefined object is applicable to a greatly reduced subset of async use cases.
You may be right but this change is meant to serve as the base ground for the future vision, which does not only focus on async stuff.
It should be possible to define multiple remotes for the same environments. For example, Kafka cluster will consist of number of remote urls:
The above set of remotes (that logically forms a Kafka cluster) will be repeated for different environments (dev, prod).
Also we should be able to specify some common remote systems (for example Secret manager) per environment. 'servers' can de defined under the environment as well.
Do you think a concept of 'environment' should be introduced on the root level?
environment:
localDev:
servers:
# servers here
remotes:
broker:
# url, protocol, etc
schemaRegistry:
# url, protocol, etc
connect:
# url, protocol, etc
secretManager:
# url, protocol, etc
staging:
servers:
# servers here
remotes:
broker:
# url, protocol, etc
schemaRegistry:
# url, protocol, etc
connect:
# url, protocol, etc
secretManager:
# url, protocol, etc
@ekozynin I think this is an interesting proposal that deserves its own separate discussion. Would you mind opening a new issue so we keep this one focused on solving the publish/subscribe confusion? π
I am currently developing a websocket communcation system by using AsyncAPI specs. This seems to be the right discussion regarding some issues I see.
Remotes.
Remotes: A remote is a remote server our application will connect to
There should be no our
. We design an API. When we implement it we may implement for example a websocket server or websocket clients or both. Therefore remote doesn't make much sense, because it takes the perspective of a client which may be not ours (or even is never ours).
Operations and messages
When I design an OpenAPI spec, I have combinations of paths and methods that are associated with operationIds, e.g. createEntry, that refers to operations createEntry(). The resource is an Entry. This name createEntry makes complete sense on the server side and on the client side. I then especially generate code for the client that will contain a createEntry().
An AsyncAPI spec should allow the automatic generation of server and client code.
A resource in OpenAPI world is losely related to message in AsyncAPI world. E.g. in a websocket connection a client may subscribe and unsubscribe to new entries and will then receive datagrams for new entries. The client therefore will send a subscribe message, an unsubscribe message.
Therefore operations createSubscription, deleteSubscription available to clients in generated client code would be appropriate in OpenAPI world and also in AsyncAPI world. These names would be at least OK in the producer/server domain. A handler function createSubscription() in the server, that handles subscription requests coming from future consumers would be OK.
That said it seems that an operationId createSubscription that is associated with the message for a subscription would be acceptable. Maybe two operationsId s createSubscription and onCreateSubscription that are both associated with that message would be even better.
Channels
What is a channel for example in a websocket system? Currently it is one connection.
I have defined a channel for entries. This is one websocket connection. In this channel a subscription message is send by the consumer and the entry message is send by the producer. But the unsubscribe message is send over the same channel, too. I don't want to open multiple connections for that.
Therefore I want to associate multiple messages with on channel. With the natural association of one (or two) operations per message, it seems that it would be good to have multiple messages associated with one channel (and not via oneOf) and (at least) one operation associated with every single message.
In your draft I don't see that option:
asyncapi: 3.0.0
channels:
userSignedUp:
address: user/signedup
message:
payload:
type: object
properties:
email:
type: string
format: email
operations:
sendUserSignedUpEvent:
channel: userSignedUp
action: send
A channel has one address (i.e. URL, i.e connection with websockets) and still has one message associated. An operation is associated with the channel and not with the message.
Proposal
I propose to associate multiple messages (array, not one of) with one channel and associate one operation with one message in one channel.
On the question of one or two operations per channel/message combination I am torn. It works fine with one for me in OpenApi world, though.
@magicmatatjahu proposed having a
kind
attribute that differentiates both and I think it's starting to make more sense now thatcomponents/remotes
is not a thing anymore.
@fmvilas, that seems reasonable to me. Thanks, @magicmatatjahu !
I just updated the issue with the latest feedback about the kind
attribute on the Server Object. I'm having some troubles there and would like your help folks. Here's the issue:
In the common.asyncapi.yaml
file, I want to define the WebSocket server and the Mosquitto broker. With the latter, everything is fine but with the former, I can't define if it's a remote
or local
server because that would vary depending on who's referencing it. An example:
components:
servers:
websiteWebSocketServer:
url: ws://mycompany.com/ws
protocol: was
kind: local # It will be local for backend.asyncapi.yaml but not for the rest.
mosquitto:
url: mqtt://test.mosquitto.org
protocol: mqtt
My first thought was to leave the kind
field empty there and add it as follows on the servers
section of each file:
servers:
websiteWebSocketServer:
$ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer'
kind: local # Or remote, depends on the file
However, that's an illegal use of the Reference Object ($ref
).
Any ideas @magicmatatjahu @jmenning-solace?
@joerg-walter-de thanks for the feedback. Let me go point by point:
There should be no our. We design an API. When we implement it we may implement for example a websocket server or websocket clients or both. Therefore remote doesn't make much sense, because it takes the perspective of a client which may be not ours (or even is never ours).
As strange as it may seem, an AsyncAPI file doesn't represent an API but an Application. It's cool to have things separated by API and implementation but we still need to know what an Application is implementing when generating docs for users, generating code, using AsyncAPI as a config file for a framework, or when using it for a Gateway. Not being clear with this is precisely what led us here. I highly recommend you watch this video: https://www.youtube.com/watch?v=YixYuYCmyJs&list=PLbi1gRlP7pigPxRRylHGCvpdppYLmSKfJ&index=4.
An AsyncAPI spec should allow the automatic generation of server and client code.
Lorna explains it well in the video linked above. That's not even 100% possible on OpenAPI either and that's an issue they've been discussing for some time now.
However, it's always easier in OpenAPI because they have a clear restriction there: the communication is always 1:1 (server and client). That's not usually the case in AsyncAPI. It may be with WebSocket but as soon as you have a broker, everything is a client except the broker who acts as a server.
What is a channel for example in a websocket system? Currently it is one connection.
In raw WebSocket, every channel name (or address
in this proposal) is a connection. Other solutions are to have just a single address
(such as /
) and a oneOf
in the channel messages. In this proposal, you'd have multiple operations on the same /
channel. I recommend this series of blog posts by @derberg https://www.asyncapi.com/blog/websocket-part1
@fmvilas I see this problem, however in the JSON Schema 2019-09 draft, there is an option to define other keywords (and override it) alongside of some object - https://json-schema.org/draft/2019-09/release-notes.html
I know that JSON Reference =/= JSON Schema and also I cannot find any other information in the web that it's a valid solution for every JSON Reference, but if it is, we can use it and change the behaviour of our Reference Object :)
@Relequestual Hi! Sorry for pinging you, but you are an only person which should dispel our doubts π We have one question, you don't need read all thread. Does using other keys next to $ref and overriding them work for every case, or is this just a feature in JSON Schema > 2019-09 draft? What is a status for it in normal JSON Reference?
Our Reference Object should be no different than JSON Schema one. My only concern is that we'd have to also upgrade to Draft 2019-09 but that's something we'd have to do at some point anyway. @Relequestual how is the state of tooling support with versions above Draft 7?
Does using other keys next to $ref and overriding them work for every case, or is this just a feature in JSON Schema > 2019-09 draft? What is a status for it in normal JSON Reference? - @magicmatatjahu
Prior to 2019-09, other keywords alongside $ref
must be ignored, not overidden, and that's an important distinction.
@fmvilas There is some support for 2019-09 and above, but not as broadly as we would like. Most languages.
In the
common.asyncapi.yaml
file, I want to define the WebSocket server and the Mosquitto broker. With the latter, everything is fine but with the former, I can't define if it's aremote
orlocal
server because that would vary depending on who's referencing it. An example:
@fmvilas I would say that kind
shouldn't be on the server
object, exactly because of what you're encountering. kind
varies relative to the connection a particular application
is making to a server
. In many use cases a server
will be both kind: local
and kind: remote
Instead, make kind
a property of the binding on the operation, not the server.
server.yaml
asyncapi: 3.0.0
servers:
production:
url: wss://api.gemini.com
protocol: wss
channels:
v1MarketDataSymbol:
address: /v1/marketdata/{symbol}
parameters:
...
operations:
onMarketSymbolUpdate:
action: receive
channel: v1MarketDataSymbol
description: Receive market updates on a given symbol.
binding:
wss
kind:local
client.yaml
asyncapi: 3.0.0
servers:
production:
url: wss://api.gemini.com
protocol: wss
channels:
v1MarketDataSymbol:
address: /v1/marketdata/{symbol}
parameters:
...
operations:
onMarketSymbolUpdate:
action: send
channel: v1MarketDataSymbol
description: Send market updates on a given symbol.
binding:
wss
kind:remote
Instead, make
kind
a property of the binding on the operation, not the server.
@jmenning-solace There are a few things with this approach:
kind
property to every binding. That proves it's not something specific to the protocol and therefore should be in the core spec.kind: local
and kind: remote
". Well yeah, a server can be both local and remote depending on the application that is referencing it but it will never be both things from the same AsyncAPI file. I mean, if your application exposes a local server it will never be remote for your application. And if it connects to a remote server, it will never be local for your application.For the reasons exposed above, given that the behavior of additional properties in the JSON Schema $ref
object is not overriding the referenced object, and tooling support for newer versions of JSON Schema "is not yet there", I think we should revert back to having a remotes
object in the root of the document. Having a kind
property in the Server Object doesn't seem viable as it will break reusability.
To clarify, I don't mean we should have remotes and servers in components
. Just a remotes
object in the root of the file that will contain references to server definitions. @magicmatatjahu thoughts?
For the reasons exposed above, given that the behavior of additional properties in the JSON Schema $ref object is not overriding the referenced object, and tooling support for newer versions of JSON Schema "is not yet there", I think we should revert back to having a remotes object in the root of the document. Having a kind property in the Server Object doesn't seem viable as it will break reusability.
ππΌ Good for me. We can back to this proposal with kind
when tolling will support newest versions of JSON Schema, but... if we will go with remotes
then we'll stay with remotes until the end π
To clarify, I don't mean we should have remotes and servers in components. Just a remotes object in the root of the file that will contain references to server definitions. @magicmatatjahu thoughts?
As we discussed in previous comments, you did not want to have the definition of Remote Object and Server Object identical in order to extend remote in the future. I didn't exactly follow the whole discussion and maybe I missed something, but Remote Object will be identical as Server Object?
if we will go with remotes then we'll stay with remotes until the end π
Yeah, most probably. I don't see it as something bad actually.
but Remote Object will be identical as Server Object?
There will not be any Remote Object. The remotes
field in the root of the document will be a map of <string, Server Object> instead. It's the same as having kind
but outside the server definition.
2. If we choose to go this way, we'll have to add the
kind
property to every binding. That proves it's not something specific to the protocol and therefore should be in the core spec.
Not true. Look through the bindings already defined: Solace, JMS, IBM MQ, Kafka, AMQP, SNS, SQS, AnyPoint. None of these will ever need kind
because they will always be remote
. No code generator or human is going to assume that it needs to spin up some sort of internal IBM MQ server. Or some sort of local Kafka cluster.
To the contrary, remote
/server
distinction is very protocol specific. It applies primarily to websockets and http (and maybe a couple others which I've missed) . remote
/server
ties something very confusing that applies to a subset of use cases onto core concepts that should be purely logical.
3. will never be both things from the same AsyncAPI file
Correct. But again the manifestation of AsyncAPI isn't limited to files, and especially a single file. As you correctly state, we need libraries of components, whether that is in file form or a more abstract registry. So we continue on this path we either have:
remotes
and servers
which are the same object defined twice (anti-reuse)There is one thing that's bothering me a bit, and that is not using $ref to refer to a channel from an operation. I got what you said about not wanting to create a $ref to user/signedup, but if we're going to give each channel its own identifier, separate from the address, why can we do a $ref to '/channels/userSignedup'?
I believe we have a number of use cases:
Do we need to ensure we have this number of concepts, and an optional field on the channel definition of host to indicate who, if anyone hosts you. This might mean explicitly defining broker as an object, so that you could create a Binding Object against it, if you wanted to add some configuration around how you created or used it.
I wonder if we are asking remote/server to do too much work here
Not true. Look through the bindings already defined: Solace, JMS, IBM MQ, Kafka, AMQP, SNS, SQS, AnyPoint. None of these will ever need kind because they will always be remote. No code generator or human is going to assume that it needs to spin up some sort of internal IBM MQ server. Or some sort of local Kafka cluster.
To the contrary, remote/server distinction is very protocol specific. It applies primarily to websockets and http (and maybe a couple others which I've missed) . remote/server ties something very confusing that applies to a subset of use cases onto core concepts that should be purely logical.
@jmenning-solace Totally agree. I now see your point π
why can we do a $ref to '/channels/userSignedup'?
@damaru-inc Cause after dereferencing it, we'd lose the id of the channel. We've seen this to be a problem in tooling in multiple places.
channels:
userSignedup:
address: '/user/signedup'
...
operations:
myop:
channel:
$ref: '/channels/userSignedup'
would become
channels:
userSignedup:
address: '/user/signedup'
...
operations:
myop:
channel:
address: '/user/signedup' # <------- The channel id is lost
...
And as you can see we lose the id of the channel. We have it on the channels map but that would require us to deep compare objects to guess which one is the one we're using.
This might mean explicitly defining broker as an object, so that you could create a Binding Object against it, if you wanted to add some configuration around how you created or used it.
I wonder if we are asking remote/server to do too much work here
@iancooper You got me curious. Mind expanding on this? Maybe some rough examples?
@iancooper You got me curious. Mind expanding on this? Maybe some rough examples?
Sure, not this weekend, but early next week I'll try to rough something out for you.
@iancooper You got me curious. Mind expanding on this? Maybe some rough examples?
@iancooper +1 on this from me.
@fmvilas you did a great job! but we are far from ending π
I'm reading this proposal for the first time, from the beginning to the end with all 40 comments. Before, I only looked at it twice. The first time when it was created, but I saw remotes
and I was like nah, I'll look at it another time
. The next time was when Fran asked for a solution for a problem with kind
next to $ref
, I could relate to it without knowing what the proposal is about.
Some detailed feedback and thoughts:
Glad we ended up with kind
so far. My only concern is vocabulary here, this local
thingy. My brain immediately thinks: localhost
, my local machine
, I got it all locally
.
I understand that kind
means that the AsyncAPI specifies if communication is done with a remote
server or AsyncAPI specifies that described app is actually a server. Shouldn't we then say self
rather than local
?
@jmenning-solace wrote:
Not true. Look through the bindings already defined: Solace, JMS, IBM MQ, Kafka, AMQP, SNS, SQS, AnyPoint. None of these will ever need kind because they will always be remote.
correct me if I'm wrong, but my understanding of kind
, and also understanding of other changes in the proposal is to enable others to use AsyncAPI to basically describe not only a server/consumer/client but also a broker. I can have an AsyncAPI file without operations
, only servers
and channels
- what else do I need to provide a basic description of a broker?
If I'm right, then I actually can have a situation when Kafka server is described as kind: local
(or kind: self
).
I might be missing something, that was a lot of text to digest.
Please remove them from this sentence The example above shows how we explicitly reference the resources (remotes, servers, and channels)
as it really confused me when reading just a proposal without going into the comments section.
I like it, that is it, pub/sub confusion is a good reason for a change.
It starts like this:
asyncapi: 3.0.0
info:
title: Website Backend
version: 1.0.0
servers:
websiteWebSocketServer:
$ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer'
mosquitto:
$ref: 'common.asyncapi.yaml#/components/servers/mosquitto'
These are 2 servers for completely 2 different protocols. In 2.1 you cannot have this, as it basically means that the same channels are available on both servers. This proposal also doesn't solve https://github.com/asyncapi/spec/pull/531
So are you just thinking with shortcuts, and assume https://github.com/asyncapi/spec/pull/531 ends up in 2.2 and by the time 3.0 comes, channels already can specify on what servers they are? or?
operationId
s and description
sI like that this proposal addresses this problem. This was confusing in code generation. You just need a different id for the server and a different one for the client, same with description π€·πΌ
The side effect, users need more than just one AsyncAPI file if they want to achieve more. Reusability is cool to make it simpler, but can't we try to figure out something to keep it in one file?
In the below example I only took operations
and info
from both, backend and frontend:
entities:
frontend:
title: Website WebSocket Client
version: 0.1.0
backend:
title: Website Backend
version: 1.0.0
operations:
#backend
onCommentLike:
action: receive
channel: likeComment
description: When a comment like is received from the frontend.
entities:
- backend
onCommentLikesCountChange:
action: receive
channel: commentLikesCountChanged
description: When an event from the broker arrives telling us to update the comment likes count on the frontend.
entities:
- backend
sendCommentLikesUpdate:
action: send
channel: updateCommentLikes
description: Update comment likes count in the frontend.
entities:
- backend
sendCommentLiked:
action: send
channel: commentLiked
description: Notify all the services that a comment has been liked.
entities:
- backend
#frontend
sendCommentLike:
action: send
channel: likeComment
description: Notify the backend that a comment has been liked.
entities:
- frontend
onCommentLikesUpdate:
action: receive
channel: updateCommentLikes
description: Update the UI when the comment likes count is updated.
entities:
- frontend
entities
yeah, I guess the name sucks, subject for change. More important is the concept of one doc for all than multiple docsinfo
objectchannel
in operation
, we can make it a standard way and use it for referencing entities
@jmenning-solace wrote:
an imbalance between tooling needs and developers to implement them.
@fmvilas
Maybe other companies should follow.
This is exactly what for AsyncAPI Initiative is not just about specification but also tooling. This is exactly why AsyncAPI Initiative joined Linux Foundation to assure works are done on a neutral ground. This is exactly why we have an open governance model that assures the power is in people and not any single company.
We have a great foundation to work on things together. There is obviously a huge risk that vendors will need time to catch up with the new version. This should not be a blocker for spec evolution though but rather a call for action to work jointly on the development of libraries that do not bring profit. For example, we have so many Java-using companies, but no Java Parser work is being done under AsyncAPI Initiative or any other neutral foundation. This means that everyone implements their own Java Parser - huge duplication of efforts.
Whenever I use AsyncAPI as a user, to document some existing API or to design an API, or whenever I think on EDA...
I always think about a message
first.
we anyway need to add support for MessageId in some way (https://github.com/asyncapi/spec/issues/458) + This is kinda cool that I think once we have messages
on the root, I do not need channels
for WebSocket (especially those that have one connection and then subprotocol for further message exchange)
So I would love to see messages
on the root, and then reference messages, just like we reference channel in operation, or server under channel (proposal for 2.2). It will be a consistency-wise approach.
@fmvilas mentioned this problem with $ref
and kind
next to it
servers:
websiteWebSocketServer:
$ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer'
kind: local # Or remote, depends on the file
the only solution I see is what @magicmatatjahu proposed for similar case in another proposal. We just need to have all details other than kind
nested under another object:
servers:
websiteWebSocketServer:
server:
$ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer'
kind: local # Or remote, depends on the file
I understand that kind means that the AsyncAPI specifies if communication is done with a remote server or AsyncAPI specifies that described app is actually a server. Shouldn't we then say self rather than local?
Yes, it also sounds better to me. Especially, because self
would be "the application" and that's what an AsyncAPI document defines.
correct me if I'm wrong, but my understanding of kind, and also understanding of other changes in the proposal is to enable others to use AsyncAPI to basically describe not only a server/consumer/client but also a broker. I can have an AsyncAPI file without operations, only servers and channels - what else do I need to provide a basic description of a broker?
In this case, you can have them in the components
object. In this proposal, we keep the AsyncAPI document related to a specific application. The only exception is when you only define asyncapi
, info
, and components
. In this case, it becomes a library/menu/collection, which is perfect to describe a whole broker. I should probably move this to another proposal.
Please remove them from this sentence The example above shows how we explicitly reference the resources (remotes, servers, and channels) as it really confused me when reading just a proposal without going into the comments section.
Done. Thanks!
So are you just thinking with shortcuts, and assume #531 ends up in 2.2 and by the time 3.0 comes, channels already can specify on what servers they are? or?
Yes. This proposal is only to solve publish/subscribe confusion. I thought including servers/remotes discussion would be worth it because they also affect the perspective. By no means this is an AsyncAPI 3.0.0 draft, although v3.0.0 would have this included.
That said, I should probably remove the library/menu/collection concept from this proposal too.
The side effect, users need more than just one AsyncAPI file if they want to achieve more. Reusability is cool to make it simpler, but can't we try to figure out something to keep it in one file?
In the below example I only took operations and info from both, backend and frontend:
Can we have this discussion separately? I don't think it affects the publish/subscribe confusion. It is more about having the AsyncAPI file to define a single application or multiple applications.
So I would love to see messages on the root, and then reference messages, just like we reference channel in operation, or server under channel (proposal for 2.2). It will be a consistency-wise approach.
Agree π It would also solve #458.
the only solution I see is what @magicmatatjahu proposed for similar case in another proposal. We just need to have all details other than kind nested under another object:
Well, it's not the only solution. You can also have another object called remotes
like this:
servers:
websiteWebSocketServer:
$ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer'
remotes:
mosquitto:
$ref: 'common.asyncapi.yaml#/components/servers/mosquitto'
This brings me to another thought that I had about your proposal, @jmenning-solace. If we add kind
to the server bindings, then reusability is broken because we can add kind
in components/servers
too. I know, you suggested it to be part of operation bindings but then the problem is that we'll have to be repeating it all the time for all the operations of a server. To follow your example, this is how it would look like:
asyncapi: 3.0.0
servers:
production:
url: wss://api.gemini.com
protocol: wss
channels:
v1MarketDataSymbol:
address: /v1/marketdata/{symbol}
parameters:
...
operations:
onMarketSymbolUpdate:
action: receive
channel: v1MarketDataSymbol
description: Receive market updates on a given symbol.
bindings:
wss
kind:local
onMarketSymbolCreate:
action: receive
channel: v1MarketDataSymbol
description: Receive market creation on a given symbol.
bindings:
wss
kind:local
onMarketSymbolDelete:
action: receive
channel: v1MarketDataSymbol
description: Receive market deletion on a given symbol.
bindings:
wss
kind:local
As you can see, we have to duplicate bindings -> wss -> kind:local
all the time in each operation. Also, that would tie the operation to a specific server. We want operations to be reusable across servers. E.g., development, staging, test, production, etc.
I can't think of a better proposal than the one about having a root remotes
object tbh π€· Tho, I'm still open to suggestions.
Started a new issue about the scope of an AsyncAPI file: #628
As you can see, we have to duplicate
bindings -> wss -> kind:local
all the time in each operation.
@fmvilas . It's duplicative in your example, but only because the application only receives
messages on a local server. There is nothing to prevent an application from receiving messages on a local
wss server, doing business logic, then sending messages to a remote
server, whether that's another wss server, or a broker. Having kind
(or whatever term, I'm not a huge fan either), on the individual operation allows us to express that richness.
asyncapi: 3.0.0
servers:
production:
url: wss://api.gemini.com
protocol: wss
channels:
v1MarketDataSymbol:
address: /v1/marketdata/{symbol}
parameters:
...
operations:
onMarketSymbolUpdate:
action: receive
channel: v1MarketDataSymbol
description: Receive market updates on a given symbol.
bindings:
wss
kind:local
sendEnrichedData:
action: send
channel: v1MarketDataSymbol
description: Send enriched market creation on a given symbol.
bindings:
wss
kind:remote
Also, that would tie the operation to a specific server. We want operations to be reusable across servers. E.g., development, staging, test, production, etc.
This concern would seem best addressed by introducing the concept of environments (see #623 and #65 among others) . @dalelane and @GeraldLoeffler also have proposals related to this that I unfortunately can't locate at this moment. Environments would allow references between an operations and a logical server object, which then has a physical bindings defined per environment. The would allow operation bindings, which should stay consistent across environments (e.g. topic and queue names, remote/local implementation) to be reused. While things that vary across environments (URLs) can vary.
This touches on API lifecycle management, which I know @kinlane has been thinking a lot about.
It's duplicative in your example, but only because the application only receives messages on a local server.
Not true. I was lazy in my example but what I wanted to illustrate still applies:
asyncapi: 3.0.0
servers:
production:
url: wss://api.gemini.com
protocol: wss
channels:
v1MarketDataSymbol:
address: /v1/marketdata/{symbol}
parameters:
...
operations:
onMarketSymbolUpdate:
action: receive
channel: v1MarketDataSymbol
description: Receive market updates on a given symbol.
bindings:
ws:
kind:local
sendSomeInfoToClient:
action: send
channel: anotherChannel
bindings:
ws:
kind:local
sendEnrichedData:
action: send
channel: v1MarketDataSymbol
description: Send enriched market creation on a given symbol.
bindings:
ws:
kind:remote
Also, to what server is this operation pointing? It says "kind: local" but we may have different local servers on different ports. Really curious, how is this better than having the remotes
map along with the servers
map?
It's duplicative in your example, but only because the application only receives messages on a local server.
Not true. I was lazy in my example but what I wanted to illustrate still applies:
Are you trying to illustrate that having kind
on the operation is duplicative? Because in your updated example, kind
is not duplicative. It provides the proper level of granularity to indicate the relationship between operations and servers.
Also, to what server is this operation pointing? It says "kind: local" but we may have different local servers on different ports. Really curious, how is this better than having the
remotes
map along with theservers
map?
Correct, there is an assumption, which I probably should have made explicit, that there is a reference between the operation and the server
. Different operations for the same application may reference different servers
, whether within the same file, or a "library" file as you suggest earlier.
Operations-level bindings are better because we are trying to address the perspective problem and increase reuse. By contrast, having remote
and server
introduces another perspective problem (a server
is a remote
depending on who you are) and decreases reuse (the same underlying physical object needs to be defined as both a server
and a remote
)
(a server is a remote depending on who you are) and decreases reuse (the same underlying physical object needs to be defined as both a server and a remote)
I think this is solved by:
The only exception is when you only define asyncapi, info, and components. In this case, it becomes a library/menu/collection To clarify, I don't mean we should have remotes and servers in components. Just a remotes object in the root of the file that will contain references to server definitions.
(fyi @fmvilas it does currently state remotes
in components
in the proposal)
The presence of locals
dictates the need for implementer to create the local server (which can reference a library
or its own components
. This server can then be referenced by other applications using their own remotes
map (pointing to server from that document, or also from a library
).
It does mean that #628 requires a way of separating "applications" to make it clear which has the local
vs remote
reference, but with kind
you'd have to traverse operation(kind:local
)->channel->server to determine if that application should own the server.
Happy with the rename and perspective clarification.
This would work differently than it is now. Instead of defining the server as we do with OpenAPI (and AsyncAPI v2), we'd have to define how a client would look like.
Currently, in v2, the spec lets you describe the server and infer the client from this description (as with OpenAPI). However, this causes a discrepancy with servers, especially in production systems. For instance, my server may be listening on port 5000 because it's behind a proxy. The client should be sending requests/messages to port 80 and they'll be forwarded by the proxy. If we define our port as 80 in the AsyncAPI file and generate a client, everything is ok but if we generate a server, our code will try to listen in port 80, which is not what we want. This means we can't infer clients from server definitions.
The drawback is that we'd have to define 2 files, one for the server and one for the client but the benefit is that we can be 100% accurate with our intents. Tooling can help auto-generating a client definition from a server definition.
I'm not sure I follow - in v2, would I not just describe port 5000 in an internal API doc, and port 80 on the externally shared? Which is still defining 2 files. And tooling can infer intent by a configuration flag of (consume api
or provide api
). So for v3 - for public facing API - I would just define an application with remotes
with the right address - and for internal documentation it uses local
with the local address. Again, tooling can infer with a flag?
As someone who has been bitten by the pub/sub confusion (and has code 'out there' with it backwards...) I'm fully on board with this proposal (at least with the pub/sub/operation/channel/message changes... can't really comment on the local/server issue, as we tend to specify those at runtime)
Your revised hierarchy for the channel/operation/message also makes perfect sense, and is a much better model than previously. It actually models exactly how our code generator (and generated code) sees the world. We are in the 'defining a common set of reusable components' camp, and this reorganisation would make this much easier, and allows much more reuse.
fyi @fmvilas it does currently state remotes in components in the proposal)
@nictownsend Thanks! Lots of left-overs π It should be fixed now.
I'm not sure I follow - in v2, would I not just describe port 5000 in an internal API doc, and port 80 on the externally shared? Which is still defining 2 files. And tooling can infer intent by a configuration flag of (consume api or provide api). So for v3 - for public facing API - I would just define an application with remotes with the right address - and for internal documentation it uses local with the local address. Again, tooling can infer with a flag?
@nictownsend My point was that it's sometimes the same server. For instance, when you have nginx in the middle between the server and the client. The code of your app is listening to 5000 but people can reach out to your server in port 80 because there's a reverse proxy in the middle. In any case, this can be easily solved by an extension or a future property we could add, but what's really making us having to define two files (or twice in a single file if we opt for multi-application files) are things like description
, summary
, and operation Ids. A description like "Receives an event when a user signs up" is tied to a point of view and would only make sense from the subscriber's point of view. The publisher would want to have something like "Sends an event when a user signs up".
FYI I'm planning on moving this to a discussion or pull request, so it's easier to review and leave feedback. So far, it looks like we all agree that we need this to happen. We now have to discuss what's the best way to rebuild "servers" and I think we can discuss it better over a PR. Also please don't forget about #628. I think it's crucial these things are clearly defined.
Reading through the discussion started by @boyney123 on eventstorming, I think highlights the need for an endpoint concept within the spec itself, which is missing in this proposal.
The eventstorming tool (even in early stages) shows the value of understanding how endpoints interact with each other in an environment. But as the endpoints increase substantially (as they will in complex enterprise environments), forcing the need for a separate file for each endpoint will get cumbersome. This is also highlighted by @clemensv .
Not having an endpoint concept also eliminates the possibility of having inheritance, which could be particularly powerful in emerging Event API Product use cases.
I will echo @fmvilas that the discussion of #628 is an important one.
TL;DR: What's new?
kind
property. By default, all servers areremote
servers (e.g., brokers). However, if thekind
property is set tolocal
, that means the application is the one actually exposing the server (e.g., WebSocket or HTTP Server-Sent Events server).channels
object is optional. The only two fields that are required areasyncapi
andinfo
.servers
andchannels
can be defined insidecomponents
for reusability purposes. This way, an AsyncAPI document may only contain acomponents
object that can serve as an organization-wide menu of servers, channels, messages, etc.Abstract
For some years now, we've been discussing how to solve the publish/subscribe perspective problem. Some were arguing we should be giving priority to broker-based architectures because "they are the most common type of architecture AsyncAPI is used for". Although they're not wrong, by simply changing the meaning of
publish
andsubscribe
keywords, we'd be leaving many people and use cases out. We have to come up with a way to fix this problem without losing the ability to define client/server interactions too, which are especially useful for public WebSockets and HTTP Server-Sent Events APIs. And more importantly, we need a structure that will allow us to grow well and meet our vision.The latest Thinking Out Loud episodes with @lornajane, @dalelane, @damaru-inc, and @jmenning-solace have been key to better understand the problem. The intent of this proposal is to be a mix of the ideas and feedback I got from the community. I hope I managed to capture them well π
Foundational concepts
We gotta start from the beginning: an AsyncAPI file represents an application. This doesn't change. However, if no
channels
oroperations
are provided, the file becomes a "menu", a "library", a collection of reusable items we can later use from other AsyncAPI files.We have been very focused on the meaning of
publish
andsubscribe
but there is another key aspect of the spec that is confusing to many people:servers
. As it is right now, theservers
keyword holds an object with definitions of the servers (brokers) we may have to connect to. However, it also holds information about the server our application is exposing (HTTP server, WebSocket server, etc.) In some tools, we have been incorrectly assuming that if someone specifiesws
as the protocol, it means they want to create a WebSocket server instead of a WebSocket client. But what if someone wants to connect to a broker using the WebSocket protocol? This whole thing about the role of our application has been confusing all of us. As @lornajane pointed out on multiple occasions, an application can be both a server and a client, or just a server, or just a client. Therefore,servers
can't be made up of that mix. Exposed server interfaces and remote servers (usually brokers) have to be separated because βeven though they look the sameβ they're semantically different.Remotes vs Local Servers
This proposal introduces the concept of a
remote
orlocal
server. Remote servers are those our application has to connect to. They're usually brokers but can also be other kinds of servers.On the other hand,
local
servers are server interfaces our application exposes. Their URL defines where clients can reach them.Example
New
channels
andoperations
objectsAnother common concern related to the current state of
publish
andsubscribe
is the channel reusability problem we encounter because these verbs are part of the channel definition. To avoid this problem, we remove the operation verbs from the channel definitions and move them to their own root objectoperations
. Let's have a look at an example:There are a few new things here:
address
: it's the logical address where you can find this channel. Usually, this is the topic name, the routing key, the URL path, etc.userSignedUp
then? This is the channel identifier. It's an identifier that serves to reference the channel from another part of the document or another document.user/signedup
using JSON Pointer would beuser~1signedup
as opposed touser/signedup
like many people would think. That's highly unreadable and error-prone, therefore I'm introducing channel identifiers.channel
hints against which channel is this operation performed andaction
is the type of operation, i.e., we're "sending" or "receiving".sendUserSignedUpEvent
? It's the operation identifier. Yes, the oldoperationId
is now mandatory and it's implicit, i.e., theoperationId
field doesn't exist anymore.channel
a$ref
or JSON Pointer? To keep things simple. If you're referring to a channel here, it must be defined in thechannels
object. If we allow$ref
here, it means it can be dereferenced and therefore the channel ID would be lost. To make things easier and more consistent, this field is simply a string with the name of the channel ID.send
andreceive
? No, it's not that I'm hatingpublish
andsubscribe
already π I'm usingsend
andreceive
here to avoid confusion for those thinking that AsyncAPI is only meant to describe pub/sub architectures. I think "send" and "receive" are pretty common verbs that shouldn't be linked to any super-very-special meaning.Organization and reusability at its best
I'm adding two new objects to the
components
object:servers
andchannels
. Some may be already wondering "why? don't we haveservers
andchannels
already in the root of the document?". To understand this decision, I think it's better if I just describe the reusability model I have in mind.Reusability
Some people expressed their interest in having a "menu" of channels, messages, servers, etc. They're not really interested in a specific application. In other words, they want an organization-wide "library". This is the
components
object. It's now possible to do something like the following:This is now a valid AsyncAPI file and it can be referenced from other AsyncAPI files:
As you can see, I'm not defining any channel or server in this file but instead, I'm pointing to their definitions in the org-wide document (
common.asyncapi.yaml
). And this leads me to the other part of this section: "organization".Organization
The example above shows how we explicitly reference the resources (servers and channels) that our application is using. Having them inside
components
doesn't mean they are making any use of it. And that's key. The file is split into two "sections": application-specific and reusable items. Let's see an example:So for those of you who were wondering before "why? don't we have
servers
andchannels
already in the root of the document?": This is the reason, it's no different than it's right now in v2.x but I thought I'd make it more clear this time. Just because we put something incomponents
doesn't mean the application is making any use of it.Life is better with examples
Microservices
Say we have a social network, a very basic one. We want to have a website, a backend WebSocket server that sends and receives events for the UI to update in real-time, a message broker, and some other services subscribed to some topics in the broker.
We'll define everything that's common to some or all the applications in a file called
common.asyncapi.yaml
. Then, each application is going to be defined in its own AsyncAPI file, following the template{app-name}.asyncapi.yaml
.The
```yaml asyncapi: 3.0.0 info: title: Organization-wide stuff version: 0.1.0 components: servers: websiteWebSocketServer: url: ws://mycompany.com/ws protocol: ws mosquitto: url: mqtt://test.mosquitto.org protocol: mqtt channels: commentLiked: address: comment/liked message: ... likeComment: address: likeComment message: ... commentLikesCountChanged: address: comment/{commentId}/changed message: ... updateCommentLikes: address: updateCommentLikes message: ... ```common.asyncapi.yaml
fileThe
```yaml asyncapi: 3.0.0 info: title: Website Backend version: 1.0.0 servers: websiteWebSocketServer: $ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer' mosquitto: $ref: 'common.asyncapi.yaml#/components/servers/mosquitto' channels: commentLiked: $ref: 'common.asyncapi.yaml#/components/channels/commentLiked' likeComment: $ref: 'common.asyncapi.yaml#/components/channels/likeComment' commentLikesCountChanged: $ref: 'common.asyncapi.yaml#/components/channels/commentLikesCountChanged' updateCommentLikes: $ref: 'common.asyncapi.yaml#/components/channels/updateCommentLikes' operations: onCommentLike: action: receive channel: likeComment description: When a comment like is received from the frontend. onCommentLikesCountChange: action: receive channel: commentLikesCountChanged description: When an event from the broker arrives telling us to update the comment likes count on the frontend. sendCommentLikesUpdate: action: send channel: updateCommentLikes description: Update comment likes count in the frontend. sendCommentLiked: action: send channel: commentLiked description: Notify all the services that a comment has been liked. ```backend.asyncapi.yaml
fileThe
```yaml asyncapi: 3.0.0 info: title: Website WebSocket Client version: 1.0.0 servers: websiteWebSocketServer: $ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer' channels: likeComment: $ref: 'common.asyncapi.yaml#/components/channels/likeComment' updateCommentLikes: $ref: 'common.asyncapi.yaml#/components/channels/updateCommentLikes' operations: sendCommentLike: action: send channel: likeComment description: Notify the backend that a comment has been liked. onCommentLikesUpdate: action: receive channel: updateCommentLikes description: Update the UI when the comment likes count is updated. ```frontend.asyncapi.yaml
fileThe
```yaml asyncapi: 3.0.0 info: title: Notifications Service version: 1.0.0 servers: mosquitto: $ref: 'common.asyncapi.yaml#/components/servers/mosquitto' channels: commentLiked: $ref: 'common.asyncapi.yaml#/components/channels/commentLiked' operations: onCommentLiked: action: receive channel: commentLiked description: When a "comment has been liked" message is received, it sends an SMS or push notification to the author. ```notifications-service.asyncapi.yaml
fileThe
```yaml asyncapi: 3.0.0 info: title: Comments Service version: 1.0.0 description: This service is in charge of processing all the events related to comments. servers: mosquitto: $ref: 'common.asyncapi.yaml#/components/servers/mosquitto' channels: commentLiked: $ref: 'common.asyncapi.yaml#/components/channels/commentLiked' commentLikesCountChanged: $ref: 'common.asyncapi.yaml#/components/channels/updateCommentLikes' operations: onCommentLiked: action: receive channel: commentLiked description: Updates the likes count in the database and sends the new count to the broker. sendCommentLikesUpdate: action: send channel: commentLikesCountChanged description: Sends the new count to the broker after it has been updated in the database. ```comments-service.asyncapi.yaml
filePublic-facing API
Another common use case for AsyncAPI is to provide a definition of a public-facing API. Examples of this are Slack, Gitter, and Gemini.
This would work differently than it is now. Instead of defining the server as we do with OpenAPI (and AsyncAPI v2), we'd have to define how a client would look like. For instance:
Currently, in v2, the spec lets you describe the server and infer the client from this description (as with OpenAPI). However, this causes a discrepancy with servers, especially in production systems. For instance, my server may be listening on port 5000 because it's behind a proxy. The client should be sending requests/messages to port 80 and they'll be forwarded by the proxy. If we define our port as 80 in the AsyncAPI file and generate a client, everything is ok but if we generate a server, our code will try to listen in port 80, which is not what we want. This means we can't infer clients from server definitions.
The drawback is that we'd have to define 2 files, one for the server and one for the client but the benefit is that we can be 100% accurate with our intents. Tooling can help auto-generating a client definition from a server definition.
Further expansion
Override channel messages with operation-specific messages
The new
operations
object would help us define additional use cases, like those where we define what message is sent or received to/from a channel at the operation level. Let's see an example:Adding request/reply support
Adding support for the so-demanded request/reply pattern, would be as easy as adding a new verb and a
reply
keyword. See example:FAQ
Does it serve as a base ground to meet our vision without a future major version?
I think so. Whenever we're ready to support REST, GraphQL, and RPC APIs, this structure should perfectly serve as a base ground.
Does it enable for better reusability of channels and other elements?
Yes. It completely decouples channels from operations and even clarifies the reusability model of the specification.
Does it allow users to define their whole architecture in a single file?
No, but it allows users to have a "common resources" AsyncAPI file where most of the information can reside.
Is it backward-compatible?
Absolutely not.
Does it remove the publish/subscribe confusion without introducing a new confusing term?
Yes. We keep the basic terms as before with just a bit of reorganization.
Is it easy to define a broker-based microservices architecture?
Yes. I provided an example.
Is it easy to define a public asynchronous API?
Yes. I provided an example.
Does it set the base ground to define an RPC (Remote Procedure Call) system over a message broker?
Yes. I provided a basic non-normative example.
Does it set the base ground to define a point-to-point RPC (Remote Procedure Call) API?
Yes. I provided a basic non-normative example.