Can someone point me to where in the Golang SDK repo the serialization/deserialization happens? I’m thinking about how to build out an internal layer for idempotency by capturing the serialized/deserialized results and storing it transactionally with the rest of the activity execution on our DB.
Or is Temporal utilizing a base Golang construct like JSON encoding?
The serialization is handled by DataConverter. Note that we expect to refactor its implementation in the near future.
Would you elaborate on the idempotency layer? Temporal allows configuring activity retry policy to not retry it unless asked by a workflow. What exactly are you trying to achieve adding the DB in the mix?
Hmm - I’m not sure I understand “retry it unless asked by a workflow”.
I think you’ve noted the case that would likely run into these issues. Imagine that your activity is 1 DB transaction. The DB transaction is committed, but when returning to Temporal, the activity cannot be mark completed because of a network connectivity issue to Temporal (rare but possible).
I believe Temporal would retry the activity after the timeout since it hasn’t been marked complete. If that’s the case, then I wouldn’t want the transaction to run again. I would also want the previous attempt to return some interface{} to Temporal to return the same value.
My thought would be the following:
Create some notion of an idempotent transactional activity for my DB. My DB would have a DB table like (workflow_run_id, activity, activity_return_blob)
Internal utility would be able to transactionally insert activity_return_blob into this DB table
Activity logic would be as follows:
begin DB transaction
check idempotency_table for (workflow_run_id, activity)
if it exists, return activity_return_blob
...
execute a bunch of code in a DB transaction
...
save workflow_run_id, activity, activity_return_blob
commit to DB
return activity_return_blob
This allows activity returns to be consistent across the retry if something is needed to be idempotent. Saving this record to the DB requires similar data conversion as Temporal, so that’s why I’m thinking about using similar logic to do this.
Hmm - I’m not sure I understand “retry it unless asked by a workflow”.
When an activity has retry options with MaximumAttempts set to 1, Temporal is going to execute it exactly once or deliver a timeout to the workflow. So the workflow can execute some other activity to check if the update emitted by the first activity was indeed committed.
Your proposal makes sense to me. I don’t think that DataConverter is the right place for such logic. You either put it into an ActivityInterceptor or just create some DB wrapper that is used by the activity code directly.
If I understand you correctly, you are describing a “standard” distributed commit problem, the parties being your DB and Temporal’s activity manager.
Have you considered one of the “standard” and cheap solutions for this: generate a domain specific transaction id for each invocation of your activity, and pass it as an argument to the activity?
Have the workflow (your business logic) that invokes the activity generate a unique code. It is your business logic, so you can always make up a unique value that could never ever be re-used again! When the activity commits, store this code as part of the transaction in a way that allows efficient look up.
Then if the activity is retried due to a failure after commit, you will see the same unique id in the activity arguments. You use it to detect that the activity is being re-run, and if so you simply skip ahead to (re)sending the response.
I believe this will require zero additional support from the framework, and you can choose the best id generation solution for your specific application.
I hope I’m not totally misunderstanding your problem, or telling you something you already knew and eliminated as a possibility
If there’s some fundamental reason this very old school approach is not suitable with Temporal I’d really like to understand why (I’m new here!).
Maybe I’m not communicating this correctly above - but I think that’s exactly what my solution intends on doing. This is a bit of an odd question for this community since this isn’t really about Temporal, but more so - how do you know what to resend as the response?
My idea is to exactly use a unique ID - in this scenario, I’ve designated it as workflow run ID + activity name, but I realistically would want this provided as an argument. This unique ID would exist in our DB alongside some blob that was the previous intended return result of the activity.
What I love about Temporal is that the data conversion is done for us without the need for struct tags, etc. I’d love to use that data conversion to store the Golang objects to return in my DB. I’d love to utilize similar logic/behavior since Temporal seems to do it so well, but not necessarily incorporate it directly from the framework.
I’m a bit of a Golang newbie, so that’s why I need to rely on a good example like Temporal’s implementation .
If it helps any (and you didn’t already know this): you can fetch the invoking workflow name/id, the activity name/run id/retry number and few other tidbits about your activity from its activity context. It’s not an explicit argument to your activity, but this is all available to you via the activity environment.
I was planning to use this to build the unique id for similar use cases. It seems to do the trick (still need to validate from temporal docs/code).