Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit 112e345

Browse files
committed
[CBR-243] improve wallet worker start-up and exception handling
1 parent 9644c97 commit 112e345

File tree

5 files changed

+96
-62
lines changed

5 files changed

+96
-62
lines changed

pkgs/default.nix

+3
Original file line numberDiff line numberDiff line change
@@ -17760,6 +17760,7 @@ license = stdenv.lib.licenses.mit;
1776017760
, lens
1776117761
, log-warper
1776217762
, memory
17763+
, mmorph
1776317764
, mtl
1776417765
, mwc-random
1776517766
, neat-interpolation
@@ -17865,6 +17866,7 @@ json-sop
1786517866
lens
1786617867
log-warper
1786717868
memory
17869+
mmorph
1786817870
mtl
1786917871
mwc-random
1787017872
neat-interpolation
@@ -17887,6 +17889,7 @@ servant-swagger-ui-core
1788717889
servant-swagger-ui-redoc
1788817890
sqlite-simple
1788917891
sqlite-simple-errors
17892+
stm
1789017893
swagger2
1789117894
text
1789217895
time

wallet-new/cardano-sl-wallet-new.cabal

+2
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ library
173173
, lens
174174
, log-warper
175175
, memory
176+
, mmorph
176177
, mtl
177178
, mwc-random
178179
, neat-interpolation
@@ -196,6 +197,7 @@ library
196197
, servant-swagger-ui-redoc
197198
, sqlite-simple
198199
, sqlite-simple-errors
200+
, stm
199201
, swagger2
200202
, text
201203
, time

wallet-new/src/Cardano/Wallet/Kernel/Actions.hs

+72-30
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
module Cardano.Wallet.Kernel.Actions
33
( WalletAction(..)
44
, WalletActionInterp(..)
5-
, forkWalletWorker
6-
, walletWorker
5+
, withWalletWorker
6+
, Err_WalletWorkerExpired(..)
77
, interp
88
, interpList
99
, WalletWorkerState
@@ -12,8 +12,10 @@ module Cardano.Wallet.Kernel.Actions
1212
, isValidState
1313
) where
1414

15-
import Control.Concurrent.Async (async, link)
16-
import Control.Concurrent.Chan
15+
import qualified Control.Concurrent.Async as Async
16+
import qualified Control.Concurrent.STM as STM
17+
import qualified Control.Exception.Safe as Ex
18+
import Control.Monad.Morph (MFunctor(hoist))
1719
import Control.Lens (makeLenses, (%=), (+=), (-=), (.=))
1820
import Formatting (bprint, build, shown, (%))
1921
import qualified Formatting.Buildable
@@ -55,13 +57,11 @@ data WalletWorkerState b = WalletWorkerState
5557

5658
makeLenses ''WalletWorkerState
5759

58-
-- A helper function for lifting a `WalletActionInterp` through a monad transformer.
59-
lifted :: (Monad m, MonadTrans t) => WalletActionInterp m b -> WalletActionInterp (t m) b
60-
lifted i = WalletActionInterp
61-
{ applyBlocks = lift . applyBlocks i
62-
, switchToFork = \n bs -> lift (switchToFork i n bs)
63-
, emit = lift . emit i
64-
}
60+
instance MFunctor WalletActionInterp where
61+
hoist nat i = WalletActionInterp
62+
{ applyBlocks = fmap nat (applyBlocks i)
63+
, switchToFork = fmap (fmap nat) (switchToFork i)
64+
, emit = fmap nat (emit i) }
6565

6666
-- | `interp` is the main interpreter for converting a wallet action to a concrete
6767
-- transition on the wallet worker's state, perhaps combined with some effects on
@@ -121,20 +121,24 @@ interp walletInterp action = do
121121
Shutdown -> error "walletWorker: unreacheable dead code, reached!"
122122

123123
where
124-
WalletActionInterp{..} = lifted walletInterp
124+
WalletActionInterp{..} = hoist lift walletInterp
125125
prependNewestFirst bs = \nf -> NewestFirst (getNewestFirst bs <> getNewestFirst nf)
126126

