git.iko.soy/distributed-process
on master help sign in
Remove Mx Data Tables. This API isn't used, is easy to replace with various other packages, and contains races that are leading to failing test cases in CI.
f28ea4bTim Watson
distributed-process-tests/src/Control/Distributed/Process/Tests/Mx.hs+1−82
@@ -22,11 +22,6 @@ import Control.Distributed.Process.Management
22 , mxNotify
22 , mxNotify
23 , mxBroadcast
23 , mxBroadcast
24 , mxGetId
24 , mxGetId
25− , mxGet
26− , mxSet
27− , mxClear
28− , mxPurgeTable
29− , mxDropTable
30 )
25 )
31import Control.Monad (void)
26import Control.Monad (void)
32import Data.Binary
27import Data.Binary
@@ -180,61 +175,6 @@ testAgentEventHandling result = do
180
175
181 stash result $ seenAlive && seenDead
176 stash result $ seenAlive && seenDead
182
177
183−testAgentPublication :: TestResult (Maybe Publish) -> Process ()
184−testAgentPublication result = do
185− (syncChan, rp) <- newChan
186− let ourId = MxAgentId "publication-agent"
187− agentPid <- mxAgent ourId () [
188− mxSink $ \() -> do
189− selfId <- mxGetId
190− liftMX $ mxSet selfId "publish" Publish >> sendChan syncChan ()
191− mxReady
192− ]
193−
194− mxNotify ()
195− () <- receiveChan rp
196−
197− stash result =<< mxGet ourId "publish"
198−
199− mref <- monitor agentPid
200− kill agentPid "finished"
201− receiveWait [
202− matchIf (\(ProcessMonitorNotification ref _ _) -> ref == mref)
203− ((\_ -> return ()))
204− ]
205−
206−testAgentTableClear :: Int -> TestResult (Maybe Int, Maybe Int) -> Process ()
207−testAgentTableClear val result =
208− let tId = (MxAgentId "agent-1")
209− tKey = "key-1" in do
210− mxSet tId tKey val
211− get1 <- mxGet tId tKey
212− mxClear tId tKey
213− get2 <- mxGet tId tKey
214− stash result (get1, get2)
215−
216−testAgentTablePurge :: TestResult (Maybe Int) -> Process ()
217−testAgentTablePurge result =
218− let tId = MxAgentId "agent-2"
219− tKey = "key-2" in do
220− mxSet tId tKey (12345 :: Int)
221− mxPurgeTable tId
222− stash result =<< mxGet tId tKey
223−
224−testAgentTableDelete :: Int
225− -> TestResult (Maybe Int, Maybe Int, Maybe Int)
226− -> Process ()
227−testAgentTableDelete val result =
228− let tId = (MxAgentId "agent-3")
229− tKey = "key-3" in do
230− mxSet tId tKey val
231− get1 <- mxGet tId tKey
232− mxDropTable tId
233− get2 <- mxGet tId tKey
234− mxSet tId tKey val
235− get3 <- mxGet tId tKey
236− stash result (get1, get2, get3)
237−
238tests :: TestTransport -> IO [Test]
178tests :: TestTransport -> IO [Test]
239tests TestTransport{..} = do
179tests TestTransport{..} = do
240 node1 <- newLocalNode testTransport initRemoteTable
180 node1 <- newLocalNode testTransport initRemoteTable
@@ -262,25 +202,4 @@ tests TestTransport{..} = do
262 node1 (sort ["first", "second",
202 node1 (sort ["first", "second",
263 "third", "fourth",
203 "third", "fourth",
264 "fifth"]) testAgentPrioritisation)
204 "fifth"]) testAgentPrioritisation)
265− ],
205+ ]]
266− testGroup "Mx Global Properties" [
267− testCase "Global Property Publication"
268− (delayedAssertion
269− "expected (Just Publish), but no table entry was found"
270− node1 (Just Publish) testAgentPublication)
271− , testCase "Clearing Global Properties"
272− (delayedAssertion
273− "expected (Just 1024, Nothing): invalid table entry found!"
274− node1 (Just 1024, Nothing) (testAgentTableClear 1024))
275− , testCase "Purging Global Tables"
276− (delayedAssertion
277− "expected Nothing, but a table entry was found"
278− node1 Nothing testAgentTablePurge)
279− , testCase "Deleting and (Re)Creating Global Tables"
280− (delayedAssertion
281− "expected (Just 15, Nothing, Just 15): invalid table entry found!"
282− node1 (Just 15, Nothing, Just 15) (testAgentTableDelete 15))
283− -- Wait for other tests to finish.
284− , testCase "Wait" $
285− threadDelay 100000
286− ]]
distributed-process.cabal−1
@@ -75,7 +75,6 @@ Library
75 Control.Distributed.Process.UnsafePrimitives
75 Control.Distributed.Process.UnsafePrimitives
76 Control.Distributed.Process.Management.Internal.Agent,
76 Control.Distributed.Process.Management.Internal.Agent,
77 Control.Distributed.Process.Management.Internal.Bus,
77 Control.Distributed.Process.Management.Internal.Bus,
78− Control.Distributed.Process.Management.Internal.Table,
79 Control.Distributed.Process.Management.Internal.Types,
78 Control.Distributed.Process.Management.Internal.Types,
80 Control.Distributed.Process.Management.Internal.Trace.Primitives,
79 Control.Distributed.Process.Management.Internal.Trace.Primitives,
81 Control.Distributed.Process.Management.Internal.Trace.Remote,
80 Control.Distributed.Process.Management.Internal.Trace.Remote,
src/Control/Distributed/Process/Management.hs+4−79
@@ -83,28 +83,8 @@
83-- system information. Agents maintain their own internal state privately (via a
83-- system information. Agents maintain their own internal state privately (via a
84-- state transformer - see 'mxGetLocal' et al), however it is possible for
84-- state transformer - see 'mxGetLocal' et al), however it is possible for
85-- agents to share additional data with each other (and the outside world)
85-- agents to share additional data with each other (and the outside world)
86−-- using /data tables/.
86+-- using whatever mechanism the user wishes, e.g., acidstate, or shared memory
87−--
87+-- primitives.
88−-- Each agent is assigned its own data table, which acts as a shared map, where
89−-- the keys are @String@s and the values are @Serializable@ datum of whatever
90−-- type the agent or its clients stores.
91−--
92−-- Because an agent's /data table/ stores its values in raw 'Message' format,
93−-- it works effectively as an /un-typed dictionary/, into which data of varying
94−-- types can be fed and later retrieved. The upside of this is that different
95−-- keys can be mapped to various types without any additional work on the part
96−-- of the developer. The downside is that the code reading these values must
97−-- know in advance what type(s) to expect, and the API provides no additional
98−-- support for handling that.
99−--
100−-- Publishing is accomplished using the 'mxPublish' and 'mxSet' APIs, whilst
101−-- querying and deletion are handled by 'mxGet', 'mxClear', 'mxPurgeTable' and
102−-- 'mxDropTable' respectively.
103−--
104−-- When a management agent terminates, their tables are left in memory despite
105−-- termination, such that an agent may resume its role (by restarting) or have
106−-- its 'MxAgentId' taken over by another subsequent agent, leaving the data
107−-- originally captured in place.
108--
88--
109-- [Defining Agents]
89-- [Defining Agents]
110--
90--
@@ -194,10 +174,6 @@
194-- the event bus /and/ their own mailboxes, plus searching through the set of
174-- the event bus /and/ their own mailboxes, plus searching through the set of
195-- event sinks (for each agent) to determine the right handler for the event.
175-- event sinks (for each agent) to determine the right handler for the event.
196--
176--
197−-- Each management agent requires not only its own @Process@ (in which the agent
198−-- code is run), but also a peer process that provides its /data table/. These
199−-- data tables also have to be coordinated and manaaged on each agent's behalf.
200−--
201-- [Architecture Overview]
177-- [Architecture Overview]
202--
178--
203-- The architecture of the management event bus is internal and subject to
179-- The architecture of the management event bus is internal and subject to
@@ -264,13 +240,6 @@ module Control.Distributed.Process.Management
264 , mxGetLocal
240 , mxGetLocal
265 , mxUpdateLocal
241 , mxUpdateLocal
266 , liftMX
242 , liftMX
267− -- * Mx Data API
268− , mxPublish
269− , mxSet
270− , mxGet
271− , mxClear
272− , mxPurgeTable
273− , mxDropTable
274 ) where
243 ) where
275
244
276import Control.Applicative ((<$>))
245import Control.Applicative ((<$>))
@@ -281,10 +250,7 @@ import Control.Concurrent.STM.TChan
281 , TChan
250 , TChan
282 )
251 )
283import Control.Distributed.Process.Internal.Primitives
252import Control.Distributed.Process.Internal.Primitives
284− ( newChan
285− , nsend
286− , receiveWait
253+ ( receiveWait
287− , matchChan
288 , matchAny
254 , matchAny
289 , matchSTM
255 , matchSTM
290 , unwrapMessage
256 , unwrapMessage
@@ -303,14 +269,12 @@ import Control.Distributed.Process.Internal.Types
303 , unsafeCreateUnencodedMessage
269 , unsafeCreateUnencodedMessage
304 )
270 )
305import Control.Distributed.Process.Management.Internal.Bus (publishEvent)
271import Control.Distributed.Process.Management.Internal.Bus (publishEvent)
306−import qualified Control.Distributed.Process.Management.Internal.Table as Table
307import Control.Distributed.Process.Management.Internal.Types
272import Control.Distributed.Process.Management.Internal.Types
308 ( MxAgentId(..)
273 ( MxAgentId(..)
309 , MxAgent(..)
274 , MxAgent(..)
310 , MxAction(..)
275 , MxAction(..)
311 , ChannelSelector(..)
276 , ChannelSelector(..)
312 , MxAgentState(..)
277 , MxAgentState(..)
313− , MxAgentStart(..)
314 , MxSink
278 , MxSink
315 , MxEvent(..)
279 , MxEvent(..)
316 )
280 )
@@ -334,42 +298,6 @@ mxNotify msg = do
334 bus <- localEventBus . processNode <$> ask
298 bus <- localEventBus . processNode <$> ask
335 liftIO $ publishEvent bus $ unsafeCreateUnencodedMessage msg
299 liftIO $ publishEvent bus $ unsafeCreateUnencodedMessage msg
336
300
337−-- | Publish an arbitrary @Message@ as a property in the management database.
338−--
339−-- For publishing @Serializable@ data, use 'mxSet' instead.
340−--
341−mxPublish :: MxAgentId -> String -> Message -> Process ()
342−mxPublish a k v = Table.set k v (Table.MxForAgent a)
343−
344−-- | Sets an arbitrary @Serializable@ datum against a key in the management
345−-- database. Note that /no attempt is made to force the argument/, therefore
346−-- it is very important that you do not pass unevaluated thunks that might
347−-- crash some other, arbitrary process (or management agent!) that obtains
348−-- and attempts to force the value later on.
349−--
350−mxSet :: Serializable a => MxAgentId -> String -> a -> Process ()
351−mxSet mxId key msg = do
352− Table.set key (unsafeCreateUnencodedMessage msg) (Table.MxForAgent mxId)
353−
354−-- | Fetches a property from the management database for the given key.
355−-- If the property is not set, or does not match the expected type when
356−-- typechecked (at runtime), returns @Nothing@.
357−mxGet :: Serializable a => MxAgentId -> String -> Process (Maybe a)
358−mxGet = Table.fetch . Table.MxForAgent
359−
360−-- | Clears a property from the management database using the given key.
361−-- If the key does not exist in the database, this is a noop.
362−mxClear :: MxAgentId -> String -> Process ()
363−mxClear mxId key = Table.clear key (Table.MxForAgent mxId)
364−
365−-- | Purges a table in the management database of all its stored properties.
366−mxPurgeTable :: MxAgentId -> Process ()
367−mxPurgeTable = Table.purge . Table.MxForAgent
368−
369−-- | Deletes a table from the management database.
370−mxDropTable :: MxAgentId -> Process ()
371−mxDropTable = Table.delete . Table.MxForAgent
372−
373--------------------------------------------------------------------------------
301--------------------------------------------------------------------------------
374-- API for writing user defined management extensions (i.e., agents) --
302-- API for writing user defined management extensions (i.e., agents) --
375--------------------------------------------------------------------------------
303--------------------------------------------------------------------------------
@@ -489,10 +417,7 @@ mxAgentWithFinalize mxId initState handlers dtor = do
489 return pid
417 return pid
490 where
418 where
491 start (sendTChan, recvTChan) = do
419 start (sendTChan, recvTChan) = do
492− (sp, rp) <- newChan
493− nsend Table.mxTableCoordinator (MxAgentStart sp mxId)
494− tablePid <- receiveWait [ matchChan rp (\(p :: ProcessId) -> return p) ]
495− let nState = MxAgentState mxId sendTChan tablePid initState
420+ let nState = MxAgentState mxId sendTChan initState
496 runAgent dtor handlers InputChan recvTChan nState
421 runAgent dtor handlers InputChan recvTChan nState
497
422
498 runAgent :: MxAgent s ()
423 runAgent :: MxAgent s ()
src/Control/Distributed/Process/Management/Internal/Table.hsdeleted−222
@@ -1,222 +0,0 @@
1−{-# LANGUAGE DeriveDataTypeable #-}
2−{-# LANGUAGE ScopedTypeVariables #-}
3−{-# LANGUAGE RankNTypes #-}
4−{-# LANGUAGE DeriveGeneric #-}
5−{-# LANGUAGE RecordWildCards #-}
6−
7−module Control.Distributed.Process.Management.Internal.Table
8− ( MxTableRequest(..)
9− , MxTableId(..)
10− , mxTableCoordinator
11− , startTableCoordinator
12− , delete
13− , purge
14− , clear
15− , set
16− , get
17− , fetch
18− ) where
19−
20−import Control.Distributed.Process.Internal.Primitives
21− ( receiveWait
22− , receiveChan
23− , match
24− , matchAny
25− , matchIf
26− , matchChan
27− , send
28− , nsend
29− , sendChan
30− , getSelfPid
31− , link
32− , monitor
33− , unwrapMessage
34− , newChan
35− , withMonitor
36− )
37−import Control.Distributed.Process.Internal.Types
38− ( Process
39− , ProcessId
40− , ProcessMonitorNotification(..)
41− , SendPort
42− , ReceivePort
43− , Message
44− , unsafeCreateUnencodedMessage
45− )
46−import Control.Distributed.Process.Management.Internal.Types
47− ( MxTableId(..)
48− , MxAgentId(..)
49− , MxAgentStart(..)
50− , Fork)
51−import Control.Distributed.Process.Serializable (Serializable)
52−import Control.Monad.IO.Class (liftIO)
53−import Data.Accessor (Accessor, accessor, (^=), (^:))
54−import Data.Binary (Binary)
55−import Data.Map (Map)
56−import qualified Data.Map as Map
57−import Data.Typeable (Typeable)
58−
59−import GHC.Generics
60−
61−-- An extremely lightweight shared Map implementation, for use
62−-- by /management agents/ and their cohorts. Each agent is assigned
63−-- a table, into which any serializable @Message@ can be inserted.
64−-- Data are inserted, removed and searched for via their key, which
65−-- is a string. Tables can be purged, values can be set, fetched or
66−-- cleared/removed.
67−--
68−
69−data MxTableRequest =
70− Delete
71− | Purge
72− | Clear !String
73− | Set !String !Message
74− | Get !String !(SendPort (Maybe Message)) -- see [note: un-typed send port]
75− deriving (Typeable, Generic)
76−instance Binary MxTableRequest where
77−
78−data MxTableState = MxTableState { _name :: !String
79− , _entries :: !(Map String Message)
80− }
81−
82−type MxTables = Map MxAgentId ProcessId
83−
84−mxTableCoordinator :: String
85−mxTableCoordinator = "mx.table.coordinator"
86−
87−delete :: MxTableId -> Process ()
88−delete = sendReq Delete
89−
90−purge :: MxTableId -> Process ()
91−purge = sendReq Purge
92−
93−clear :: String -> MxTableId -> Process ()
94−clear k = sendReq (Clear k)
95−
96−set :: String -> Message -> MxTableId -> Process ()
97−set k v = sendReq (Set k v)
98−
99−fetch :: forall a. (Serializable a)
100− => MxTableId
101− -> String
102− -> Process (Maybe a)
103−fetch (MxForPid pid) key = get pid key
104−fetch mxId@(MxForAgent _) key = do
105− (sp, rp) <- newChan :: Process (SendPort (Maybe Message),
106− ReceivePort (Maybe Message))
107− sendReq (Get key sp) mxId
108− receiveChan rp >>= maybe (return Nothing)
109− (unwrapMessage :: Message -> Process (Maybe a))
110−
111−-- [note: un-typed send port]
112−-- Here, fetch uses a typed channel over a raw Message to obtain
113−-- its result, so type checking is deferred until receipt and will
114−-- be handled in the caller's thread. This is necessary because
115−-- the server portion of the code knows nothing about the types
116−-- involved, nor should it, since these tables can be used to
117−-- store arbitrary serializable data.
118−
119−get :: forall a. (Serializable a)
120− => ProcessId
121− -> String
122− -> Process (Maybe a)
123−get pid key = do
124− safeFetch pid key >>= maybe (return Nothing)
125− (unwrapMessage :: Message -> Process (Maybe a))
126−
127−safeFetch :: ProcessId -> String -> Process (Maybe Message)
128−safeFetch pid key = do
129− (sp, rp) <- newChan
130− send pid $ Get key sp
131− withMonitor pid $ do
132− receiveWait [
133− matchChan rp return
134− , matchIf (\(ProcessMonitorNotification _ pid' _) -> pid' == pid)
135− (\_ -> return $ Just (unsafeCreateUnencodedMessage ()))
136− ]
137−
138−sendReq :: MxTableRequest -> MxTableId -> Process ()
139−sendReq req tid = (resolve tid) req
140−
141−resolve :: Serializable a => MxTableId -> (a -> Process ())
142−resolve (MxForAgent agent) = \msg -> nsend mxTableCoordinator (agent, msg)
143−resolve (MxForPid pid) = \msg -> send pid msg
144−
145−startTableCoordinator :: Fork -> Process ()
146−startTableCoordinator fork = run Map.empty
147− where
148− run :: MxTables -> Process ()
149− run tables =
150− receiveWait [
151− -- note that this state change can race with MxAgentStart requests
152− match (\(ProcessMonitorNotification _ pid _) -> do
153− return $ Map.filter (/= pid) tables)
154− , match (\(MxAgentStart ch agent) -> do
155− lookupAgent tables agent >>= \(p, t) -> do
156− sendChan ch p >> return t)
157− , match (\req@(agent, tReq :: MxTableRequest) -> do
158− case tReq of
159− Get k sp -> do
160− lookupAgent tables agent >>= \(p, t) -> do
161− safeFetch p k >>= sendChan sp >> return t
162− _ -> do
163− handleRequest tables req)
164− , matchAny (\_ -> return tables) -- unrecognised messages are dropped
165− ] >>= run
166−
167− handleRequest :: MxTables
168− -> (MxAgentId, MxTableRequest)
169− -> Process MxTables
170− handleRequest tables' (agent, req) = do
171− lookupAgent tables' agent >>= \(p, t) -> send p req >> return t
172−
173− lookupAgent :: MxTables -> MxAgentId -> Process (ProcessId, MxTables)
174− lookupAgent tables' agentId' = do
175− case Map.lookup agentId' tables' of
176− Nothing -> launchNew agentId' tables'
177− Just p -> return (p, tables')
178−
179− launchNew :: MxAgentId
180− -> MxTables
181− -> Process (ProcessId, MxTables)
182− launchNew mxId tblMap = do
183− let initState = MxTableState { _name = (agentId mxId)
184− , _entries = Map.empty
185− }
186− (pid, _) <- spawnSup $ tableHandler initState
187− return $ (pid, mxId `seq` pid `seq` Map.insert mxId pid tblMap)
188−
189− spawnSup proc = do
190− us <- getSelfPid
191− -- we need to use that passed in "fork", in order to
192− -- break an import cycle with Node.hs courtesy of the
193− -- management agent, API and tracing modules
194− them <- liftIO $ fork $ link us >> proc
195− ref <- monitor them
196− return (them, ref)
197−
198−tableHandler :: MxTableState -> Process ()
199−tableHandler state = do
200− ns <- receiveWait [
201− match (handleTableRequest state)
202− , matchAny (\_ -> return (Just state))
203− ]
204− case ns of
205− Nothing -> return ()
206− Just s' -> tableHandler s'
207− where
208− handleTableRequest _ Delete = return Nothing
209− handleTableRequest st Purge = return $ Just $ (entries ^= Map.empty) $ st
210− handleTableRequest st (Clear k) = return $ Just $ (entries ^: (k `seq` Map.delete k)) $ st
211− handleTableRequest st (Set k v) = return $ Just $ (entries ^: (k `seq` v `seq` Map.insert k v)) st
212− handleTableRequest st (Get k c) = getEntry k c st >> return (Just st)
213−
214−getEntry :: String
215− -> SendPort (Maybe Message)
216− -> MxTableState
217− -> Process ()
218−getEntry k m MxTableState{..} = do
219− sendChan m =<< return (Map.lookup k _entries)
220−
221−entries :: Accessor MxTableState (Map String Message)
222−entries = accessor _entries (\ls st -> st { _entries = ls })
src/Control/Distributed/Process/Management/Internal/Types.hs−17
@@ -5,12 +5,10 @@
5{-# LANGUAGE DeriveGeneric #-}
5{-# LANGUAGE DeriveGeneric #-}
6module Control.Distributed.Process.Management.Internal.Types
6module Control.Distributed.Process.Management.Internal.Types
7 ( MxAgentId(..)
7 ( MxAgentId(..)
8− , MxTableId(..)
9 , MxAgentState(..)
8 , MxAgentState(..)
10 , MxAgent(..)
9 , MxAgent(..)
11 , MxAction(..)
10 , MxAction(..)
12 , ChannelSelector(..)
11 , ChannelSelector(..)
13− , MxAgentStart(..)
14 , Fork
12 , Fork
15 , MxSink
13 , MxSink
16 , MxEvent(..)
14 , MxEvent(..)
@@ -100,17 +98,10 @@ type Fork = (Process () -> IO ProcessId)
100newtype MxAgentId = MxAgentId { agentId :: String }
98newtype MxAgentId = MxAgentId { agentId :: String }
101 deriving (Typeable, Binary, Eq, Ord)
99 deriving (Typeable, Binary, Eq, Ord)
102
100
103−data MxTableId =
104− MxForAgent !MxAgentId
105− | MxForPid !ProcessId
106− deriving (Typeable, Generic)
107−instance Binary MxTableId where
108−
109data MxAgentState s = MxAgentState
101data MxAgentState s = MxAgentState
110 {
102 {
111 mxAgentId :: !MxAgentId
103 mxAgentId :: !MxAgentId
112 , mxBus :: !(TChan Message)
104 , mxBus :: !(TChan Message)
113− , mxSharedTable :: !ProcessId
114 , mxLocalState :: !s
105 , mxLocalState :: !s
115 }
106 }
116
107
@@ -128,14 +119,6 @@ newtype MxAgent s a =
128 , Applicative
119 , Applicative
129 )
120 )
130
121
131−data MxAgentStart = MxAgentStart
132− {
133− mxAgentTableChan :: SendPort ProcessId
134− , mxAgentIdStart :: MxAgentId
135− }
136− deriving (Typeable, Generic)
137−instance Binary MxAgentStart where
138−
139data ChannelSelector = InputChan | Mailbox
122data ChannelSelector = InputChan | Mailbox
140
123
141-- | Represents the actions a management agent can take
124-- | Represents the actions a management agent can take
src/Control/Distributed/Process/Node.hs−6
@@ -177,10 +177,6 @@ import Control.Distributed.Process.Internal.Types
177import Control.Distributed.Process.Management.Internal.Agent
177import Control.Distributed.Process.Management.Internal.Agent
178 ( mxAgentController
178 ( mxAgentController
179 )
179 )
180−import qualified Control.Distributed.Process.Management.Internal.Table as Table
181− ( mxTableCoordinator
182− , startTableCoordinator
183− )
184import qualified Control.Distributed.Process.Management.Internal.Trace.Remote as Trace
180import qualified Control.Distributed.Process.Management.Internal.Trace.Remote as Trace
185 ( remoteTable
181 ( remoteTable
186 )
182 )
@@ -304,8 +300,6 @@ startServiceProcesses node = do
304 -- before /that/ process has started - this is a totally harmless race
300 -- before /that/ process has started - this is a totally harmless race
305 -- however, so we deliberably ignore it
301 -- however, so we deliberably ignore it
306 startDefaultTracer node
302 startDefaultTracer node
307− tableCoordinatorPid <- fork $ Table.startTableCoordinator fork
308− runProcess node $ register Table.mxTableCoordinator tableCoordinatorPid
309 logger <- forkProcess node loop
303 logger <- forkProcess node loop
310 runProcess node $ do
304 runProcess node $ do
311 register "logger" logger
305 register "logger" logger