spec icon indicating copy to clipboard operation
spec copied to clipboard

Support request/reply pattern

Open adrianhopebailie opened this issue 7 years ago • 64 comments

Somewhat related to:

  • https://github.com/asyncapi/asyncapi/issues/55
  • https://github.com/asyncapi/asyncapi/issues/78

It would be useful if it was possible to describe messages that are explicitly requests and responses and for the auto-generated code to deal with creating the appropriate ephemeral queues and performing matching on the correlationid.

The pattern that seems most common when using a pub-sub message broker is for the requestor to create a single use topic and provide this address as the 'reply-to' header in the request message. The requestor also provides a correlationId which is echoed back to help match requests and replies.

However, when using a transport like WebSockets it would be necessary to do additional work in the generated code to match requests and responses and also deal with message state and expiry.

Ideally this should be abstracted away from API designers who may prefer to define their API in a manner similar to Open API as follows (see the Responses Object - https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responsesObject) :

topics:
  UPDATEARTICLE:
    request:
      headers:
        type: object
        properties:
          ...
      payload:
        type: object
        properties:
          ...
     reply:
       match-on: {$response.headers.type}
       responses:
         'success':
           headers:
             type: object
             properties:
               ...
           payload:
             type: object
             properties:
               ...
         'fail':
           headers:
             type: object
             properties:
               ...
           payload:
             type: object
             properties:
               ...

I've used the topics object but maybe a new operations object would be more appropriate?

One of the challenges here is the flexibility of having multiple possible responses but also defining the logic for identifying what response has actually been received. (Open API matches on HTTP response code so that's pretty simple).

In my example I just provide a matching rule but this could probably be a lot more flexible.

adrianhopebailie avatar Oct 11 '18 09:10 adrianhopebailie

Thanks for the suggestion @adrianhopebailie. I think we'll need to somehow support the request/response pattern in the future. We need to figure out how to properly design it so it ages well in the spec. Sorry for the short answer, this feature can't be added quickly without adding so much complexity to the current spec status. I'll have a look in detail as soon as I have time.

fmvilas avatar Oct 24 '18 19:10 fmvilas

+1 to this from me. Common pattern on messaging platforms. Done many such implementations on WebSphere MQ in a former life. As mentioned above, #78 would be fundamental to this. Something in the style of a Callback Object would be a good approach.

SensibleWood avatar Oct 25 '18 20:10 SensibleWood

Also +1 We are interested in support for the new request/response pattern introduced in MQTT 5.

mpe85 avatar Feb 26 '19 14:02 mpe85

Interesting, I haven't heard about MQTT 5. Going to take a look. Thanks for commenting!

fmvilas avatar Feb 26 '19 14:02 fmvilas

Doing the investigation on this for a post v2.0.0 solution

SensibleWood avatar Mar 07 '19 13:03 SensibleWood

Hi there, we are starting to use async communication more and more and we have both variants in use, the basic request/response pattern where we send an responseEvent parameter like the mqtt corrId, on incoming data it will do stuff and send the wanted information back to the responseEvent.

Also we use the publish/subscribe pattern were we need it.

So we are very intereseted to get this into the definition.

For me the most flexible way would be just a combination of

subscribe:
publish:

I think a flexible and good way would be to name subscribe as mentioned "request" Within the request there will be a response: which is also defined as a operation obect

https://www.asyncapi.com/docs/specifications/2.0.0-rc1/#operationObject

For example:

channels:
  get-last-messages:
    request:
      summary: Get the latest messages from the user
      message:
        payload:
          responseEvent:
            type: string
      response:
        message:
          payload:
            messages:
              type: array
              description: A list of messages
              items:
                type: string
                description: The message.
    

If we assume errors / success status, we could just create a template by our own defintions which we can $ref in a, for example, called "status" property

status:
 $ref: '#/components/schemas/responseStatus'
content:
  type: object
  properties:
    resultData:
      type: string

With the use of the operation object we would not get into trouble in my opinion, as the definition should cover all feature variants of communication, so the operation object will be mutated and extended in future versions of asyncapi and that will extend also the response type.

What do you think?

prdatur avatar Jun 20 '19 08:06 prdatur

The suggestion of @prdatur matches pretty well with our thought (except we would expect the response/reply object on the same structural level as the request object). One thing that we think is also required, is to state whether the application should act as a request/reply producer or consumer - i.e. whether the application would send requests, or would receive requests and send replies (such detail could maybe be specified in the binding object for the channel)

jdall avatar Sep 12 '19 06:09 jdall

hey, any update on this?

ig0rsky avatar Nov 06 '19 16:11 ig0rsky

It's on the list for the next minor versions. Any research on how this could be done is appreciated.

fmvilas avatar Nov 08 '19 19:11 fmvilas

Interested party here also for request/response pattern!

basickarl avatar Dec 12 '19 14:12 basickarl

@fmvilas Any draft available for a sneak peek?

basickarl avatar Dec 12 '19 14:12 basickarl

Hey @basickarl! There's no literature on this yet. Anything you can provide as a starting point would be greatly appreciated.

fmvilas avatar Dec 15 '19 11:12 fmvilas

Request-Response in the context of MQTT 5.

  • There are request and response topics.
  • Both sides are subscribed to those topics.
  • Requests carry information about the response topics.
  • You can add correlation data so response can be bound to request.

jruizaranguren avatar Dec 17 '19 09:12 jruizaranguren

@jruizaranguren

Slightly different scenarios. The one in the MQTT has a broker. Client <-> Broker <-> Client. I'm specifically interested in the Client <-> Client scenario, e.g. for WebSocket connections from web clients to servers. Also for public facing WebSocket API's.

basickarl avatar Jan 08 '20 13:01 basickarl

@fmvilas based on your first examples which is a way we can go. https://github.com/asyncapi/asyncapi/blob/b21cf1d854994a8488c6575ae0dda461b51eb1d4/examples/2.0.0/rpc-server.yml

I have one question or one change request. Was is meant with '{queue}' I know that this is the response queue which is defined in amqp within the replyTo header but in the example we would only have one response queue because the string '{queue}' can only occure once.

We should just name the reply queue within the request definition like adding a new field before "correlationId" which is named "replyTo" of type string, this string can hold an existing channel which defines the response data.

Another way would be a variable as the channel, in this case a generator or user will not be confused to check wether the channel is just a response or actually a channel which the client can subscribe. For example defining the same way as within your example

channels:
   '{$rpc_queue.publish.message.header#/replyTo}':
      subscribe:
         ...

So this channel would define the response channel which is located within the rpc_queue message header field "replyTo". The definition will be a subscribe operation.

Third option is to define that if publish and subscribe exists within the same channel declares that this channel is a request / response operation.

A self subscribing channel where the software will publish data would be a bit confusing :) So we can use this for the request response definition. Additional we could create the "replyTo" field to let generates know where to find the reply queue name.

