Skip to content

Commit 29b936d

Browse files
authored
Handle ClientInput operation failures in HTTP API responses (#2184)
Resolves #1911 <!-- Describe your change here --> --- <!-- Consider each and tick it off one way or the other --> * [x] CHANGELOG updated or not needed * [x] Documentation updated or not needed * [x] Haddocks updated or not needed * [x] No new TODOs introduced or explained herafter
2 parents a3061a1 + 8201de3 commit 29b936d

File tree

3 files changed

+283
-34
lines changed

3 files changed

+283
-34
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ changes.
5555

5656
- Add API endpoint `POST /transaction` to submit transaction to the head.
5757

58+
- Improve HTTP API status codes for side-effecting endpoints to reflect operation outcome:
59+
- `POST /snapshot`: 200 on successful side-load, 400 on validation failure, 202 on timeout
60+
- `POST /decommit`: 200 on finalize, 400 on invalid/failed, 202 on timeout
61+
- `DELETE /commits/:txid`: 200 on recovered, 400 on failed, 202 on timeout
62+
- See [Issue #1911](https://github.com/cardano-scaling/hydra/issues/1911) and [PR #2124](https://github.com/cardano-scaling/hydra/pull/2124).
63+
5864
- Tested with `cardano-node 10.4.1` and `cardano-cli 10.8.0.0`.
5965

6066
Fix rotation log id consistency after restart by changing the rotation check to trigger only

hydra-node/src/Hydra/API/HTTPServer.hs

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Data.ByteString.Short ()
1414
import Data.Text (pack)
1515
import Hydra.API.APIServerLog (APIServerLog (..), Method (..), PathInfo (..))
1616
import Hydra.API.ClientInput (ClientInput (..))
17-
import Hydra.API.ServerOutput (ClientMessage, CommitInfo (..), ServerOutput (..), TimedServerOutput (..), getConfirmedSnapshot, getSeenSnapshot, getSnapshotUtxo)
17+
import Hydra.API.ServerOutput (ClientMessage (..), CommitInfo (..), ServerOutput (..), TimedServerOutput (..), getConfirmedSnapshot, getSeenSnapshot, getSnapshotUtxo)
1818
import Hydra.Cardano.Api (Coin, LedgerEra, Tx)
1919
import Hydra.Chain (Chain (..), PostTxError (..), draftCommitTx)
2020
import Hydra.Chain.ChainState (IsChainState)
@@ -230,21 +230,21 @@ httpApp tracer directChain env pparams getHeadState getCommitInfo getPendingDepo
230230
respond . okJSON $ getSeenSnapshot hs
231231
("POST", ["snapshot"]) ->
232232
consumeRequestBodyStrict request
233-
>>= handleSideLoadSnapshot putClientInput
233+
>>= handleSideLoadSnapshot putClientInput apiTransactionTimeout responseChannel
234234
>>= respond
235235
("POST", ["commit"]) ->
236236
consumeRequestBodyStrict request
237237
>>= handleDraftCommitUtxo env pparams directChain getCommitInfo
238238
>>= respond
239239
("DELETE", ["commits", _]) ->
240240
consumeRequestBodyStrict request
241-
>>= handleRecoverCommitUtxo putClientInput (last . fromList $ pathInfo request)
241+
>>= handleRecoverCommitUtxo putClientInput apiTransactionTimeout responseChannel (last . fromList $ pathInfo request)
242242
>>= respond
243243
("GET", ["commits"]) ->
244244
getPendingDeposits >>= respond . responseLBS status200 jsonContent . Aeson.encode
245245
("POST", ["decommit"]) ->
246246
consumeRequestBodyStrict request
247-
>>= handleDecommit putClientInput
247+
>>= handleDecommit putClientInput apiTransactionTimeout responseChannel
248248
>>= respond
249249
("GET", ["protocol-parameters"]) ->
250250
respond . responseLBS status200 jsonContent . Aeson.encode $ pparams
@@ -329,15 +329,38 @@ handleRecoverCommitUtxo ::
329329
forall tx.
330330
IsChainState tx =>
331331
(ClientInput tx -> IO ()) ->
332+
ApiTransactionTimeout ->
333+
TChan (Either (TimedServerOutput tx) (ClientMessage tx)) ->
332334
Text ->
333335
LBS.ByteString ->
334336
IO Response
335-
handleRecoverCommitUtxo putClientInput recoverPath _body = do
337+
handleRecoverCommitUtxo putClientInput apiTransactionTimeout responseChannel recoverPath _body = do
336338
case parseTxIdFromPath recoverPath of
337339
Left err -> pure err
338340
Right recoverTxId -> do
341+
dupChannel <- atomically $ dupTChan responseChannel
339342
putClientInput Recover{recoverTxId}
340-
pure $ responseLBS status200 jsonContent (Aeson.encode $ Aeson.String "OK")
343+
let wait = do
344+
event <- atomically $ readTChan dupChannel
345+
case event of
346+
Left TimedServerOutput{output = CommitRecovered{}} ->
347+
pure $ responseLBS status200 jsonContent (Aeson.encode $ Aeson.String "OK")
348+
Right (CommandFailed{clientInput = Recover{}}) ->
349+
pure $ responseLBS status400 jsonContent (Aeson.encode $ Aeson.String "Recover failed")
350+
_ -> wait
351+
timeout (realToFrac (apiTransactionTimeoutNominalDiffTime apiTransactionTimeout)) wait >>= \case
352+
Just r -> pure r
353+
Nothing ->
354+
pure $
355+
responseLBS
356+
status202
357+
jsonContent
358+
( Aeson.encode $
359+
object
360+
[ "tag" .= Aeson.String "RecoverSubmitted"
361+
, "timeout" .= Aeson.String ("Operation timed out after " <> pack (show apiTransactionTimeout) <> " seconds")
362+
]
363+
)
341364
where
342365
parseTxIdFromPath txIdStr =
343366
case Aeson.eitherDecode (encodeUtf8 txIdStr) :: Either String (TxIdType tx) of
@@ -364,29 +387,82 @@ handleSubmitUserTx directChain body = do
364387
where
365388
Chain{submitTx} = directChain
366389

367-
handleDecommit :: forall tx. FromJSON tx => (ClientInput tx -> IO ()) -> LBS.ByteString -> IO Response
368-
handleDecommit putClientInput body =
390+
handleDecommit ::
391+
forall tx.
392+
FromJSON tx =>
393+
(ClientInput tx -> IO ()) ->
394+
ApiTransactionTimeout ->
395+
TChan (Either (TimedServerOutput tx) (ClientMessage tx)) ->
396+
LBS.ByteString ->
397+
IO Response
398+
handleDecommit putClientInput apiTransactionTimeout responseChannel body =
369399
case Aeson.eitherDecode' body :: Either String tx of
370400
Left err ->
371401
pure $ responseLBS status400 jsonContent (Aeson.encode $ Aeson.String $ pack err)
372402
Right decommitTx -> do
403+
dupChannel <- atomically $ dupTChan responseChannel
373404
putClientInput Decommit{decommitTx}
374-
pure $ responseLBS status200 jsonContent (Aeson.encode $ Aeson.String "OK")
405+
let wait = do
406+
event <- atomically $ readTChan dupChannel
407+
case event of
408+
Left TimedServerOutput{output = DecommitFinalized{}} ->
409+
pure $ responseLBS status200 jsonContent (Aeson.encode $ Aeson.String "OK")
410+
Left TimedServerOutput{output = DecommitInvalid{}} ->
411+
pure $ responseLBS status400 jsonContent (Aeson.encode $ Aeson.String "Decommit invalid")
412+
Right (CommandFailed{clientInput = Decommit{}}) ->
413+
pure $ responseLBS status400 jsonContent (Aeson.encode $ Aeson.String "Decommit failed")
414+
_ -> wait
415+
timeout (realToFrac (apiTransactionTimeoutNominalDiffTime apiTransactionTimeout)) wait >>= \case
416+
Just r -> pure r
417+
Nothing ->
418+
pure $
419+
responseLBS
420+
status202
421+
jsonContent
422+
( Aeson.encode $
423+
object
424+
[ "tag" .= Aeson.String "DecommitSubmitted"
425+
, "timeout" .= Aeson.String ("Operation timed out after " <> pack (show apiTransactionTimeout) <> " seconds")
426+
]
427+
)
375428

376429
-- | Handle request to side load confirmed snapshot.
377430
handleSideLoadSnapshot ::
378431
forall tx.
379432
IsChainState tx =>
380433
(ClientInput tx -> IO ()) ->
434+
ApiTransactionTimeout ->
435+
TChan (Either (TimedServerOutput tx) (ClientMessage tx)) ->
381436
LBS.ByteString ->
382437
IO Response
383-
handleSideLoadSnapshot putClientInput body = do
438+
handleSideLoadSnapshot putClientInput apiTransactionTimeout responseChannel body = do
384439
case Aeson.eitherDecode' body :: Either String (SideLoadSnapshotRequest tx) of
385440
Left err ->
386441
pure $ responseLBS status400 jsonContent (Aeson.encode $ Aeson.String $ pack err)
387442
Right SideLoadSnapshotRequest{snapshot} -> do
443+
dupChannel <- atomically $ dupTChan responseChannel
388444
putClientInput $ SideLoadSnapshot snapshot
389-
pure $ responseLBS status200 jsonContent (Aeson.encode $ Aeson.String "OK")
445+
let wait = do
446+
event <- atomically $ readTChan dupChannel
447+
case event of
448+
Left TimedServerOutput{output = SnapshotSideLoaded{}} ->
449+
pure $ responseLBS status200 jsonContent (Aeson.encode $ Aeson.String "OK")
450+
Right (CommandFailed{clientInput = SideLoadSnapshot{}}) ->
451+
pure $ responseLBS status400 jsonContent (Aeson.encode $ Aeson.String "Side-load snapshot failed")
452+
_ -> wait
453+
timeout (realToFrac (apiTransactionTimeoutNominalDiffTime apiTransactionTimeout)) wait >>= \case
454+
Just r -> pure r
455+
Nothing ->
456+
pure $
457+
responseLBS
458+
status202
459+
jsonContent
460+
( Aeson.encode $
461+
object
462+
[ "tag" .= Aeson.String "SideLoadSnapshotSubmitted"
463+
, "timeout" .= Aeson.String ("Operation timed out after " <> pack (show apiTransactionTimeout) <> " seconds")
464+
]
465+
)
390466

391467
-- | Handle request to submit a transaction to the head.
392468
handleSubmitL2Tx ::

0 commit comments

Comments
 (0)