asyncapi / spec

The AsyncAPI specification allows you to create machine-readable definitions of your asynchronous APIs.
https://www.asyncapi.com
Apache License 2.0
4.22k stars 269 forks source link

Proposal to solve publish/subscribe confusion #618

Closed fmvilas closed 10 months ago

fmvilas commented 3 years ago

TL;DR: What's new?

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 and subscribe 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 or operations 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 and subscribe but there is another key aspect of the spec that is confusing to many people: servers. As it is right now, the servers 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 specifies ws 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 or local 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

asyncapi: 3.0.0

servers:
  test:
    url: ws://test.mycompany.com/ws
    protocol: was
    kind: local
    description: The application creates a WebSocket server and listens for messages. Clients can connect on the given URL.
  mosquitto:
    url: mqtt://test.mosquitto.org
    protocol: mqtt
    kind: remote # This is the default value
    description: The application is connecting to the Mosquitto Test broker.

New channels and operations objects

Another common concern related to the current state of publish and subscribe 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 object operations. Let's have a look at an example:

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

There are a few new things here:

Organization and reusability at its best

I'm adding two new objects to the components object: servers and channels. Some may be already wondering "why? don't we have servers and channels 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:

asyncapi: 3.0.0

info:
  title: Organization-wide definitions
  version: 3.4.22

components:
  servers:
    ...
  channels:
    ...
  messages:
    ...

This is now a valid AsyncAPI file and it can be referenced from other AsyncAPI files:

asyncapi: 3.0.0

info:
  title: My MQTT client
  version: 3.1.9

servers:
  mosquitto:
    $ref: 'common.asyncapi.yaml#/components/servers/mosquitto'

channels:
  userSignedUp:
    $ref: 'common.asyncapi.yaml#/components/channels/userSignedUp'

operations:
  sendUserSignedUpEvent:
    channel: userSignedUp
    action: send

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:

asyncapi: 3.0.0

##### Application-specific #####
info: ...
servers: ...
channels: ...
operations: ...
##### Reusable items ######
components:
  servers: ...
  channels: ...
  schemas: ...
  messages: ...
  securitySchemes: ...
  parameters: ...
  correlationIds: ...
  operationTraits: ...
  messageTraits: ...
  serverBindings: ...
  channelBindings: ...
  operationBindings: ...
  messageBindings: ...
###########

So for those of you who were wondering before "why? don't we have servers and channels 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 in components 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 common.asyncapi.yaml file ```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: ... ```
The backend.asyncapi.yaml file ```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. ```
The frontend.asyncapi.yaml file ```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. ```
The notifications-service.asyncapi.yaml file ```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. ```
The comments-service.asyncapi.yaml file ```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. ```

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

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.

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:

channels:
  commentLikesCountChanged:
    message:
      oneOf:
        - $ref: 'common.asyncapi.yaml#/components/messages/updateCommentLikes'
        - $ref: 'common.asyncapi.yaml#/components/messages/updateCommentLikesV2'

operations:
  sendCommentLikesUpdate:
    action: send
    channel: commentLikesCountChanged
    description: Sends the new count to the broker after it has been updated in the database.
  sendCommentLikesUpdateV2:
    action: send
    channel: commentLikesCountChanged
    message:
      $ref: 'common.asyncapi.yaml#/components/messages/updateCommentLikesV2'
    description: Does the same as above but uses version 2 of the message.

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:

channels:
  users:
    message:
      oneOf:
        - $ref: 'common.asyncapi.yaml#/components/messages/createUser'
        - $ref: 'common.asyncapi.yaml#/components/messages/userCreated'

operations:
  createUser:
    action: request
    channel: users
    message:
      $ref: 'common.asyncapi.yaml#/components/messages/createUser'
    description: Creates a user and expects a response in the same channel.
    reply:
      message:
        $ref: 'common.asyncapi.yaml#/components/messages/userCreated'

This is just an example. We should also consider dynamic channel names created at runtime and probably other stuff.

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.

magicmatatjahu commented 3 years 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.

The main problem - message(s) in the channel

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

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.

Splitting servers to the two groups

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.

Servers and remotes in the channel