What do you mean?

prdatur avatar Jan 10 '20 08:01 prdatur

I think that opening the door for expressions in the channel name would open a door for potential complexity but we will explore it.

...because the string '{queue}' can only occure once.

{queue} is a channel item variable, meaning that it can be anything. It doesn't mean that the queue should be exactly {queue}.

Does it make sense?

fmvilas avatar Jan 20 '20 11:01 fmvilas

{queue} is a channel item variable, meaning that it can be anything. It doesn't mean that the queue should be exactly {queue}.

I understood that this is a variable. While I was writing an example what I didn't understand, I got your point. for example we can just write multiple '{queue}' or better for each a name like {sendSumResponseQueue} which then will be mostly the same definition like the one in your example.

However with this, we currently have two problems

First, the response is not linked to any operation.

After I used and wrote the word "operation" I might got a solution, why not link the operationId's... they must be unique per definition, so we could use something like responseOf: {operationId}

Second, linking such a response to a publish operation we are not able to know for which message the response is

Let me explain. Within the issue #303 I made an example. This is exactly what I mean here. Many systems just connect to one queue/exchange and publish an own structure, mostly something like:

{
   "action": "do-something",
   "data": "...."
}

This means, a schema will look like:

publish:
  message:
    oneOf:
    - $ref: '#/components/messages/message1'
    - $ref: '#/components/messages/message2'
    - $ref: '#/components/messages/message3'

If we have only one response channel, we will not know if the defined response is for message 1, 2 or 3. Also If we define 3 response messages, we will not know if the response message 1 will be published for message 1, 2 or 3.

So with a simple change to not link the response channel to publish operation, we should link a response to a message. In that way we never get in trouble where something can be published and we did not know what we get returned. If we link it to a message we really know this published message will us return message XY.

So the solution with channel expression would be one way. Other solutions:

  • Add messageId same ways as the operationId, then it would be possible to define within the message object something like responseTo: {messageId}
  • Just add "response" keyword to a message object which is another messageObject, infinity loop must be adressed, Would be not the ideal solution because we have no definition for the response channel.

