CAP-level Messaging
CAP provides intrinsic support for emitting and receiving events. This is complemented by Messaging Services connecting to message brokers to exchange event messages across remote services.
Why Using Messaging?
Using messaging has two major advantages:
Resilience
If a receiving service goes offline for a while, event messages are safely stored, and guaranteed to be delivered to the receiver as soon as it goes online again.
Decoupling
Emitters of event messages are decoupled from the receivers and don't need to know them at the time of sending. This way a service is able to emit events that other services can register on in the future, for example, to implement extension points.
Using Message Channels
When emitters and receivers live in separate processes, you need to add a message channel to forward event messages. CAP provides messaging services, which take care for that message channel behind the scenes as illustrated in the following graphic:
Uniform, Agnostic Messaging
CAP provides messaging services, which transport messages behind the scenes using different messaging channels and brokers. All of this happens without the need to touch your code, which stays on conceptual level.
1. Use file-based-messaging in Development
For quick tests during development, CAP provides a simple file-based messaging service implementation. Configure that as follows for the [development] profile:
"cds": {
"requires": {
"messaging": {
"[development]": { "kind": "file-based-messaging" }
},
}
}Learn more about cds.env profiles.
In our samples, you find that in @capire/reviews/package.json as well as @capire/bookstore/package.json, which you'll run in the next step as separate processes.
2. Start the reviews Service and bookstore Separately
First start the reviews service separately:
cds watch reviewsThe trace output should contain these lines, confirming that you're using file-based-messaging, and that the ReviewsService is served by that process at port 4005:
[cds] - connect to messaging > file-based-messaging { file: '~/.cds-msg-box' }
[cds] - serving ReviewsService { path: '/reviews', impl: '../reviews/srv/reviews-service.js' }
[cds] - server listening on { url: 'http://localhost:4005' }
[cds] - launched at 5/25/2023, 4:53:46 PM, version: 7.0.0, in: 593.274msThen, in a separate terminal start the bookstore server as before:
cds watch bookstoreThis time the trace output is different to when you started all in a single server. The output confirms that you're using file-based-messaging, and that you now connected to the separately started ReviewsService at port 4005:
[cds] - connect to messaging > file-based-messaging { file: '~/.cds-msg-box' }
[cds] - mocking OrdersService { path: '/orders', impl: '../orders/srv/orders-service.js' }
[cds] - serving CatalogService { path: '/browse', impl: '../reviews/srv/cat-service.js' }
[cds] - serving AdminService { path: '/admin', impl: '../reviews/srv/admin-service.js' }
[cds] - connect to ReviewsService > odata { url: 'http://localhost:4005/reviews' }
[cds] - server listening on { url: 'http://localhost:4004' }
[cds] - launched at 5/25/2023, 4:55:46 PM, version: 7.0.0, in: 1.053s3. Add or Update Reviews
Similar to before, open http://localhost:4005/vue/index.html to add or update reviews.
→ In the terminal window for the reviews server you should see this:
[cds] - PATCH /reviews/Reviews/74191a20-f197-4829-bd47-c4676710e04a
< emitting: reviewed { subject: '251', count: 1, rating: 3 }→ In the terminal window for the bookstore server you should see this:
> received: reviewed { subject: '251', count: 1, rating: 3 }Agnostic Messaging APIs
Without touching any code the event emitted from the ReviewsService got transported via file-based-messaging channel behind the scenes and was received in the bookstore as before, when you used in-process eventing → which was to be shown (QED).
4. Shut Down and Restart Receiver → Resilience by Design
You can simulate a server outage to demonstrate the value of messaging for resilience as follows:
- Terminate the
bookstoreserver withCtrl+Cin the respective terminal. - Add or update more reviews as described before.
- Restart the receiver with
cds watch bookstore.
→ You should see some trace output like that:
[cds] - server listening on { url: 'http://localhost:4004' }
[cds] - launched at 5/25/2023, 10:45:42 PM, version: 7.0.0, in: 1.023s
[cds] - [ terminate with ^C ]
> received: reviewed { subject: '207', count: 1, rating: 2 }
> received: reviewed { subject: '207', count: 1, rating: 2 }
> received: reviewed { subject: '207', count: 1, rating: 2 }Resilience by Design
All messages emitted while the receiver was down stayed in the messaging queue and are delivered when the server is back.
Have a Look Into ~/.cds-msg-box
You can watch the messages flowing through the message queue by opening ~/.cds-msg-box in a text editor. When the receiver is down and therefore the message not already consumed, you can see the event messages emitted by the ReviewsService in entries like that:
ReviewsService.reviewed {"data":{"subject":"201","count":4,"rating":5}, "headers": {...}}Using Multiple Channels
By default CAP uses a single message channel for all messages.
For example: If you consume messages from SAP S/4HANA in an enhanced version of bookstore, as well as emit messages a customer could subscribe and react to in a customer extension, the overall topology would look like that:
Using Separate Channels
Now, sometimes you want to use separate channels for different emitters or receivers. Let's assume you want to have a dedicated channel for all events from SAP S/4HANA, and yet another separate one for all outgoing events, to which customer extensions can subscribe too. This situation is illustrated in this graphic:
This is possible when using low-level messaging, but comes at the price of loosing all advantages of conceptual-level messaging as explained in the following.
Using composite-messaging Implementation
To avoid falling back to low-level messaging, CAP provides the composite-messaging implementation, which basically acts like a transparent dispatcher for both, inbound and outbound messages. The resulting topology would look like that:
Transparent Topologies
The composite-messaging implementation allows to flexibly change topologies of message channels at deployment time, without touching source code or models.
Configuring Individual Channels and Routes
You would configure this in bookstore's package.json as follows:
"cds": {
"requires": {
"messaging": {
"kind": "composite-messaging",
"routes": {
"ChannelA": ["**/ReviewsService/*"],
"ChannelB": ["**/sap/s4/**"]
"ChannelC": ["**/bookshop/**"]
}
},
"ChannelA": {
"kind": "enterprise-messaging", ...
},
"ChannelB": {
"kind": "enterprise-messaging", ...
},
"ChannelC": {
"kind": "enterprise-messaging", ...
}
}
}In essence, you first configure a messaging service for each channel. In addition, you would configure the default messaging service to be of kind composite-messaging.
In the routes, you can use the glob pattern to define filters for event names, that means:
**will match any number of characters.*will match any number of characters except/and..?will match a single character.
Tip
You can also refer to events declared in CDS models, by using their fully qualified event name (unless annotation @topic is used on them).
Low-Level Messaging
In the previous sections it's documented how CAP promotes messaging on conceptual levels, staying agnostic to topologies and message brokers. While CAP strongly recommends staying on that level, CAP also offers lower-level messaging, which loses some of the advantages but still stays independent from specific message brokers.
Messaging as Just Another CAP Service
All messaging implementations are provided through class cds.MessagingService and broker-specific subclasses of that. This class is in turn a standard CAP service, derived from cds.Service, hence it's consumed as any other CAP service, and can also be extended by adding event handlers as usual.
Configure Messaging Services
As with all other CAP services, add an entry to cds.requires in your package.json or .cdsrc.json like that:
"cds": {
"requires": {
"messaging": {
"kind": // ...
},
}
}Learn more about cds.env and cds.requires.
You're free how you name your messaging service. Could be messaging as in the previous example, or any other name you choose. You can also configure multiple messages services with different names.
Connect to the Messaging Service
Instead of connecting to an emitter service, connect to the messaging service:
const messaging = await cds.connect.to('messaging')Emit Events to Messaging Service
Instead of emitter services emitting to themselves, emit to the messaging service:
await messaging.emit ('ReviewsService.reviewed', { ... })Receive Events from Messaging Service
Instead of registering event handlers with a concrete emitter service, register handlers on the messaging service:
messaging.on ('ReviewsService.reviewed', msg => console.log(msg))Declared Events and @topic Names
When declaring events in CDS models, be aware that the fully qualified name of the event is used as topic names when emitting to message brokers. Based on the following model, the resulting topic name is my.namespace.SomeEventEmitter.SomeEvent.
namespace my.namespace;
service SomeEventEmitter {
event SomeEvent { ... }
}If you want to manually define the topic, you can use the @topic annotation:
//...
@topic: 'some.very.different.topic-name'
event SomeEvent { ... }Conceptual vs. Low-Level Messaging
When looking at the previous code samples, you see that in contrast to conceptual messaging you need to provide fully qualified event names now. This is just one of the advantages you lose. Have a look at the following list of advantages you have using conceptual messaging and lose with low-level messaging.
- Service-local event names (as already mentioned)
- Event declarations (as they go with individual services)
- Generated typed API classes for declared events
- Run in-process without any messaging service
Always prefer conceptual-level API over low-level API variants.
Besides the things listed above, this allows you to flexibly change topologies, such as starting with co-located services in a single process, and moving single services out to separate micro services later on.
CloudEvents Standard
CAP messaging has built-in support for formatting event data compliant to the CloudEvents standard. Enable this using the format config option as follows:
"cds": {
"requires": {
"messaging": {
"format": "cloudevents"
}
}
}With this setting, all mandatory and some more basic header fields, like type, source, id, datacontenttype, specversion, time are filled in automatically. The event name is used as type. The message payload is in the data property anyways.
CloudEvents is a wire protocol specification.
Application developers shouldn't have to care for such technical details. CAP ensures that for you, by filling in the respective fields behind the scenes.