I mentioned about it the first section:

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

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 πŸ˜„

Adding request/reply support

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 πŸ˜‚

fmvilas commented 3 years ago

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

iancooper commented 3 years ago

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.

magicmatatjahu commented 3 years ago

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

fmvilas commented 3 years ago

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.

jonaslagoni commented 3 years ago

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:

  1. You don't have to verify that operations have valid channels in runtime.
  2. From a parser perspective, you don't have to manually match operations with channels.

And it still enables all the same things, but without complexity.

The common.asyncapi.yaml file ```yaml asyncapi: 3.0.0 info: title: Organization-wide stuff version: 0.1.0 components: servers: websiteWebSocketServer: url: ws://mycompany.com/ws protocol: ws remotes: 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: ... ```
The backend.asyncapi.yaml file ```yaml asyncapi: 3.0.0 info: title: Website Backend version: 1.0.0 servers: websiteWebSocketServer: $ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer' remotes: mosquitto: $ref: 'common.asyncapi.yaml#/components/servers/mosquitto' operations: onCommentLike: action: receive channel: $ref: 'common.asyncapi.yaml#/components/channels/likeComment' description: When a comment like is received from the frontend. onCommentLikesCountChange: action: receive channel: $ref: 'common.asyncapi.yaml#/components/channels/commentLikesCountChanged' description: When an event from the broker arrives telling us to update the comment likes count on the frontend. sendCommentLikesUpdate: action: send channel: $ref: 'common.asyncapi.yaml#/components/channels/updateCommentLikes' description: Update comment likes count in the frontend. sendCommentLiked: action: send channel: $ref: 'common.asyncapi.yaml#/components/channels/commentLiked' description: Notify all the services that a comment has been liked. ```
The frontend.asyncapi.yaml file ```yaml asyncapi: 3.0.0 info: title: Website WebSocket Client version: 1.0.0 remotes: websiteWebSocketServer: $ref: 'common.asyncapi.yaml#/components/servers/websiteWebSocketServer' operations: sendCommentLike: action: send channel: $ref: 'common.asyncapi.yaml#/components/channels/likeComment' description: Notify the backend that a comment has been liked. onCommentLikesUpdate: action: receive channel: $ref: 'common.asyncapi.yaml#/components/channels/updateCommentLikes' description: Update the UI when the comment likes count is updated. ```
The notifications-service.asyncapi.yaml file ```yaml asyncapi: 3.0.0 info: title: Notifications Service version: 1.0.0 remotes: mosquitto: $ref: 'common.asyncapi.yaml#/components/remotes/mosquitto' operations: onCommentLiked: action: receive channel: $ref: 'common.asyncapi.yaml#/components/channels/commentLiked' description: When a "comment has been liked" message is received, it sends an SMS or push notification to the author. ```
The comments-service.asyncapi.yaml file ```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. remotes: mosquitto: $ref: 'common.asyncapi.yaml#/components/remotes/mosquitto' operations: onCommentLiked: action: receive channel: $ref: 'common.asyncapi.yaml#/components/channels/commentLiked' description: Updates the likes count in the database and sends the new count to the broker. sendCommentLikesUpdate: action: send channel: $ref: 'common.asyncapi.yaml#/components/channels/updateCommentLikes' description: Sends the new count to the broker after it has been updated in the database. ```

Do you see any reason we would not do this? πŸ€”

fmvilas commented 3 years ago

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.

jonaslagoni commented 3 years ago

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 😬

magicmatatjahu commented 3 years ago

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

fmvilas commented 3 years ago

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

fmvilas commented 3 years ago

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.

magicmatatjahu commented 3 years ago

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

The broker.yaml file ```yaml asyncapi: 3.0.0 info: title: Broker version: 1.0.0 servers: brokerServer: ... channels: someChannel1: ... someChannel2: ... ```
The app1.yaml file ```yaml asyncapi: 3.0.0 info: title: App1 version: 1.0.0 servers: someServer: ... remotes: brokerRemote: $ref: 'broker.yaml#/servers/brokerServer' operations: someOperation1: channel: $ref: 'broker.yaml#/channels/someChannel1' someOperation2: channel: $ref: 'broker.yaml#/channels/someChannel2' ```

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

