Internally DABL heavily relies on Request
contracts; a good example is the creation of a ledger:
template LedgerRequest
with
user : Party
operator : Party
ledgerName : Text
projectId : Text
where
signatory user
controller user can
LedgerRequestCancel : ()
do return ()
controller operator can
LedgerRequestAccept : ContractId Ledger
with
acceptTime : Time
ledgerId : Text
do
create Ledger with
createTime = acceptTime
owner = user
ledgerData = LedgerData with
metadata = empty
..
..
LedgerRequestReject : ()
do return ()
template Ledger
with
operator : Party
owner : Party
ledgerData : LedgerData
createTime : Time
where
signatory operator, owner
key (operator, ledgerData.ledgerId) : (Party, Text)
maintainer key._1
controller operator can
LedgerUpdateMetadata : ContractId Ledger
with
newMetadata : [(Text, Text)]
do
create this with
ledgerData = ledgerData with
metadata = fromList newMetadata
LedgerOperatorArchive :
ContractId ArchivedLedger
with
archiveTime : Time
do
create ArchivedLedger with ..
controller owner can
LedgerOwnerArchive :
ContractId ArchivedLedger
with
archiveTime : Time
do
create ArchivedLedger with ..
The UI issues a POST HTTP call to a web service, which in turn uses the gRPC Ledger API to create LedgerRequest
with a party that corresponds to the user. A bot in a separate process sees the request, and calls either LedgerRequestAccept
or LedgerRequestReject
. The pattern works well when you stay on the happy path.
The Unhappy Path: Some failure modes that we are starting to encounter
Bot not running when a Request contract is created
Not all of our handlers consider both the ACS and transaction event stream as sources of Request contracts. There is no single gRPC Ledger API primitive for this stream; the HTTP JSON Websocket API does implement semantics that allow the client to abstract away whether a Request contract existed or not. However, that is not necessarily without its problems as wellâŚ
Poison pill problem
Bot receives a contract that confuses it enough to crash, hard.
If the bot ignores all requests that are issued when the bot is not running, then the bot recovers, but an unacknowledged request remains sitting on the ledger forever.
If the bot retroactively acts on requests that already exist in the ledger on startup, the bot runs the risk of crashing again on the same payload (poison pill), preventing it from ever starting again.
This can somewhat be mitigated by all clients following a pattern with affordances in the models to allow this:
def onContract(event) {
if event.cdata.allowedRetryCount > 0 {
try {
process(event)
} catch {
decrementAllowedRetryCount(event.cid)
}
} else {
killRequest(event.cid)
}
}
However, a poison pill could still easily occur by exploiting mistakes in the models/bot that allow a bot visibility into a contract that it canât actually act on, even though the bot is coded with the expectation that it can with respect to tracking the retry count as a field in a template.
Temporarily failure to process problem
If a poison pill does not crash the process but merely fails to process temporarily (we observe this with bots that make decisions based on the result of an external service call), there is no way, gRPC or HTTP JSON API, to ârewindâ the tape and re-process failed requests. In all of DABLâs current systems, this results in requests remaining âstuckâ until processes are restarted (and even then, thatâs only if the ACS is considered). However, maniacal retries may also cause the process to be stuck endlessly retrying a request that is doomed to forever fail.
There isnât a question that needs to be answered for this post; this is merely our current state thinking on the various pros/cons to employing different approaches to application building over the Ledger API, particularly when using it in a similar fashion to what message queues might traditionally be used for.
References
http://zguide.zeromq.org/php:chapter4
https://www.rabbitmq.com/dlx.html