Hi Discuss,
I face an issue with in-flight commands bookkeeping in what I believe is a valid use case. Affected Daml version: 1.18.1. Suppose the following code:
template PostalService
with
letters : [(Address, Letter)]
where
choice TryDeliverLetters : ()
do
result <- tryDeliverLetters letters
case getUnsuccessful result of
[] -> pure ()
someLetters -> create $ DeliveryPending someLetters
template DeliveryPending
with
letters : [(Address, Letter)]
where
-- exercised by trigger when the global address store is updated
-- achives itself conditionally
nonconsuming choice RetryDeliverLetters : ()
do
result <- tryDeliverLetters letters
case getUnsuccessful result of
[] -> archive self
someLetters
| length someLetters < length letters -> do
archive self
create $ DeliveryPending someLetters
| otherwise -> pure () -- to avoid the trigger picking up the contract again and again
---- trigger:
trigger = Trigger {
...
, registeredTemplates = [ registeredTemplate @GlobalAddressStore, registeredTemplate @DeliveryPending ]
, rule = const retryDelivery
...
}
retryDelivery = do
pendingDeliveries <- query @DeliveryPending
forA_ pendingDeliveries \(id, _) -> dedupExercise id RetryDeliverLetters
The success of delivering letters depends on some other contract, which holds the list of all known addresses (say, of a country).
RetryDeliverLetters
is exercised by a trigger whenever the global address store is updated. Most of the time, RetryDeliverLetters
doesn’t change the ledger and thus the exercise command sent by the trigger remains in-flight indefinitely. Ultimately, exercise command is sent once, then dedupExercise
becomes no-op.
I believe this behavior relates to the following bug report: [BUG] Triggers show already completed commands as in-flight · Issue #12233 · digital-asset/daml · GitHub
My questions: is this an anti-pattern of how triggers are supposed be used? Or is it a use case not covered yet by the Daml runtime?
We have 2 workarounds for the behavior of dedupExercise
and how in-flight commands are handled:
- use
emitCommands
instead ofdedupExercise
- pass a dummy argument to
RetryDeliverLetters
so thatdedupExercise
doesn’t realize that we are actually sending the same command:nonconsuming choice RetryDeliverLetters : () with dummyArgToDistinguishCalls : ContractId GlobalAddressStore do result <- tryDeliverLetters letters case getUnsuccessful result of [] -> archive self someLetters | length someLetters < length letters -> do archive self create $ DeliveryPending someLetters | otherwise -> pure () -- to avoid the trigger picking up the contract again and again
In both cases, the in-flight commands container retains commands indefinitely and keeps growing (leaking memory).