The broker.yaml file ```yaml asyncapi: 3.0.0 info: title: Broker version: 1.0.0 servers: brokerServer: ... channels: someChannel1: ... someChannel2: ... ```
The app1.yaml file ```yaml asyncapi: 3.0.0 info: title: App1 version: 1.0.0 servers: someServer: ... remotes: brokerRemote: $ref: 'broker.yaml#/servers/brokerServer' channels: anotherIdForChannel1: $ref: 'broker.yaml#/channels/someChannel1' anotherIdForChannel2: $ref: 'broker.yaml#/channels/someChannel2' operations: someOperation1: channel: anotherIdForChannel1 someOperation2: channel: anotherIdForChannel2 ```

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

The broker.yaml file ```yaml asyncapi: 3.0.0 info: title: Broker version: 1.0.0 servers: brokerServer: ... channels: someChannel1: channelID: broker-someChannel1 ... someChannel2: channelID: broker-someChannel2 ... ```
The app1.yaml file ```yaml asyncapi: 3.0.0 info: title: App1 version: 1.0.0 servers: someServer: ... remotes: brokerRemote: $ref: 'broker.yaml#/servers/brokerServer' operations: someOperation1: channel: $ref: 'broker.yaml#/channels/someChannel1' someOperation2: channel: $ref: 'broker.yaml#/channels/someChannel2' ```

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

fmvilas commented 3 years ago

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

magicmatatjahu commented 3 years ago

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

fmvilas commented 3 years ago

Yes, the address can be the global id.

jessemenning commented 3 years ago

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

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.

jessemenning commented 3 years ago

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.

fmvilas commented 3 years ago

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.

dalelane commented 3 years ago

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:

jessemenning commented 3 years ago

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.

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.

fmvilas commented 3 years ago

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.

ekozynin commented 3 years ago

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
fmvilas commented 3 years ago

@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? πŸ™

joerg-walter-de commented 3 years ago

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.

jessemenning commented 3 years ago

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

@fmvilas, that seems reasonable to me. Thanks, @magicmatatjahu !

fmvilas commented 3 years ago

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?

fmvilas commented 3 years ago

@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

magicmatatjahu commented 3 years ago

@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

image

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?

fmvilas commented 3 years ago

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?

Relequestual commented 3 years ago

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.

jessemenning commented 3 years ago

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:

@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
fmvilas commented 3 years ago

Instead, make kind a property of the binding on the operation, not the server.

@jmenning-solace There are a few things with this approach:

  1. Bindings are for protocol-specific information. If a server is being exposed by the application we're defining or if it's somewhere else (remote) is not a concern of the protocol.
  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.
  3. From your comment: "In many use cases a server will be both 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?

magicmatatjahu commented 3 years ago

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?

fmvilas commented 3 years ago

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.

jessemenning commented 3 years ago

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:

damaru-inc commented 3 years ago

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

iancooper commented 3 years ago

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

fmvilas commented 3 years ago

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 commented 3 years ago

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

jessemenning commented 3 years ago

@iancooper You got me curious. Mind expanding on this? Maybe some rough examples?

@iancooper +1 on this from me.

derberg commented 3 years ago

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

Remotes vs Kind

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.

Remotes are still there in the 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.

Send and receive

I like it, that is it, pub/sub confusion is a good reason for a change.

Proposal example of backend.syncapi.yml

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?

Old confusion of operationIds and descriptions

I 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

Breaking change dilemma

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

Messages on the root

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.

$ref vs kind

@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
fmvilas commented 3 years ago

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.

fmvilas commented 3 years ago

Started a new issue about the scope of an AsyncAPI file: #628

jessemenning commented 3 years ago

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.

fmvilas commented 3 years ago

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?

jessemenning commented 3 years ago

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 the servers 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)

nictownsend commented 3 years ago

Remote/Local:

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

Send/Receive

Happy with the rename and perspective clarification.

Public API

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?

tristanpye commented 3 years ago

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.

fmvilas commented 3 years ago

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.

jessemenning commented 3 years ago

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.