127-
-- | Connect a wallet action interpreter to a channel of actions.
128-
walletWorker :: forall b. Chan (WalletAction b) -> WalletActionInterp IO b -> IO ()
129-
walletWorker chan ops = do
130-
emit ops "Starting wallet worker."
131-
void $ (`evalStateT` initialWorkerState) tick
132-
emit ops "Finishing wallet worker."
133-
where
134-
tick :: StateT (WalletWorkerState b) IO ()
135-
tick = lift (readChan chan) >>= \case
136-
Shutdown -> return ()
137-
msg -> interp ops msg >> tick
127+
-- | Connect a wallet action interpreter to a source actions. This function
128+
-- returns as soon as the given action returns 'Shutdown'.
129+
walletWorker
130+
:: Ex.MonadMask m
131+
=> WalletActionInterp m b
132+
-> m (WalletAction b)
133+
-> m ()
134+
walletWorker wai getWA = Ex.bracket_
135+
(emit wai "Starting wallet worker.")
136+
(evalStateT
137+
(fix $ \next -> lift getWA >>= \case
138+
Shutdown -> pure ()
139+
wa -> interp wai wa >> next)
140+
initialWorkerState)
141+
(emit wai "Stoping wallet worker.")
138142

139143
-- | Connect a wallet action interpreter to a stream of actions.
140144
interpList :: Monad m => WalletActionInterp m b -> [WalletAction b] -> m (WalletWorkerState b)
@@ -147,13 +151,51 @@ initialWorkerState = WalletWorkerState
147151
, _lengthPendingBlocks = 0
148152
}
149153

150-
-- | Start up a wallet worker; the worker will respond to actions issued over the
151-
-- returned channel.
152-
forkWalletWorker :: WalletActionInterp IO b -> IO (WalletAction b -> IO ())
153-
forkWalletWorker ops = do
154-
c <- newChan
155-
link =<< async (walletWorker c ops)
156-
return (writeChan c)
154+
-- | Thrown by 'withWalletWorker''s continuation in case it's used outside of
155+
-- its intended scope.
156+
data Err_WalletWorkerExpired = Err_WalletWorkerExpired deriving (Show)
157+
instance Ex.Exception Err_WalletWorkerExpired
158+
159+
-- | Start a wallet worker in backround who will react to input provided via the
160+
-- 'STM' function, in FIFO order.
161+
--
162+
-- After the given continuation returns (successfully or due to some exception),
163+
-- the worker will continue processing any pending input before returning,
164+
-- re-throwing the continuation's exception if any. Async exceptions from any
165+
-- source will always be prioritized.
166+
--
167+
-- Usage of the obtained 'STM' action after the given continuation has returned
168+
-- is not possible. It will throw 'Err_WalletWorkerExpired'.
169+
withWalletWorker
170+
:: (MonadIO m, Ex.MonadMask m)
171+
=> WalletActionInterp IO a
172+
-> ((WalletAction a -> STM ()) -> m b)
173+
-> m b
174+
withWalletWorker wai k = do
175+
-- 'tqWA' keeps items to be processed by the worker in FIFO order.
176+
tqWA :: STM.TQueue (WalletAction a) <- liftIO STM.newTQueueIO
177+
-- 'tvOpen' is 'True' as long as 'tqWA' can receive new input.
178+
tvOpen :: STM.TVar Bool <- liftIO (STM.newTVarIO True)
179+
-- 'getWA' returns the next action to be processed. This function retries
180+
-- unless 'tvOpen' is 'False', in which case 'Shutdown' is returned.
181+
let getWA :: STM (WalletAction a)
182+
getWA = STM.tryReadTQueue tqWA >>= \case
183+
Just wa -> pure wa
184+
Nothing -> STM.readTVar tvOpen >>= \case
185+
False -> pure Shutdown
186+
True -> STM.retry
187+
Ex.bracket
188+
(liftIO (Async.async (walletWorker wai (STM.atomically getWA))))
189+
(\a1 -> liftIO $ do
190+
-- Prevent new input.
191+
STM.atomically (STM.writeTVar tvOpen False)
192+
-- Wait for the worker to finish, re-throwing any exceptions from it.
193+
Async.wait a1)
194+
(\_ -> k $ \wa -> do
195+
-- Add a WalletAction to the queue, unless it's been closed already.
196+
STM.readTVar tvOpen >>= \case
197+
True -> STM.writeTQueue tqWA wa
198+
False -> Ex.throwM Err_WalletWorkerExpired)
157199

158200
-- | Check if this is the initial worker state.
159201
isInitialState :: Eq b => WalletWorkerState b -> Bool

wallet-new/src/Cardano/Wallet/Kernel/Wallets.hs

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ instance Exception CreateWalletError
5959
-------------------------------------------------------------------------------}
6060

6161
-- | Creates a new HD 'Wallet'.
62-
createHdWallet :: PassiveWallet
62+
createHdWallet
63+
:: PassiveWallet
6364
-> Mnemonic nat
6465
-- ^ The set of words (i.e the mnemonic) to generate the initial seed.
6566
-- See <https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#From_mnemonic_to_seed>

wallet-new/src/Cardano/Wallet/WalletLayer/Kernel.hs

