Reentrancy improvements to Minions in Sitecore Commerce 9.1
tl;dr In XC91, you’ll no longer need to worry about how long your Minion takes to run. Just override Minion.Execute
rather than Minion.Run
and you’re good to go.
Back in March of last year, I responded to StackExchange question on the limitations of Sitecore Commerce’s Minion in terms of reentrancy. Put more plainly, in XC9.0 Minions used a “fire and forget” approach for starting each ‘invocation’ which resulted in Minions running twice in parallel when the previous invocation took longer to execute than the configured WakeupInterval
.
In my answer I described two workarounds to the problem, both having a subtle impact on the behavior of Minions:
- Detect reentrant calls using
Interlocked.CompareExchange
and skip them, which may result in an “invocation” being skipped entirely - Delay the WakeupInterval pause until after the invocation is complete, which moves the pause from the “start” of the invocation to the end
The recently released Commerce 9.1 aims to tackle this issue of reentrancy and the implementation actually takes a little from both of the approaches above, as well as handling some additional scenarios. Let’s start with an overview of how Minions are started in XC9.0:
StartEnvironmentMinionsBlock
Minion.Start()
loop forever
Minion.Run (fire and forget) <-- RunMinion calls this
WakeupInterval (blocking)
And here’s a visualisation to illustrate the problem this causes:
As you can see, XC 9.0 starts it’s WakeupInterval delay from the moment Minion.Run is invoked. This is actually fine… for some configurations. The problem arises when Run takes longer than WakeupInterval, and results in Run being executed twice in parallel. If a Minion was not written with parallel execution in mind, it can cause some serious problems.
I’m happy to report that XC 9.1 fixes all of the flaws in the original design. Starting with the issue described above, Minions now apply the WakeupInterval
delay after the main method (now called Execute
, with Run
being marked as “obsolete”) completes asynchronously:
As you can see, it’s now impossible for Execute
to be invoked twice in parallel.
The changes don’t stop there, though. In XC90 it will also possible to start a minion via the RunMinion
API while it was already running, resulting in it running twice in parallel. The new model not only prevents the same Minion from running twice, but will also skip an invocation if there is another Minion running that includes any of the values from its MinionPolicy.Entities
array. It also doesn’t matter how the Minion is started: calling the RunMinion
API will still skip execution if the Minion is currently executing on its schedule.
Here’s a deeper look into the new flow:
StartEnvironmentMinionsBlock
Minion.StartAsync() - called by StartEnvironmentMinionsBlock
Warn if no WakeupInterval set
loop forever
await Minion.Process <-- RunMinion calls this
Skip if policy is marked as running
Skip if policy with any intersecting `Entities` value is running
Mark as running
await Minion.Execute
Mark as not running
await WakeupInterval
To summarise the behaviour changes:
- Override Minion.Execute instead of Minion.Start
- As before, handling exceptions is the responsibility of Minion.Execute
- WakeupInterval now starts after Execute has completed asynchronously
- Execute will be skipped if the minion is already running (either on a timer or via API)