Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do you have a simpler example ? #31

Closed
jogelin opened this issue Aug 1, 2017 · 6 comments
Closed

Do you have a simpler example ? #31

jogelin opened this issue Aug 1, 2017 · 6 comments

Comments

@jogelin
Copy link

jogelin commented Aug 1, 2017

After a some researches about "how to document message-driven communication", I found your framework which looks very good but There is something that I don't understand...

I am beginner in the "message driven" way of communication but today I have an app using websockets and I didn't sucess to write the spec using asyncapi.

A simple example to display a list of filtered users:

  1. A user display the page containing the list of "active" users:
  • the frontend send a message to the backend to load the list of users with the status in parameters/payload
  • the frontend display the loader and wait
  1. The server receive the message:
  • load the list of users based on the status
  • the backend send another message to the frontend with the list of "active" users

Every call is asynchrone. So I tried to specify that using the asyncapi format:

asyncapi: '1.0.0'
info:
  title: 'User Api'
  version: '1.0.0'

topics:
  company.user.1.0.event.getall:
    publish:
      $ref: '#/components/messages/requestAllUsers'
  company.user.1.0.event.getall.succeed:
    publish:
      $ref: '#/components/messages/responseAllUsers'

components:
  messages:

    requestAllUsers:
      summary: 'Request the list of Users'
      payload:
        type: 'object'
          properties:
            status: 
              type: 'string'

    responseAllUsers:
      summary: 'Return the list of Users'
      payload:
        type: 'array'
        items:
          $ref: '#/components/schemas/user'

  schemas:

    id:
      title: 'id'
      description: 'Resource identifier'
      type: 'string'
    name:
      title: 'name'
      description: 'Resource name'
      type: 'string'

    user:
      type: 'object'
      required:
        - 'id'
        - 'name'
      properties:
        id:
          $ref: '#/components/schemas/id'
        name:
          $ref: '#/components/schemas/name'

What I misunderstand is the link between request message and response message. In your singup example, I am mixing the role of the "accounts.1.0.event.user.signup" message. It look like the reponse message but it's a topic that we should subscribe....

@fmvilas
Copy link
Member

fmvilas commented Aug 1, 2017

Hi @jogelin, I see a few problems here:

Your code is asynchronous, your approach does not

What you're trying to achieve, as per my understanding, is not asynchronous. You said:

the frontend display the loader and wait

If your app has to wait, then this is the first sign that it's not async. If you still want to do it like this I'll better suggest you use an HTTP API instead of WebSockets communication, because your approach is based on request/response. Of course, you can always implement RPC-style APIs using message-driven communication but that's a land I don't recommend you to walk. However, let's talk about it.

RPC

What I misunderstand is the link between request message and response message.

There's no link between messages. At least not if you don't define it. You can create a unique identifier for every message and respond back with a message referencing it. For instance, you send a message like this one:

{
  "id": "1234",
  "status": "active"
}

and the server will respond back with a message like this:

{
  "id": "5678",
  "responseTo": "1234",
  "users": [
    { "id": "fmvilas", "name": "Fran" },
    { "id": "jogelin", "name": "Jogelin" }
  ]
}

That will perfectly work, however, what you would be doing there is exactly what you get with an HTTP API. So I'd not mess around with RPC over WebSockets and would use an HTTP API instead.

Event-driven communication

If you can change how your app works I'd suggest you to events instead of requests and responses. An example could be:

  1. Your app loads and gets the current list of active users via an HTTP API.
    • This is not event-driven communication yet.
    • This could be documented using OpenAPI instead of AsyncAPI.
  2. Your app connects to the server using WebSockets and will listen for user status changes.
  3. Every time a user changes her status, the server will send a message to the frontend saying: "Hey! this is the new list of active users".
    • This event can be documented using AsyncAPI.
  4. Every time the user who is logged in changes its status, the frontend sends a message to the server saying: "Hey! user jogelin has changed his status to busy".
    • This event can be documented using AsyncAPI.

You have to think about the messages in step 3 and 4 as fire and forget. You just communicate that "something happened". You never expect a response.

