| synopsis | Learn details about the outbox feature. |
|---|---|
| redirect_from | node.js/outbox |
| status | released |
[[toc]]
Often, remote operations should be delayed until the main transaction succeeded. Otherwise, the remote operations are also triggered in case of a rollback. To enable this, an outbox can be used to defer remote operations until the success of the current transaction.
Every CAP service can be outboxed, that means event dispatching becomes asynchronous.
Programmatically, you can get the outboxed service with
const srv = await cds.connect.to('yourService')
const outboxed = cds.outboxed(srv)
await outboxed.emit('someEvent', { some: 'message' }) // asynchronous
await outboxed.send('someEvent', { some: 'message' }) // synchronous::: tip
You still need to await these operations. Then the messages are stored in the database, together with the main transaction.
:::
The cds.outboxed function can also be called with optional configuration options.
const outboxed = cds.outboxed(srv, { kind: 'persistent-outbox' })The persistent outbox can only be used if it's enabled globally with
cds.requires.outbox = truebecause it requires a dedicated database table.
::: warning One-time configuration Once you outboxed a service, you cannot override its outbox configuration options again. :::
Use this on an outboxed service to get back the original service:
const unboxed = cds.unboxed(srv)This is useful if your service is outboxed per configuration.
You can also configure services to be outboxed by default:
{
"requires": {
"yourService": {
"kind": "odata",
"outbox": true
}
}
}::: tip Outboxed by default
Some services are outboxed by default, these include cds.MessagingService and cds.AuditLogService.
:::
For transactional safety, you're encouraged to enable the persistent outbox.
You can enable it globally for all outboxed services with:
{
"requires": {
"outbox": true
}
}Using the persistent outbox, the to-be-emitted message is stored in a database table first. The same database transaction is used as for other operations, therefore transactional consistency is guaranteed.
You can use the following configuration options:
{
"requires": {
"outbox": {
"kind": "persistent-outbox",
"maxAttempts": 20,
"chunkSize": 100,
"storeLastError": true,
"parallel": true
}
}
}The optional parameters are:
maxAttempts(default20): The number of unsuccessful emits until the message is ignored. It will still remain in the database table.chunkSize(default100): The number of messages which are read from the database table in one go.storeLastError(defaulttrue): Specifies if error information of the last failed emit should be stored in the outbox table.parallel(defaulttrue): Specifies if messages are sent in parallel (faster but the order isn't guaranteed).
Once the transaction succeeds, the messages are read from the database table and emitted. If an emit was successful, the respective message
is deleted from the database table. If not, there will be retries after (exponentially growing) waiting times.
After a maximum number of attempts, the message is ignored for processing and remains in the database table which
therefore also acts as a dead letter queue.
There is only one active message processor per service, tenant and app instance, hence there won't be
duplicate emits except in the unlikely case of an app crash right after the emit and before the deletion of the
message entry.
::: tip
Some errors during the emit are identified as unrecoverable, for example in SAP Event Mesh if the used topic is forbidden.
The respective message is then updated and the attempts field is set to maxAttempts to prevent further processing.
Programming errors crash the server instance and must be fixed.
:::
Your database model is automatically extended by the entity cds.outbox.Messages:
namespace cds.outbox;
entity Messages {
key ID : UUID;
timestamp : Timestamp;
target : String;
msg : LargeString;
attempts : Integer default 0;
partition : Integer default 0;
lastError : LargeString;
lastAttemptTimestamp : Timestamp @cds.on.update: $now;
}In your CDS model, you can refer to the entity cds.outbox.Messages using the path @sap/cds/srv/outbox,
for example to expose it in a service.
- If the app crashes, another emit for the respective tenant and service is necessary to restart the message processing.
- The emitting service must not use user roles and attributes as they are not stored. However, the user id is stored to recreate the correct context.
- The emitting service must not perform any database modifications, because a global database transaction is used when dispatching the events.
You can enable it globally for all outboxed services with:
{
"requires": {
"outbox": {
"kind": "in-memory-outbox"
}
}
}Messages are emitted when the current transaction is successful. Until then, messages are only kept in memory. This is similar to the following code if done manually:
cds.context.on('succeeded', () => this.emit(msg))::: warning No retry mechanism The message is lost if its emit fails, there is no retry mechanism. :::
To disable deferred emitting for a particular service, you can set the outbox option of your service to false:
{
"requires": {
"messaging": {
"kind": "enterprise-messaging",
"outbox": false
}
}
}To manually delete entries in the table cds.outbox.Messages, you can either
expose it in a service or programmatically modify it using the cds.outbox.Messages
entity:
const db = await cds.connect.to('db')
const { Messages } = db.entities('cds.outbox')
await DELETE.from(Messages)If the outbox table is not found on the database, this can be caused by insufficient configuration data in package.json.
In case you have overwritten requires.db.model there, make sure to add the outbox model path @sap/cds/srv/outbox:
The following is only relevant if you're using @sap/cds version < 6.7.0 and you've configured options.model in custom build tasks.
Add the model path accordingly:
"build": {
"tasks": [{ ...
"options": { "model": [..., "@sap/cds/srv/outbox"] }
}]
}Note that model configuration isn't required for CAP projects using the standard project layout that contain the folders db, srv, and app. In this case, you can delete the entire model configuration.