I would prefer the expression, because it will not be a big change within the specification, but yes it will be more complex also for generators. Next would be the messageId and responseTo or responseOf solution. Mostly there will be just one channel we need to describe, because a system will mostly response in the same way as before. So there will be just a bunch of ref's within message.oneOf...

Example of messageId solution:


components:
  messages:
    responseSum:
      payload:
        type: object
        properties:
          result:
            type: number
            examples:
              - 7
    responseDevide:
      payload:
        type: object
        properties:
          result:
            type: float
            examples:
              - 7.5
    operationSum:
      bindings:
        amqp:
          replyTo:
            type: string
      responseTo: '#/components/messages/responseSum'
      payload:
        type: object
        properties:
          operation:
            type: string
            enum:
            - sum
          numbers:
            type: array
            items:
              type: number
            examples:
              - [4,3]
    operationDevide:
      bindings:
        amqp:
          replyTo:
            type: string
      responseTo: '#/components/messages/responseDevide'
      payload:
        type: object
        properties:
          operation:
            type: string
            enum:
            - devide
          numbers:
            type: array
            items:
              type: number
            examples:
              - [15,2]

channels:
  '{responseQueue}':
    parameters:
      responseQueue:
        schema:
          type: string
          pattern: '^amq\\.gen\\-.+$'
    bindings:
      amqp:
        is: queue
        queue:
          exclusive: true
    subscribe:
      bindings:
        amqp:
          ack: true
      message:
        oneOf:
        - $ref: '#/components/messages/responseSum'
        - $ref: '#/components/messages/responseDevide'

  rpc_queue:
    bindings:
      amqp:
        is: queue
        queue:
          durable: false
    publish:
      message:
        oneOf:
        - $ref: '#/components/messages/operationSum'
        - $ref: '#/components/messages/operationDevide' 

Example for using just response within the message


components:
  messages:
    responseSum:
      payload:
        type: object
        properties:
          result:
            type: number
            examples:
              - 7
    responseDevide:
      payload:
        type: object
        properties:
          result:
            type: float
            examples:
              - 7.5

    operationSum:
      bindings:
        amqp:
          replyTo:
            type: string
      response:
         $ref: '#/components/messages/responseSum'
      payload:
        type: object
        properties:
          operation:
            type: string
            enum:
            - sum
          numbers:
            type: array
            items:
              type: number
            examples:
              - [4,3]
    operationDevide:
      bindings:
        amqp:
          replyTo:
            type: string
      response:
         $ref: '#/components/messages/responseDevide'
      payload:
        type: object
        properties:
          operation:
            type: string
            enum:
            - devide
          numbers:
            type: array
            items:
              type: number
            examples:
              - [15,2]

channels:
  rpc_queue:
    bindings:
      amqp:
        is: queue
        queue:
          durable: false
    publish:
      message:
        oneOf:
        - $ref: '#/components/messages/operationSum'
        - $ref: '#/components/messages/operationDevide' 

Problem we have not defined the queue for the response.

The problem is, that the request/response is really needed by us now. So it would be nice if we can get this finished. :)

Anyway, as a real quick answer. Yes your explanation makes sense but we have no information which message will get response if someone publish to rpc_queue :)

prdatur avatar Jan 20 '20 21:01 prdatur

After a discussion with a co-worker, we will just use an x-responses property within the publich.message section which will include one or oneOf references to the response message (not the channel) example:

components:
  messages:
    responseMessage1:
      ...
    responseMessage2:
      ...
channels:
  '{rQueue}':
    ...
    subscribe:
      message:
        oneOf:
        - $ref: '#/components/messages/responseMessage1'
        - $ref: '#/components/messages/responseMessage2'
  pubQueue:
    ...
    publish:
      ...
      message:
        ...
        x-responses:
          $ref: '#/components/messages/responseMessage1'

Or if we have multiple responses like the example with ack, status, response

  pubQueue:
    ...
    publish:
      ...
      message:
        ...
        x-responses:
          oneOf:
          - $ref: '#/components/messages/responseMessage1'

This allows us to define all required things (channel, messages) and just link the message to the response message so the documentator for example place a link below one of the accepeted messages to the response message.

Maybe we can use this idea as the solution and just remove the x- part.

prdatur avatar Jan 21 '20 10:01 prdatur