+17-31
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ module Cardano.Wallet.WalletLayer.Kernel
88

99
import Universum
1010

11+
import qualified Control.Concurrent.STM as STM
1112
import Control.Lens (to)
1213
import Data.Coerce (coerce)
13-
import Data.Default (def)
1414
import Data.Maybe (fromJust)
1515
import Data.Time.Units (Second)
1616
import Formatting (build, sformat)
@@ -41,7 +41,6 @@ import Cardano.Wallet.Kernel.CoinSelection.FromGeneric
4141
(CoinSelectionOptions (..), ExpenseRegulation,
4242
InputGrouping, newOptions)
4343

44-
import qualified Cardano.Wallet.Kernel.BIP39 as BIP39
4544
import Pos.Core (Address, Coin, decodeTextAddress, mkCoin)
4645
import qualified Pos.Core as Core
4746
import Pos.Core.Chrono (OldestFirst (..))
@@ -66,38 +65,25 @@ bracketPassiveWallet
6665
-> (PassiveWalletLayer n -> Kernel.PassiveWallet -> m a) -> m a
6766
bracketPassiveWallet logFunction keystore rocksDB f =
6867
Kernel.bracketPassiveWallet logFunction keystore rocksDB $ \w -> do
69-
70-
-- Create the wallet worker and its communication endpoint `invoke`.
71-
bracket (liftIO $ Actions.forkWalletWorker $ Actions.WalletActionInterp
72-
{ Actions.applyBlocks = \blunds ->
73-
Kernel.applyBlocks w $
74-
OldestFirst (mapMaybe blundToResolvedBlock (toList (getOldestFirst blunds)))
75-
, Actions.switchToFork = \_ _ -> logFunction Debug "<switchToFork>"
76-
, Actions.emit = logFunction Debug
77-
}
78-
) (\invoke -> liftIO (invoke Actions.Shutdown))
79-
$ \invoke -> do
80-
-- TODO (temporary): build a sample wallet from a backup phrase
81-
_ <- liftIO $ do
82-
Kernel.createHdWallet w
83-
(def @(BIP39.Mnemonic 12))
84-
emptyPassphrase
85-
assuranceLevel
86-
walletName
87-
88-
f (passiveWalletLayer w invoke) w
89-
68+
let wai = Actions.WalletActionInterp
69+
{ Actions.applyBlocks = \blunds ->
70+
Kernel.applyBlocks w
71+
(OldestFirst (mapMaybe blundToResolvedBlock
72+
(toList (getOldestFirst blunds))))
73+
, Actions.switchToFork = \_ _ ->
74+
logFunction Debug "<switchToFork>"
75+
, Actions.emit = logFunction Debug }
76+
Actions.withWalletWorker wai $ \invoke -> do
77+
f (passiveWalletLayer w invoke) w
9078
where
91-
-- TODO consider defaults
92-
walletName = HD.WalletName "(new wallet)"
93-
assuranceLevel = HD.AssuranceLevelNormal
94-
9579
-- | TODO(ks): Currently not implemented!
9680
passiveWalletLayer :: Kernel.PassiveWallet
97-
-> (Actions.WalletAction Blund -> IO ())
81+
-> (Actions.WalletAction Blund -> STM ())
9882
-> PassiveWalletLayer n
9983
passiveWalletLayer wallet invoke =
100-
PassiveWalletLayer
84+
let invokeIO :: forall m'. MonadIO m' => Actions.WalletAction Blund -> m' ()
85+
invokeIO = liftIO . STM.atomically . invoke
86+
in PassiveWalletLayer
10187
{ _pwlCreateWallet =
10288
\(V1.NewWallet (V1.BackupPhrase mnemonic) mbSpendingPassword v1AssuranceLevel v1WalletName operation) -> do
10389
liftIO $ limitExecutionTimeTo (30 :: Second) CreateWalletTimeLimitReached $ do
@@ -166,8 +152,8 @@ bracketPassiveWallet logFunction keystore rocksDB f =
166152
Left err -> return (Left $ CreateAddressError err)
167153
, _pwlGetAddresses = error "Not implemented!"
168154

169-
, _pwlApplyBlocks = liftIO . invoke . Actions.ApplyBlocks
170-
, _pwlRollbackBlocks = liftIO . invoke . Actions.RollbackBlocks
155+
, _pwlApplyBlocks = invokeIO . Actions.ApplyBlocks
156+
, _pwlRollbackBlocks = invokeIO . Actions.RollbackBlocks
171157
}
172158

173159
-- The use of the unsafe constructor 'UnsafeRawResolvedBlock' is justified

0 commit comments

Comments
 (0)