Saga upcasting?

Hello,

I have some general questions about SAGAs and the decision to keep the state in serialized version in the database.

  1. If you want to refactor a saga, the structure of the fields could change (for example rename some fields, add some new structures).
    What to do with the serialized state of the ongoing transactions? Is there anyway to “upcast” this state, as this might not able to deserialize.

  2. I had the idea to have a new type of Saga to do refactoring. We keep the old Saga, XXXXSaga en we remove the @StartSaga so the old saga will end their lifecycle with the old saga. And we create a new XXXXSagaV2 to handle the new transactions. This look to be a workaround for the serialized state.

  • But what if need to revert, both scenarios are not revert-able.
  • How to write an integration test to test the transition period? (with open sagas during the release)

Kind regards,
Koen

There isn’t any baked-in support for saga upcasting the way there is for events. We’ve taken two different approaches to this depending on the specifics of the change.

Easiest is to write a migration to update the saga data directly in the database by manipulating the XML or JSON. If you normally take your application down to deploy new code anyway, this may be the best option.

Harder is to migrate in several steps. Add migration code to the saga itself and make its fields backward-compatible at each step.

We prefer to avoid downtime if at all possible since our application exposes a public API that’s used 24x7, so we almost always use the second approach. In practice, this usually takes three code releases because we run multiple instances of our application and do rolling deploys, so during the deploy some instances are running code revision N and some are running N-1 and the two need to interoperate.

First, a bit of prep work, which only needs to be done once and can be reused across many migrations: Add a “migrate yourself” event type that’s handled by every saga class in your system. When any saga instance starts up, associate with a constant value that’s present on the event. We use the saga class name. The event handler can be empty in normal operation; you’ll only add code to it when you need to do a migration. Something like this:

`
public class SagaMigrationRequestedEvent {
private String className;
public SagaMigrationRequestedEvent(String className) { this.className = className; }
public String getClassName() { return className; }
}

public class MySaga {

@StartSaga
public void on(MySagaStartEvent event) {

associateWith(“className”, getClass().getName());
}

@SagaEventHandler(associationProperty = “className”)
public void on(SagaMigrationRequestedEvent event) {
// This will normally be empty
}
}

`

With that in place, migrations follow a sequence along these lines.

Release 1. Add the new fields, but make them null by default so they aren’t included in the serialized representation of the saga objects. If any fields you’re getting rid of are primitives (int, long, etc.) change them to boxed values (Integer, Long, etc.) Add code such that if the new fields are set, they’re used, but otherwise the old fields are used. Always write the old fields.

Release 2. Add code to the migration event handler to migrate data from the old fields to the new ones and then set the old fields to null. Change the saga to start writing the new fields instead of the old fields.

Once release 2 is running everywhere, publish the migration event so all existing saga instances will be updated to only have values in the new fields.

Release 3. Remove the old fields and the migration code.

You can safely roll back release 1 without any ill effects. You can roll back release 2 to release 1 but not to the version before release 1. You can roll back release 3 to release 2. If you want to roll back further than that, you’ll need to be able to migrate the saga data in the opposite direction. There’s pretty much no avoiding this since sagas are mutable, unlike events.

The approach you suggest (introducing an entirely new saga class) could also work, but it means you’ll have a mix of two separate saga classes running business logic at the same time, and you’ll have to keep the old code around for the lifetime of any existing saga instances. Whether or not that’s problematic obviously depends on your application. It has one potential gotcha if you’re rolling out releases across multiple instances of the application: an instance that is still running the old code (that lacks the new class) could encounter an event that has an association with an instance of the new class, and it would then fail to load the saga because the new class wouldn’t be found. Again, whether that’s a problem or not is up to your application. It’s only an issue if you do zero-downtime deploys; if you take the application completely down to move to new code releases, you’d never hit that problem.

-Steve

1 Like