Me and a co-worker had a chat and we would like to support several different actions on a "channel". for channel mychannel/{myChannelId} in our case we would like a publish/subscribe (with unsubscribe and acks when subscribing and unsubscribing, we had a look at mqtt 5.0 and followed their example), request/response and command (command doesn't expect a response).

If you guys head over to https://crossbar.io/ (responsible for the WAMP protocol) and look at their fancy gif on the home page a little further down, you'll see a chronological scenario of what we want to accomplish via asyncapi.

An example of how thew asyncapi could be updated to support it:

channels:

  mychannel/{myChannelId}/:

    command: ...
    request: ...
    response: ...
    unsubscribe: ...
    subscribe: ...
    publish: ...

And we can take it a step further and include ACKs (inspired by mqtt 5.0):

channels:

  mychannel/{myChannelId}/:

    command: ...
    command ack: ...
    request: ...
    request ack: ...
    response: ...
    response ack: ...
    unsubscribe: ...
    unsubscribe ack: ...
    subscribe: ...
    subscribe ack: ...
    publish: ...
    publish ack: ...

Again, take a look at the gif at https://crossbar.io/.

Regarding the mqtt 5.0 inspiration, comes from the following:

https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901121 https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901171 https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901187

(It feels like asyncapi would have to include these properties if it wants to help support mqtt 5.0?)

I also understand @prdatur approach but would as @fmvilas says let people do their own thing and increase complexity. With the suggestion I proposed it would add on to how asyncapi is already written, add structure and also help conform to mqtt 5.0.

basickarl avatar Mar 04 '20 15:03 basickarl

{queue} problem

@prdatur cant figure out if this is a current problem you have or a suggestion for solution to request reply 😄

If it is a current problem If it is a current problem is this not something that can be solved at the implementation level i.e. is there a use-case for this property to be present in the specification? If we take template clients they would look at your AsyncAPI file and (dependent on the implementation of course) create a method for your channel `'{responseQueue}'` where you can subscribe a callback (or whatever) and get notified when a response ticks in. The generated client then tries parsing the received data to one of the `oneOf` messages and call your callback with the given message. Then with the use of `correlationId` (or something else) you can cross check and figure out which response has returned?

If it is a suggestion for the request reply, then I gotta say I am more a fan of doing something like @basickarl suggests, at the channel level, which would provide complete clarity what is defined as request and reply in a given channel without having to go through the request operation first 😄

Request reply suggestion

channels:

  mychannel/{myChannelId}/:

    command: ...
    request: ...
    response: ...
    unsubscribe: ...
    subscribe: ...
    publish: ...

@basickarl isn't this a bit too specific for mqtt for it to be in a specification? Many other protocols just have request/reply and pubsub, which is what I see as the commonality between the protocols I have encountered.

However I also like the idea of adding request and reply properties to a channel i.e.

channels:

  mychannel/{myChannelId}/:

    request: ...
    response: ...
    subscribe: ...
    publish: ...

One question i'd like to raise: IF request/reply is not a feature we foresee as a standard feature for all protocols should it really be included in the specification and not just stay in the bindings as a boolean or a request/reply property for those protocols which do support it?

jonaslagoni avatar Mar 04 '20 17:03 jonaslagoni

@jonaslagoni As stated every protocol implements their own terminology for these things so I guess AsyncAPI will either have to add ALL of these terms to make everyone happy or put their foot down and define terms that everyone should adopt. These terms should however be able to satisfy what you wish to do with each protocol.

MQTT was just an example, and one of the specs in MQTT 5.0 was that when you send a subscribe message than it sends a subscribe acknowledgement back to who sent the subscribe message. If we wished to implement this today and specify it in a document AsyncAPI would fall short.

And answering your last question @jonaslagoni I don't see the harm in adding these things, you don't need to add it to your own AsyncAPI documentation, if you don't use request/response pattern in your API, just don't include it!

basickarl avatar Mar 05 '20 10:03 basickarl

+1

GreenRover avatar Mar 27 '20 08:03 GreenRover

Would like to say we have forked 4 of the repos from https://github.com/asyncapi and have implemented the following:

command: ...
request: ...
response: ...
unsubscribe: ...
unsubscribe ack: ...
subscribe: ...
subscribe ack: ...
publish: ...
publish ack: ...

It's fufilling our needs pretty well.

basickarl avatar Mar 31 '20 13:03 basickarl

Interesting. What's the meaning of each of them? We can probably incorporate them into future versions.

fmvilas avatar Apr 16 '20 10:04 fmvilas

Hi,

Has any progress been made with this? We too have a need to document more than just pub/sub. Whilst terminology differs slightly by protocol, personally I would favour using the convention of command vs event to distinguish, and then adding reply as an optional property on the message that it would be a reply to.

Although I favour the command and event convention, I don't necessarily think it should be enforced. I see this more as an optional specialization of a message that can be provided. My object oriented mind sees this as: -

image

I also find mixing publish and subscribe confusing. I personally don't see a necessity in documenting the subscription as it's the publisher that actually owns the message contract (and in a lot of cases the channel). Whilst there are likely benefits of documenting it in some scenarios I would like to see a clear distinction between an interface a service provides and those that it consumes, therefore I would suggest breaking out subscriptions as a new root.

As a suggestion, I could see this coming together as something like: -

channels:
  channelName:
    messages:
      - name: messageName
        specialization: event # optional, can either be event, command or not provided for use cases where distinction isn't helpful
        payload: ....
      - name: messageName
        specialization: command
        payload: ....
        reply:
          #a message object
consumes: # subscriptions moved into a new root property
  channels:
    channelName:
      messages:
        - $ref: '#/aSchemaProvidedByTheProducer' 

You'll notice that messages is an array, as another issue that I have are certain scenarios where different messages are sent over the same channel. Although this isn't necessarily best practice, these situations do exist so I would favour support for them.

Whilst the proposal above changes the structure somewhat, I don't see why it couldn't be implemented in a non-breaking way with messages being introduced optionally alongside publish/subscribe. Having some sort of specialization concept also allows flexibility for new types should they be needed.

There are many different ways that this could be represented and whilst I would favour something similar to the above, any non-breaking change to the spec that would allow documentation of messages that are not pub/sub (so they don't have pub/sub next to them in the output) would be extremely helpful.

nei-l avatar Jun 12 '20 14:06 nei-l

@fmvilas

Interesting. What's the meaning of each of them? We can probably incorporate them into future versions.

All acks work the same: this is sent to the sender to notify them that the receiver has received their x message. No acks are sent for acks. (we work on airport systems and we require acks to make sure things have been sent).

We have since my last post included acks in all types now.

command: ... <-- sender sends this and there is no obligation for the receiver to send a response to the sender request: ... <-- sender sends this and there is an obligation that the receiever will send a response to the sender response: ... <-- this is what the receiver of the request sends back to the sender of the request unsubscribe: ... <-- as it is now subscribe: ... <-- as it is now publish: ... <-- as it is now

the request response is mimicking traditional http requests, example would be a GET request. command is telling something to do something and does not need a response.

I am currently writing a library which is an async version of express which follows the asyncapi specification, it will take a while to release though. I'll update you guys when it's done.

basickarl avatar Jun 13 '20 14:06 basickarl

@basickarl, may I suggest one more type:

event: ... <-- the same as command in terms of response but implies no command meaning and rather works as a notification.

kvaleev avatar Jun 14 '20 14:06 kvaleev

@kvaleev When I think of events I think that an event is broadcast meaning anyone who listens will receive it, and it feels like subscribe/publish pattern solves this? You are referring to 1-to-1 communication here (client <-> server) where both know about each other?

Events in general do not know about who will listen to them (think OS events, mouse click, the mouse has no idea who will consume the click, it just sends it out into the unknown and whoever cares about it will use it, hence subscribe/publish).

If you want to look at a full fledged example of different types of communication working together look at the gif animation on this page: https://crossbar.io/

The gif animation is what my proposal would fix. (in the gif animation someone sends a publish message type, which triggers an event message type, it's just naming differences)

basickarl avatar Jun 14 '20 14:06 basickarl

Would like to say we have forked 4 of the repos from https://github.com/asyncapi and have implemented the following:

command: ...
request: ...
response: ...
unsubscribe: ...
unsubscribe ack: ...
subscribe: ...
subscribe ack: ...
publish: ...
publish ack: ...

It's fufilling our needs pretty well.

IMO this is too much since most protocols won't ever use these operations. subscribe ack and publish ack I feel like is more a protocol binding for the subscribe/publish operations then an actual operation? The command this is pretty much the same as a single publish operation or what would the difference be? :thinking: Agree with the request/reply operations :smile:

What would unsubscribe be used for @basickarl?

jonaslagoni avatar Jun 14 '20 14:06 jonaslagoni

@kvaleev Now when I think of it event could be added, which as the gif animation explains, would be the result of a publish message type coming into the server.

basickarl avatar Jun 14 '20 14:06 basickarl