I hope this sheds some light on your problem. I'm happy to help further if you need it.

@jogelin
Copy link
Author

jogelin commented Aug 1, 2017

Thanks a lot. I knew I was wrong in my understanding of async, I just need a confirmation and you answered it perfectly.

You rock !

@jogelin jogelin closed this as completed Aug 1, 2017
@fmvilas
Copy link
Member

fmvilas commented Aug 1, 2017

Thanks! I'm glad it helped. Feel free to ask whenever you have questions 👍

@jogelin
Copy link
Author

jogelin commented Aug 2, 2017

Feel free to ask whenever you have questions

Hum..... so I have a question :p

Since your great answer, I am trying to specify my api with OpenApi but I would like to keep a Message Driven way....is that possible ?....:)

I am working on an app using:

  • On the frontend: Angular 2+ with a Redux system which is a system of store that used a list of actions (messages) to manage it (google if you don't know ;)).
  • On the backend: microservices scala + akka using commands to communicate between them

And the goal is to be able to communicate directly the actions of the frontend to the backend without mapping each of the actions to an http endpoint. The backend will also answer to the frontend using message formats. It is why I choosed websocket but I completly agree, websocket should not be used to synchronous requests.

So I think to have only one POST endpoint on my service which is receiving a message et returning another message.

Example:

POST to /userService/api with application/json 

requestBody =
{
  headers: {
    version: "1.0.0",
    jobName: "organisation.userService.query.user.getAll"
  },
  payload: {
    status: "active"
  }
}

responseBody 200 =
{
  headers: {
    version: "1.0.0",
    jobName: "organisation.userService.query.user.getAll"
  },
  payload: [
    {
      id: "123",
      name: "arthur"
    },
    {
      id: "456",
      name: "julien"
    }
  ]
}

My first try:

openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  '/user/api':
    post:
      description: Publish a message
      requestBody:
        content:
          'application/json':
            schema:
              # here we can specify a list of all accepted messages
              oneOf:
              - $ref: '#/components/schemas/getAllUserMessage'
              - $ref: '#/components/schemas/getAllUserDetailMessage'
      responses:
        200:
          description: Response Message
          content:
            'application/json':
                schema:
                  # here we can specify a list of all response messages
                  oneOf:
                  - $ref: '#/components/schemas/getAllUserSuccessMessage'
                  - $ref: '#/components/schemas/getAllUserDetailSuccessMessage'
components:
  schemas:
    Headers:
      type: object
      required:
      - jobName
      - version
      properties:
        jobName:
          type: string
        version:
          type: string
    Message:
      type: object
      required:
      - headers
      - payload
      properties:
        headers:
          type: object
          $ref: '#/components/schemas/Headers'
        payload:
          type: object
    getAllUserMessage:
      allOf:
      - $ref: '#/components/schemas/Message'
      - type: object
        properties:
          headers:
            properties:
              jobName:
                enum: ['organisation.userService.query.user.getAll']
          payload:
            properties:
              status:
                type: string
    getAllUserSuccessMessage:
      allOf:
      - $ref: '#/components/schemas/Message'
      - type: object
        properties:
          headers:
            properties:
              jobName:
                enum: ['organisation.userService.query.user.getAll']
          payload:
            type: array
            items:
              $ref: '#/components/schemas/User'
    getAllUserDetailMessage:
      allOf:
      - $ref: '#/components/schemas/Message'
      - type: object
        properties:
          headers:
            properties:
              jobName:
                enum: ['organisation.userService.query.userDetail.getAll']
          payload:
            properties:
              status:
                type: string
    getAllUserDetailSuccessMessage:
      allOf:
      - $ref: '#/components/schemas/Message'
      - type: object
        properties:
          headers:
            properties:
              jobName:
                enum: ['organisation.userService.query.userDetail.getAll']
          payload:
            type: array
            items:
              $ref: '#/components/schemas/UserDetail'
    Resource:
      type: object
      required:
      - id
      - name
      properties:
        id:
          description: 'Resource identifier'
          type: string
        name:
          description: 'Resource name'
          type: string
    User:
      type: object
      $ref: '#/components/schemas/Resource'
    UserDetail:
      type: object
      $ref: '#/components/schemas/Resource'

But it looks very complex (it is why I feel that asyncapi was good for that) so if you have any advices...?

@fmvilas
Copy link
Member

fmvilas commented Aug 2, 2017

Not sure I understand why you chose OpenAPI if you want to do it with messages. What I said previously, is that only the first call to grab the current status of users can be documented using OpenAPI as it makes sense because it's a synchronous operation over HTTP.

Then, the rest of operations can be documented using AsyncAPI because they are events over WebSockets.

If you want to go the full way with events it's also fine and you can do it. So let's break this up into many cases:

OpenAPI + AsyncAPI

OpenAPI file to describe the first operation (grabbing the current status of users):

paths:
  /users_status
    get:
      responses:
        200:
          content:
            schema:
              $ref: "#/components/schemas/UserStatusResponse"
components:
  schemas:
    UserStatusResponse:
      type: array
      items:
        type: object
        properties:
          username:
            type: string
          full_name:
            type: string
          status:
            type: string
            enum:
              - active
              - busy
              - away
              - disconnected

And now the AsyncAPI file to describe the events:

topics:
  yourcompany.user.1.0.event.user.status.list.changed:
    subscribe:
      $ref: "#/components/messages/UserStatusListChangedMessage"
    publish:
      $ref: "#/components/messages/UserStatusListChangedMessage"
  yourcompany.user.1.0.event.user.status.changed:
    subscribe:
      $ref: "#/components/messages/UserStatusChangedMessage"
    publish:
      $ref: "#/components/messages/UserStatusChangedMessage"


components:
  messages:
    UserStatusListChangedMessage:
      payload:
        type: array
        items:
          $ref: "#/components/schemas/user"

    UserStatusChangedMessage:
      payload:
        $ref: "#/components/schemas/user"

  schemas:
    user:
      type: object
      properties:
        username:
          type: string  
        full_name:
          type: string
        status:
          type: string
          enum:
            - active
            - busy
            - away
            - disconnected

Only AsyncAPI

If you choose to only use events, then you have to change your approach. When you connect to the server using WebSockets you'll send a message saying you just connected. It's very easy to fall into the temptation of creating a message saying: "Give me the list of active users". If you want to use event-driven design the message should never be an action (i.e. Give me) but instead, it should be an event (i.e. I just connected). Here's the AsyncAPI file (roughly) for this:

topics:
  yourcompany.user.1.0.event.user.status.list.changed:
    subscribe:
      $ref: "#/components/messages/UserStatusListChangedMessage"
    publish:
      $ref: "#/components/messages/UserStatusListChangedMessage"
  yourcompany.user.1.0.event.user.status.changed:
    subscribe:
      $ref: "#/components/messages/UserStatusChangedMessage"
    publish:
      $ref: "#/components/messages/UserStatusChangedMessage"
  yourcompany.frontend.1.0.event.connected:
    subscribe:
      $ref: "#/components/messages/FrontendConnectedMessage"
    publish:
      $ref: "#/components/messages/FrontendConnectedMessage"



components:
  messages:
    UserStatusListChangedMessage:
      payload:
        type: array
        items:
          $ref: "#/components/schemas/user"

    UserStatusChangedMessage:
      payload:
        $ref: "#/components/schemas/user"

    FrontendConnectedMessage:
      payload:
        $ref: "#/components/schemas/FrontendConnected"

  schemas:
    user:
      type: object
      properties:
        username:
          type: string  
        full_name:
          type: string
        status:
          type: string
          enum:
            - active
            - busy
            - away
            - disconnected

    FrontendConnected:
      type: object

You might be wondering why I'm always duplicating publish and subscribe for each topic. Well, it's because I'm defining the API for your whole system (backend and frontend) in a single file. You could have them in separate files if you want.

Hope it helps.

@fmvilas
Copy link
Member

fmvilas commented Aug 2, 2017

You might want to join our Slack channel: https://async-apis-slack.herokuapp.com/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants