Deadline Migration
Axon Framework 5 has no built-in deadline scheduling.
We have decided against this, as it is rather straightforward to schedule messages yourself.
You can schedule a command or an event yourself by picking your desired scheduling library and invoke the CommandGateway or EventGateway to dispatch the message at your desired time.
For more on how we envision message scheduling going forward, be sure to check out our Message Scheduling guide here.
However, we are aware you have perhaps already scheduled deadlines and want to move those to Axon Framework 5. To that end, we added axoniq-legacy that allows migrating already-scheduled deadlines, the same way it allows migrating running sagas.
The deadline support has moved over into axoniq-legacy, and takes a slightly different turn whether the deadline was scheduled in a saga or in an aggregate.
Please check the respective sections for details on either.
First, let’s take a look at the general approach to migrate your deadlines into Axon Framework 5:
-
Add draining support. To drain your Axon Framework 4 deadlines in Axon Framework 5, add the
axoniq-legacydependency. This gives you all theDeadlineManagerimplementations, hooks, and configuration options. Configure the deadline manager so it reads what Axon Framework 4 stored, as described in Configuring the deadline manager. -
Migrate saga deadline handlers. Depending on
axoniq-legacyis sufficient, so you’re all set! Be sure to migrate your sagas though. -
Migrate entity deadline handlers. You need to replace
@DeadlineHandlerannotations for@CommandHandlerannotations in your aggregates. Be sure to check out Aggregate deadlines for more details on the exact structure. -
Stop scheduling new deadlines. For active sagas, the
DeadlineManagercan still be invoked. Just ensure you do not start any new sagas. For aggregates, you should remove theDeadlineManagerentirely. Instead, schedule commands with your preferred scheduling tool yourself, as explained in more detail here. -
Wait for scheduled deadlines to drain. Monitor the deadline manager’s store for outstanding deadlines against the migrated entity type, and watch the count fall to zero.
-
Retire the deadline manager once there are no more outstanding deadlines for the migrated entity. You can then remove all remaining usage of the
DeadlineManager.
|
Automated code conversion can take care of much of this mechanically. See OpenRewrite code conversion for the available recipes and how to run them. |
Configuring the deadline manager
axoniq-legacy ports the Quartz, JobRunr and db-scheduler deadline managers of Axon Framework 4.
They read the deadlines Axon Framework 4 stored, and fire them in Axon Framework 5.
-
Match the Axon Framework 4 serializer. A deadline manager converts the stored payload, metadata and scope with its
Converter, which has to match the serializer the Axon Framework 4 deadline manager used, including its configuration (such as a customizedObjectMapper). Axon Framework 4’s JobRunr and db-scheduler auto-configurations stored deadlines with the event serializer, not the general serializer. The Spring Boot auto-configurations ofaxoniq-legacytherefore build these managers with theEventConverter. Configure the event converter to match the Axon Framework 4 event serializer, or define your ownDeadlineManagerbean with another converter. -
Quartz has no auto-configuration. Define a
QuartzDeadlineManageryourself, with theScopeAwareProviderandUnitOfWorkFactoryof the configuration:@Bean public DeadlineManager deadlineManager(Scheduler scheduler, ScopeAwareProvider scopeAwareProvider, Configuration configuration) { return QuartzDeadlineManager.builder() .scheduler(scheduler) .scopeAwareProvider(scopeAwareProvider) .unitOfWorkFactory(configuration.getComponent(UnitOfWorkFactory.class)) // the counterpart of the serializer your Axon Framework 4 deadline manager used .converter(new JacksonConverter()) .build(); }In Spring Boot, the Quartz
Schedulerstarts after the application has created its beans, so the deadline manager is ready before the first deadline fires. Without Spring, create theQuartzDeadlineManagerbefore callingscheduler.start(). A deadline that fires before the manager exists fails without a retry, and Quartz deletes it, as in Axon Framework 4. -
JobRunr cannot cancel all deadlines within a scope.
JobRunrDeadlineManager.cancelAllWithinScopethrows anUnsupportedOperationException, as it did in Axon Framework 4. Cancel JobRunr deadlines by their schedule identifier instead. -
db-scheduler drops the scheduled events of Axon Framework 4. Axon Framework 4’s db-scheduler auto-configuration also registered the event scheduler’s
AxonScheduledEventtask, whichaxoniq-legacydoes not provide yet. On a db-scheduler table shared with Axon Framework 4 nodes, db-scheduler deletes all executions of a task no node knows, once the first one has been unresolved fordeleteUnresolvedAfter(14 days by default). To keep your scheduled events, either let them fire before the last Axon Framework 4 node stops, or set a large finitedeleteUnresolvedAfteron the scheduler, such asDuration.ofDays(36500). They then wait, unfired, untilaxoniq-legacysupports them.
Saga deadlines
A deadline scheduled from, and handled by, a saga needs no migration action apart from depending on axoniq-legacy.
The axoniq-legacy module registers all required deadline-specific components for you, and ensures your saga deadlines keep working as they are.
This makes it so that existing @DeadlineHandler annotated methods in a saga keep firing unchanged.
However, as Sagas are deprecated, just as Deadlines, you are expected to migrate those as well. Check out the Saga Migration for the full story, including what to do once every saga instance has drained and the legacy module is no longer needed.
Reading deadlines stored by Axon Framework 4
A persistent deadline manager (Quartz, db-scheduler or JobRunr) stores each pending deadline in serialized form.
Axon Framework 4 wrote it with your Serializer.
Axon Framework 5 reads it back with a Converter, and the formats must match:
-
The auto-configured db-scheduler and JobRunr deadline managers use the event
Converter, which is Jackson-based by default. -
A deadline manager you build yourself uses the
Converteryou pass to itsconverter(..)builder method.
Check which serializer your Axon Framework 4 application used for deadlines.
If it set no axon.serializer.* properties and built no serializer itself, that was XStream.
Reading XStream data with Jackson fails with Unexpected character ('<') before the deadline handler runs, and the scheduler retries the deadline without ever delivering it.
For XStream data, configure the XStreamConverter until the Axon Framework 4 deadlines have drained.
For Jackson data, make sure your Converter uses the same ObjectMapper settings your JacksonSerializer had.
Aggregate deadlines
In Axon Framework 4, scheduling a deadline against an aggregate meant scheduling a reminder tied to that specific aggregate instance: once the deadline’s trigger time arrived, it fired directly onto a @DeadlineHandler method on that same instance, without going through a command.
Axon Framework 5 drops that direct delivery mechanism, for the same reason explained above: it is simple enough to achieve the same effect yourself, by scheduling a command that targets the entity instead. Going forward, what used to be "a deadline firing on an aggregate" becomes "a scheduled command handled by an entity", handled the same way as any other command.
axoniq-legacy helps you make that transition: it automatically drains every deadline Axon Framework 4 already scheduled against an aggregate, by translating it into a command and dispatching it through the CommandGateway, so you do not need to re-schedule anything yourself.
This changes what the migrated entity needs: replace the aggregate’s @DeadlineHandler method with a
@CommandHandler method named <aggregateType>.<deadlineName>, as shown below for an Order aggregate, first for a
deadline scheduled with a payload, then for one scheduled without.
This name keeps deadlines apart that share a payload type, and deadlines of different aggregate types that share a
name.
The aggregate type is the one Axon Framework 4 stored with the deadline: the aggregate’s simple class name, unless
@Aggregate(type = …) set another one.
Keep that name in the command name, even when the migration renames the class.
-
Axon Framework 4
-
Axon Framework 5
@DeadlineHandler(deadlineName = "paymentDue")
public void onPaymentDue(PaymentDueReminder payload) {
// ...
}
@CommandHandler(commandName = "Order.paymentDue")
void handle(PaymentDueReminder command) {
// ...
}
A payload-less deadline is named the same way, carrying the deadline name as its String
payload:
-
Axon Framework 4
-
Axon Framework 5
@DeadlineHandler(deadlineName = "accountExpired")
public void onAccountExpired() {
// ...
}
@CommandHandler(commandName = "Order.accountExpired")
void handle(String deadlineName) {
// ...
}
|
A handler migrated with |
|
Cancelling Axon Framework 4 deadlines from an entity
While Axon Framework 4 deadlines drain, an entity’s command handler may still need to cancel them.
Declare a DeadlineManager parameter on the handler for that.
It is bound to the handler’s processing context, so its calls take effect when the command’s processing commits, as
they did in Axon Framework 4.
A DeadlineManager held in a field, or a parameter declared as an implementation type such as
QuartzDeadlineManager, acts immediately instead.
Axon Framework 5 entities have no current scope, so the DeadlineManager methods that take no ScopeDescriptor
throw an IllegalStateException in an entity’s handler.
Pass the aggregate’s scope explicitly, with the aggregate type and identifier the deadline was scheduled with:
@CommandHandler
void handle(PayOrder command, DeadlineManager deadlineManager) {
deadlineManager.cancelAllWithinScope("paymentDue", new AggregateScopeDescriptor("Order", orderId));
// ...
}
Delivery of a slow command
A fired aggregate deadline waits for its command’s result for 30 seconds at most. When that wait times out, the delivery fails, and the deadline manager retries the deadline as it retries any failed delivery. With a distributed command bus, the command may still be running on another node at that moment. The retry then sends the command again, while the first one may still commit, so the entity handles the deadline twice. Keep the command handlers of migrated deadlines well within this time, or make them idempotent.