I take it that the work of this Activity happens in Service B, and the work takes too long for a synchronous call like this?
async function myActivity() {
await result = serviceB.doWork()
return result
}
In these cases, I’d recommend considering options 1 and 2 before the options you described (3 & 4):
1. Service B runs a Worker
- The Workflow in Service A starts
activityFoo
on taskQueue: service-B
.
- Service B runs a Worker that listens on
taskQueue: service-B
and only registers/handles activityFoo
.
Pro: Simplest
2. Service A starts the Activity, Service B completes it
- The Workflow in Service A starts
activityFoo
- The Worker in Service A picks up
activityFoo
- The Activity does:
async function activityFoo() {
const info = Context.current().info
const idempotencyToken = getIdempotencyToken()
await serviceB.startWork({
fullActivityId: {
activityId: info.activityId,
workflowId: info.workflowExecution.workflowId,
runId: info.workflowExecution.runId,
},
idempotencyToken,
})
throw new CompleteAsyncError()
}
// Note: there are different ways to create idempotency tokens, depending on how you
// run your Workflow—whether it can be retried, Continued As New, or restarted by a Client.
// This implementation may not be what you want.
function getIdempotencyToken() {
return Context.current().info.workflowExecution.runId + Context.current().info.activityId
}
- Service B uses
AsyncCompletionClient
to heartbeat, and when it’s done with the work, does:
await asyncClient.complete(fullActivityId, "Job's done!")
Con: You have to pass the fullActivityId
and create an idempotency token.
3. Service A polls Service B
- Service A runs the Activity, which first tells Service B to do the work, and then heartbeats and polls Service B until it’s done. Something like:
async function activityFoo() {
const info = Context.current().info
const idempotencyToken = getIdempotencyToken()
await serviceB.startWork({
fullActivityId: {
activityId: info.activityId,
workflowId: info.workflowExecution.workflowId,
runId: info.workflowExecution.runId,
},
idempotencyToken,
})
while (true) {
Context.current().heartbeat()
const response = await serviceB.getStatus({ idempotencyToken })
if (response.completed) {
return response.result
}
await Context.current().sleep('1 min')
}
}
Con: In addition to #2 con, there’s more code and network requests.
4. Service B reports completion via API call
- Service A runs an Activity that includes
serviceB.startWork()
, as in scenario #2
- Service A runs an API server with a
/complete/{runId}/{activityId}
endpoint (or equivalent gRPC method)
- When Service B is done with the work, it calls the
/complete
endpoint
- The Service B API handler uses the
AsyncCompletionClient
to complete the Activity
Cons:
- The API server is added complexity.
- No heartbeating, which means Temporal won’t know when Service B stops doing the work—Temporal will have to wait until the
scheduleToClose
or startToClose
timeout to know it failed. And Service B won’t know if the Activity gets Cancelled. So ideally Service B would heartbeat, but if it’s going to do that, it might as well report completion as well, in which case we can do scenario #2.