git.iko.soy/distributed-process
on mx-fix help sign in
479 lines 20 kB history raw
1{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE StandaloneDeriving #-}
4{-# LANGUAGE ExistentialQuantification #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE RecordWildCards #-}
7{-# LANGUAGE PatternGuards #-}
8{-# LANGUAGE FlexibleInstances #-}
9{-# LANGUAGE UndecidableInstances #-}
11-----------------------------------------------------------------------------
12-- |
13-- Module : Control.Distributed.Process.Management
14-- Copyright : (c) Well-Typed / Tim Watson
15-- License : BSD3 (see the file LICENSE)
16--
17-- Maintainer : Tim Watson <watson.timothy@gmail.com>
18-- Stability : experimental
19-- Portability : non-portable (requires concurrency)
20--
21-- [Management Extensions API]
22--
23-- This module presents an API for creating /Management Agents/:
24-- special processes that are capable of receiving and responding to
25-- a node's internal system events. These /system events/ are delivered by
26-- the management event bus: An internal subsystem maintained for each
27-- running node, to which all agents are automatically subscribed.
28--
29-- /Agents/ are defined in terms of /event sinks/, taking a particular
30-- @Serializable@ type and evaluating to an action in the 'MxAgent' monad in
31-- response. Each 'MxSink' evaluates to an 'MxAction' that specifies whether
32-- the agent should continue processing it's inputs or stop. If the type of a
33-- message cannot be matched to any of the agent's sinks, it will be discarded.
34-- A sink can also deliberately skip processing a message, deferring to the
35-- remaining handlers. This is the /only/ way that more than one event sink
36-- can handle the same data type, since otherwise the first /type match/ will
37-- /win/ every time a message arrives. See 'mxSkip' for details.
38--
39-- Various events are published to the management event bus automatically,
40-- the full list of which can be found in the definition of the 'MxEvent' data
41-- type. Additionally, clients of the /Management API/ can publish arbitrary
42-- @Serializable@ data to the event bus using 'mxNotify'. All running agents
43-- receive all events (from the primary event bus to which they're subscribed).
44--
45-- Agent processes are automatically registered on the local node, and can
46-- receive messages via their mailbox just like ordinary processes. Unlike
47-- ordinary @Process@ code however, it is unnecessary (though possible) for
48-- agents to use the base @expect@ and @receiveX@ primitives to do this, since
49-- the management infrastructure will continuously read from both the primary
50-- event bus /and/ the process' own mailbox. Messages are transparently passed
51-- to the agent's event sinks from both sources, so an agent need only concern
52-- itself with how to respond to its inputs.
53--
54-- Some agents may wish to prioritise messages from their mailbox over traffic
55-- on the management event bus, or vice versa. The 'mxReceive' and
56-- 'mxReceiveChan' API calls do this for the mailbox and event bus,
57-- respectively. The /prioritisation/ these APIs offer is simply that the chosen
58-- data stream will be checked first. No blocking will occur if the chosen
59-- (prioritised) source is devoid of input messages, instead the agent handling
60-- code will revert to switching between the alternatives in /round-robin/ as
61-- usual. If messages exist in one or more channels, they will be consumed as
62-- soon as they're available, priority is effectively a hint about which
63-- channel to consume from, should messages be available in both.
64--
65-- Prioritisation then, is a /hint/ about the preference of data source from
66-- which the next input should be chosen. No guarantee can be made that the
67-- chosen source will in fact be selected at runtime.
68--
69-- [Management API Semantics]
70--
71-- The management API provides /no guarantees whatsoever/, viz:
72--
73-- * The ordering of messages delivered to the event bus.
74--
75-- * The order in which agents will be executed.
76--
77-- * Whether messages will be taken from the mailbox first, or the event bus.
78--
79-- [Management Data API]
80--
81-- Both management agents and clients of the API have access to a variety of
82-- data storage capabilities, to facilitate publishing and consuming useful
83-- system information. Agents maintain their own internal state privately (via a
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)
86-- using whatever mechanism the user wishes, e.g., acidstate, or shared memory
87-- primitives.
88--
89-- [Defining Agents]
90--
91-- New agents are defined with 'mxAgent' and require a unique 'MxAgentId', an
92-- initial state - 'MxAgent' runs in a state transformer - and a list of the
93-- agent's event sinks. Each 'MxSink' is defined in terms of a specific
94-- @Serializable@ type, via the 'mxSink' function, binding the event handler
95-- expression to inputs of only that type.
96--
97-- Apart from modifying its own local state, an agent can execute arbitrary
98-- @Process a@ code via lifting (see 'liftMX') and even publish its own messages
99-- back to the primary event bus (see 'mxBroadcast').
100--
101-- Since messages are delivered to agents from both the management event bus and
102-- the agent processes mailbox, agents (i.e., event sinks) will generally have
103-- no idea as to their origin. An agent can, however, choose to prioritise the
104-- choice of input (source) each time one of its event sinks runs. The /standard/
105-- way for an event sink to indicate that the agent is ready for its next input
106-- is to evaluate 'mxReady'. When this happens, the management infrastructure
107-- will obtain data from the event bus and process' mailbox in a round robbin
108-- fashion, i.e., one after the other, changing each time.
109--
110-- [Example Code]
111--
112-- What follows is a grossly over-simplified example of a management agent that
113-- provides a basic name monitoring facility. Whenever a process name is
114-- registered or unregistered, clients are informed of the fact.
115--
116-- > -- simple notification data type
117-- >
118-- > data Registration = Reg { added :: Bool
119-- > , procId :: ProcessId
120-- > , name :: String
121-- > }
122-- >
123-- > -- start a /name monitoring agent/
124-- > nameMonitorAgent = do
125-- > mxAgent (MxAgentId "name-monitor") Set.empty [
126-- > (mxSink $ \(pid :: ProcessId) -> do
127-- > mxUpdateState $ Set.insert pid
128-- > mxReady)
129-- > , (mxSink $
130-- > let act =
131-- > case ev of
132-- > (MxRegistered p n) -> notify True n p
133-- > (MxUnRegistered p n) -> notify False n p
134-- > _ -> return ()
135-- > act >> mxReady)
136-- > ]
137-- > where
138-- > notify a n p = do
139-- > Foldable.mapM_ (liftMX . deliver (Reg a n p)) =<< mxGetLocal
140-- >
141--
142-- The client interface (for sending their pid) can take one of two forms:
143--
144-- > monitorNames = getSelfPid >>= nsend "name-monitor"
145-- > monitorNames2 = getSelfPid >>= mxNotify
146--
147-- For some real-world examples, see the distributed-process-platform package.
148--
149-- [Performance, Stablity and Scalability]
150--
151-- /Management Agents/ offer numerous advantages over regular processes:
152-- broadcast communication with them can have a lower latency, they offer
153-- simplified messgage (i.e., input type) handling and they have access to
154-- internal system information that would be otherwise unobtainable.
155--
156-- Do not be tempted to implement everything (e.g., the kitchen sink) using the
157-- management API though. There are overheads associated with management agents
158-- which is why they're presented as tools for consuming low level system
159-- information, instead of as /application level/ development tools.
160--
161-- Agents that rely heavily on a busy mailbox can cause the management event
162-- bus to backlog un-GC'ed data, leading to increased heap space. Producers that
163-- do not take care to avoid passing unevaluated thunks to the API can crash
164-- /all/ the agents in the system. Agents are not monitored or managed in any
165-- way, and those that crash will not be restarted.
166--
167-- The management event bus can receive a great deal of traffic. Every time
168-- a message is sent and/or received, an event is passed to the agent controller
169-- and broadcast to all agents (plus the trace controller, if tracing is enabled
170-- for the node). This is already a significant overhead - though profiling and
171-- benchmarks have demonstrated that it does not adversely affect performance
172-- if few agents are installed. Agents will typically use more cycles than plain
173-- processes, since they perform additional work: selecting input data from both
174-- the event bus /and/ their own mailboxes, plus searching through the set of
175-- event sinks (for each agent) to determine the right handler for the event.
176--
177-- [Architecture Overview]
178--
179-- The architecture of the management event bus is internal and subject to
180-- change without prior notice. The description that follows is provided for
181-- informational purposes only.
182--
183-- When a node initially starts, two special, internal system processes are
184-- started to support the management infrastructure. The first, known as the
185-- /trace controller/, is responsible for consuming 'MxEvent's and forwarding
186-- them to the configured tracer - see "Control.Distributed.Process.Debug" for
187-- further details. The second is the /management agent controller/, and is the
188-- primary worker process underpinning the management infrastructure. All
189-- published management events are routed to this process, which places them
190-- onto a system wide /event bus/ and additionally passes them directly to the
191-- /trace controller/.
192--
193-- There are several reasons for segregating the tracing and management control
194-- planes in this fashion. Tracing can be enabled or disabled by clients, whilst
195-- the management event bus cannot, since in addition to providing
196-- runtime instrumentation, its intended use-cases include node monitoring, peer
197-- discovery (via topology providing backends) and other essential system
198-- services that require knowledge of otherwise hidden system internals. Tracing
199-- is also subject to /trace flags/ that limit the specific 'MxEvent's delivered
200-- to trace clients - an overhead/complexity not shared by management agents.
201-- Finally, tracing and management agents are implemented using completely
202-- different signalling techniques - more on this later - which would introduce
203-- considerable complexity if the shared the same /event loop/.
204--
205-- The management control plane is driven by a shared broadcast channel, which
206-- is written to by the agent controller and subscribed to by all agent
207-- processes. Agents are spawned as regular processes, whose primary
208-- implementation (i.e., /server loop/) is responsible for consuming
209-- messages from both the broadcast channel and their own mailbox. Once
210-- consumed, messages are applied to the agent's /event sinks/ until one
211-- matches the input, at which point it is applied and the loop continues.
212-- The implementation chooses from the event bus and the mailbox in a
213-- round-robin fashion, until a message is received. This polling activity would
214-- lead to management agents consuming considerable system resources if left
215-- unchecked, therefore the implementation will poll for a limitted number of
216-- retries, after which it will perform a blocking read on the event bus.
217--
218-----------------------------------------------------------------------------
219module Control.Distributed.Process.Management
220 (
221 MxEvent(..)
222 , Destination(..)
223 -- * Firing Arbitrary /Mx Events/
224 , mxNotify
225 -- * Constructing Mx Agents
226 , MxAction()
227 , MxAgentId(..)
228 , MxAgent()
229 , mxAgent
230 , mxAgentWithFinalize
231 , MxSink()
232 , mxSink
233 , mxGetId
234 , mxDeactivate
235 , mxReady
236 , mxSkip
237 , mxReceive
238 , mxReceiveChan
239 , mxBroadcast
240 , mxSetLocal
241 , mxGetLocal
242 , mxUpdateLocal
243 , liftMX
244 ) where
246import Control.Applicative
247import Control.Concurrent.STM (atomically)
248import Control.Concurrent.STM.TChan
249 ( readTChan
250 , writeTChan
251 , TChan
252 )
253import Control.Distributed.Process.Internal.Primitives
254 ( receiveWait
255 , matchAny
256 , matchSTM
257 , unwrapMessage
258 , register
259 , whereis
260 , die
261 )
262import Control.Distributed.Process.Internal.Types
263 ( Process
264 , ProcessId
265 , Message
266 , LocalProcess(..)
267 , LocalNode(..)
268 , MxEventBus(..)
269 , unsafeCreateUnencodedMessage
270 )
271import Control.Distributed.Process.Management.Internal.Bus (publishEvent)
272import Control.Distributed.Process.Management.Internal.Types
273 ( MxAgentId(..)
274 , MxAgent(..)
275 , MxAction(..)
276 , ChannelSelector(..)
277 , MxAgentState(..)
278 , MxSink
279 , MxEvent(..)
280 , Destination(..)
281 )
282import Control.Distributed.Process.Serializable (Serializable)
283import Control.Monad.IO.Class (liftIO)
284import Control.Monad.Reader (ask)
285import Control.Monad.Catch (onException)
286import qualified Control.Monad.State as ST
287 ( get
288 , modify
289 , lift
290 , runStateT
291 )
292import Prelude
294-- | Publishes an arbitrary @Serializable@ message to the management event bus.
295-- Note that /no attempt is made to force the argument/, therefore it is very
296-- important that you do not pass unevaluated thunks that might crash the
297-- receiving process via this API, since /all/ registered agents will gain
298-- access to the data structure once it is broadcast by the agent controller.
299mxNotify :: (Serializable a) => a -> Process ()
300mxNotify msg = do
301 bus <- localEventBus . processNode <$> ask
302 liftIO $ publishEvent bus $ unsafeCreateUnencodedMessage msg
304--------------------------------------------------------------------------------
305-- API for writing user defined management extensions (i.e., agents) --
306--------------------------------------------------------------------------------
308-- | Return the 'MxAgentId' for the currently executing agent.
309--
310mxGetId :: MxAgent s MxAgentId
311mxGetId = ST.get >>= return . mxAgentId
313-- | The 'MxAgent' version of 'mxNotify'.
314--
315mxBroadcast :: (Serializable m) => m -> MxAgent s ()
316mxBroadcast msg = do
317 state <- ST.get
318 liftMX $ liftIO $ atomically $ do
319 writeTChan (mxBus state) (unsafeCreateUnencodedMessage msg)
321-- | Gracefully terminate an agent.
322--
323mxDeactivate :: forall s. String -> MxAgent s MxAction
324mxDeactivate = return . MxAgentDeactivate
326-- | Continue executing (i.e., receiving and processing messages).
327--
328mxReady :: forall s. MxAgent s MxAction
329mxReady = return MxAgentReady
331-- | Causes the currently executing /event sink/ to be skipped.
332-- The remaining declared event sinks will be evaluated to find
333-- a matching handler. Can be used to allow multiple event sinks
334-- to process data of the same type.
335--
336mxSkip :: forall s. MxAgent s MxAction
337mxSkip = return MxAgentSkip
339-- | Continue exeucting, prioritising inputs from the process' own
340-- /mailbox/ ahead of data from the management event bus.
341--
342mxReceive :: forall s. MxAgent s MxAction
343mxReceive = return $ MxAgentPrioritise Mailbox
345-- | Continue exeucting, prioritising inputs from the management event bus
346-- over the process' own /mailbox/.
347--
348mxReceiveChan :: forall s. MxAgent s MxAction
349mxReceiveChan = return $ MxAgentPrioritise InputChan
351-- | Lift a @Process@ action.
352--
353liftMX :: Process a -> MxAgent s a
354liftMX p = MxAgent $ ST.lift p
356-- | Set the agent's local state.
357--
358mxSetLocal :: s -> MxAgent s ()
359mxSetLocal s = ST.modify $ \st -> st { mxLocalState = s }
361-- | Update the agent's local state.
362--
363mxUpdateLocal :: (s -> s) -> MxAgent s ()
364mxUpdateLocal f = ST.modify $ \st -> st { mxLocalState = (f $ mxLocalState st) }
366-- | Fetch the agent's local state.
367--
368mxGetLocal :: MxAgent s s
369mxGetLocal = ST.get >>= return . mxLocalState
371-- | Create an 'MxSink' from an expression taking a @Serializable@ type @m@,
372-- that yields an 'MxAction' in the 'MxAgent' monad.
373--
374mxSink :: forall s m . (Serializable m)
375 => (m -> MxAgent s MxAction)
376 -> MxSink s
377mxSink act msg = do
378 msg' <- liftMX $ (unwrapMessage msg :: Process (Maybe m))
379 case msg' of
380 Nothing -> return Nothing
381 Just m -> do
382 r <- act m
383 case r of
384 MxAgentSkip -> return Nothing
385 _ -> return $ Just r
387-- private ADT: a linked list of event sinks
388data MxPipeline s =
389 MxPipeline
390 {
391 current :: !(MxSink s)
392 , next :: !(MxPipeline s)
393 } | MxStop
395-- | Activates a new agent.
396--
397mxAgent :: MxAgentId -> s -> [MxSink s] -> Process ProcessId
398mxAgent mxId st hs = mxAgentWithFinalize mxId st hs $ return ()
400-- | Activates a new agent. This variant takes a /finalizer/ expression,
401-- that is run once the agent shuts down (even in case of failure/exceptions).
402-- The /finalizer/ expression runs in the mx monad - @MxAgent s ()@ - such
403-- that the agent's internal state remains accessible to the shutdown/cleanup
404-- code.
405--
406mxAgentWithFinalize :: MxAgentId
407 -> s
408 -> [MxSink s]
409 -> MxAgent s ()
410 -> Process ProcessId
411mxAgentWithFinalize mxId initState handlers dtor = do
412 let name = agentId mxId
413 existing <- whereis name
414 case existing of
415 Just _ -> die "DuplicateAgentId" -- TODO: better error handling policy
416 Nothing -> do
417 node <- processNode <$> ask
418 pid <- liftIO $ mxNew (localEventBus node) $ start
419 register name pid
420 return pid
421 where
422 start (sendTChan, recvTChan) = do
423 let nState = MxAgentState mxId sendTChan initState
424 runAgent dtor handlers InputChan recvTChan nState
426 runAgent :: MxAgent s ()
427 -> [MxSink s]
428 -> ChannelSelector
429 -> TChan Message
430 -> MxAgentState s
431 -> Process ()
432 runAgent eh hs cs c s =
433 runAgentWithFinalizer eh hs cs c s
434 `onException` runAgentFinalizer eh s
436 runAgentWithFinalizer :: MxAgent s ()
437 -> [MxSink s]
438 -> ChannelSelector
439 -> TChan Message
440 -> MxAgentState s
441 -> Process ()
442 runAgentWithFinalizer eh' hs' cs' c' s' = do
443 msg <- getNextInput cs' c'
444 (action, state) <- runPipeline msg s' $ pipeline hs'
445 case action of
446 MxAgentReady -> runAgent eh' hs' InputChan c' state
447 MxAgentPrioritise priority -> runAgent eh' hs' priority c' state
448 MxAgentDeactivate _ -> runAgentFinalizer eh' state
449 MxAgentSkip -> error "IllegalState"
450-- MxAgentBecome h' -> runAgent h' c state
452 getNextInput sel chan =
453 let matches =
454 case sel of
455 Mailbox -> [ matchAny return
456 , matchSTM (readTChan chan) return]
457 InputChan -> [ matchSTM (readTChan chan) return
458 , matchAny return]
459 in receiveWait matches
461 runAgentFinalizer :: MxAgent s () -> MxAgentState s -> Process ()
462 runAgentFinalizer f s = ST.runStateT (unAgent f) s >>= return . fst
464 pipeline :: forall s . [MxSink s] -> MxPipeline s
465 pipeline [] = MxStop
466 pipeline (sink:sinks) = MxPipeline sink (pipeline sinks)
468 runPipeline :: forall s .
469 Message
470 -> MxAgentState s
471 -> MxPipeline s
472 -> Process (MxAction, MxAgentState s)
473 runPipeline _ state MxStop = return (MxAgentReady, state)
474 runPipeline msg state MxPipeline{..} = do
475 let act = current msg
476 (pass, state') <- ST.runStateT (unAgent act) state
477 case pass of
478 Nothing -> runPipeline msg state next
479 Just result -> return (result, state')