Handling & Testing idempotency in java

I have a simple activity and am looking to ensure its idempotent, for that I will use the following:

String idempotencyKey = info.getRunId() + “-” + info.getActivityId();

Persisted in a database as a primary key and checked for each execution of the activity.

In the example code: What Is Idempotency? Why It Matters for Durable Systems | Temporal

if ExternalAPI.UserExists(userData.Email) {
return User{}, temporal.NewApplicationError(“Can’t create user: email already exists”, “UserAlreadyExists”)
}

What should the java equivalent be here? How do we distinguish errors in an activity that result in the activity being retried, Vs an idempotency error which results in the workflow continuing to the next step? How would we test that in some form of integration test to see the different behaviour?

At the moment i’ve no way to trigger a replay of an activity to validate that idempotency handling works.

What should the java equivalent be here?

You can define a custom exception type, for example UserAlreadyExistsException and in activity options set it as a non-retryable failure, for example:

ActivityOptions.newBuilder()
    // ...
    .setRetryOptions(RetryOptions.newBuilder()
            .setDoNotRetry(UserAlreadyExistsException.class.getName())
            .build())
    .build());

in your workflow code then you can handle ActivityFailure and check its cause to see type, for example:

try {
  activities.createUser(User user);
} catch (ActivityFailure activityFailure) {
  if (activityFailure.getCause() instanceof ApplicationFailure) {
    ApplicationFailure af = (ApplicationFailure) activityFailure.getCause();
    // af.getMessage();
    // af.getType(); --> would be UserAlreadyExistsException
  } else if (activityFailure.getCause() instanceof TimeoutFailure) {
    TimeoutFailure tf = (TimeoutFailure) activityFailure.getCause();
    // tf.getTimeoutType(); -- can be schedule_to_start/schedule_to_close
  }
}

In case of a timeout, meaning activity timed out on last attempt, means your code maybe never executed, so maybe best to invoke activity again which would check if it was actually ever created.

Alternatively you could from activity code throw a non-retryable application failure with cause:

throw ApplicationFailure.newNonRetryableFailureWithCause(...);

where you can specify details that can later on inspect in workflow code

// ...
if (activityFailure.getCause() instanceof ApplicationFailure) {
  ApplicationFailure af = (ApplicationFailure) activityFailure.getCause();
  // af.getDetails();

}

Thanks tihomir, very clear. I think what had confused me was how replay would handle activity invocation, i.e. when a workflow is replayed i was worried that it would invoke the activities that had already run, but as i understand it, it will not do that. It will use the stored history from the activities and get the workflow back to where it was running.

I guess the idempotency use case is more focused on activity retries rather than a full workflow replay?