From 63046040d22435139227372783618a091d6ffa6d Mon Sep 17 00:00:00 2001 From: Jeff Epstein Date: Mon, 14 Feb 2011 18:00:29 +0000 Subject: [PATCH] first commit --- LICENSE | 31 + POTS/FakeTeleOS.hs | 43 - POTS/StateMachine.hs | 241 ----- README | 107 ++ RegServ.hs | 5 + Remote/Call.hs | 241 +++++ Remote/Channel.hs | 102 ++ Remote/Closure.hs | 27 + Remote/Encoding.hs | 142 +++ Remote/Init.hs | 40 + Remote/Peer.hs | 128 +++ Remote/Process.hs | 1740 ++++++++++++++++++++++++++++++++ Test-Channel-Global.hs | 64 ++ Test-Channel-Merge.hs | 63 ++ Test-Channel.hs | 59 ++ Test-Dialog.hs | 59 ++ Test-Discover.hs | 38 + Test-Global.hs | 75 ++ config | 4 + examples/kmeans1/KMeans.hs | 207 ++++ examples/kmeans1/kmeans | 26 + examples/kmeans1/kmeans-viewer | 74 ++ examples/kmeans1/mouse.png | Bin 0 -> 112938 bytes examples/pi/Pi-2.hs | 153 +++ examples/pi/Pi6.hs | 121 +++ notes.txt | 251 +++++ remote.cabal | 17 + test-POTS.hs | 81 -- test-POTS1.hs | 55 - test-POTS2.hs | 74 -- 30 files changed, 3774 insertions(+), 494 deletions(-) create mode 100644 LICENSE delete mode 100644 POTS/FakeTeleOS.hs delete mode 100644 POTS/StateMachine.hs create mode 100644 README create mode 100644 RegServ.hs create mode 100644 Remote/Call.hs create mode 100644 Remote/Channel.hs create mode 100644 Remote/Closure.hs create mode 100644 Remote/Encoding.hs create mode 100644 Remote/Init.hs create mode 100644 Remote/Peer.hs create mode 100644 Remote/Process.hs create mode 100644 Test-Channel-Global.hs create mode 100644 Test-Channel-Merge.hs create mode 100644 Test-Channel.hs create mode 100644 Test-Dialog.hs create mode 100644 Test-Discover.hs create mode 100644 Test-Global.hs create mode 100644 config create mode 100644 examples/kmeans1/KMeans.hs create mode 100755 examples/kmeans1/kmeans create mode 100755 examples/kmeans1/kmeans-viewer create mode 100644 examples/kmeans1/mouse.png create mode 100644 examples/pi/Pi-2.hs create mode 100644 examples/pi/Pi6.hs create mode 100644 notes.txt create mode 100644 remote.cabal delete mode 100644 test-POTS.hs delete mode 100644 test-POTS1.hs delete mode 100644 test-POTS2.hs diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0eeee12 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright Jeff Epstein 2011 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the author nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/POTS/FakeTeleOS.hs b/POTS/FakeTeleOS.hs deleted file mode 100644 index 014ede6..0000000 --- a/POTS/FakeTeleOS.hs +++ /dev/null @@ -1,43 +0,0 @@ -module POTS.FakeTeleOS where - -import Remote.Process -import Data.Char (isDigit) -import Control.Monad.Trans (liftIO) - -type HardwareAddress = String -notAnAddress :: HardwareAddress -notAnAddress = "null HardwareAddress" - -data ToneType = DialTone | RingTone | BusyTone | FaultTone deriving (Show) - -say :: String -> ProcessM () -say = liftIO . putStrLn - -startRing :: HardwareAddress -> ProcessM () -startRing ha = say $ concat ["Start ringing ", ha] - -stopRing :: HardwareAddress -> ProcessM () -stopRing ha = say $ concat ["Stop ringing ", ha] - -connect :: HardwareAddress -> HardwareAddress -> ProcessM () -connect ha1 ha2 = say $ concat ["Connection established between ", ha1," and ", ha2] - -disconnect :: HardwareAddress -> HardwareAddress -> ProcessM () -disconnect ha1 ha2 = say $ concat ["Connection broken between ", ha1," and ", ha2] - -startTone :: HardwareAddress -> ToneType -> ProcessM () -startTone ha tn = say $ concat ["Started ", show tn, " on ", ha] - -stopTone :: HardwareAddress -> ProcessM () -stopTone ha = say $ concat ["Stopped tone on ", ha] - -data AnalyseResult = ArGetMoreDigits | ArInvalid | ArOK ProcessId HardwareAddress - -analyse :: String -> ProcessM AnalyseResult -analyse s | not $ all isDigit s = return ArInvalid -analyse s | length s < 6 = return ArGetMoreDigits -analyse s = do res <- lookupProcessName s - if res == nullPid - then return ArInvalid - else return $ ArOK res s - diff --git a/POTS/StateMachine.hs b/POTS/StateMachine.hs deleted file mode 100644 index 9079d96..0000000 --- a/POTS/StateMachine.hs +++ /dev/null @@ -1,241 +0,0 @@ -{-# LANGUAGE TemplateHaskell,DeriveDataTypeable,RankNTypes #-} - -module POTS.StateMachine where - -import POTS.FakeTeleOS - -import Remote.Process -import Remote.Call (registerCalls,remoteCall) -import Remote.Encoding (genericGet, genericPut) - -import Control.Monad (when) -import Control.Monad.Trans (liftIO) -import Control.Concurrent (threadDelay) - -import Data.Data -import Data.Typeable -import Data.Binary - -while :: (Monad m) => m Bool -> m () -while a = do f <- a - when (f) - (while a >> return ()) - return () - - - -data TelephoneState = IsOnHook | IsOffHook - -data TelephoneMessage = TmOffHook | TmOnHook | TmDigitDialed Char deriving (Data, Typeable, Eq) -instance Binary TelephoneMessage where - get = genericGet - put = genericPut --- sent from the telephone hardware - -isOffHookTm tm = tm == TmOffHook -isOnHookTm tm = tm == TmOnHook - -data ControlMessages = CmSeize ProcessId | CmSeized | CmRejected | CmCleared | CmAnswered deriving (Data, Typeable, Eq) -instance Binary ControlMessages where - get = genericGet - put = genericPut --- sent from the control processes - -isSeizeCm cm = case cm of - CmSeize pid -> True - _ -> False - - -telephoneProcess :: TelephoneState -> ProcessM() -telephoneProcess state = receiveWait [ - match (\msg -> - case msg of - TmOffHook -> telephoneProcess IsOffHook - TmOnHook -> telephoneProcess IsOnHook - TmDigitDialed digit -> telephoneProcess state -- TODO: send a digit message somewhere ... - ) - ] - -initialProcess "NODE" = do - spawn (telephoneProcess IsOnHook) - return () - --- state functions: each corresponds to a state in the POTS state machine - -idle :: HardwareAddress -> ProcessM () - -idle adr = receiveWait [ --- Our phone is on-hook and no calls are in process to or from it - - match (\msg -> case msg of - CmSeize pidForA -> do - -- A is trying to call us - send pidForA CmSeized -- let them know - startRing adr -- start our bell ringing - ringingBSide adr pidForA -- next state - _ -> idle adr ), -- stay in this state - match (\msg -> case msg of - TmOffHook -> do - -- our handset has been lifted - startTone adr DialTone - gettingNumber adr [] -- next state - _ -> idle adr ) -- stay in this state - ] - --- Note the code duplication of "idle a". This is because the first wildcard match will --- match against "other" messages that are of type ControlMessage, while the second will --- match against those that are TelephoneMessages. The code for idle' below is a --- another way of saying, this without the duplication. - --- Will all of the "idle a" calls be compiled as tail calls? This is essential! - - -idle' adr = receiveWait [ - matchIf isSeizeCm - (\msg -> case msg of - CmSeize pid -> do - send pid CmSeized - startRing adr - ringingBSide adr pid ), - matchIf isOffHookTm - (\msg -> do - startTone adr DialTone - gettingNumber adr [] ), - matchOther (idle adr) -- Note that "other" may be a TelephoneMessage or a ControlMessage; we ignore both kinds - ] - -gettingNumber :: HardwareAddress -> String -> ProcessM () - -gettingNumber adr phoneNumber = --- Our handset (the A-side) is off-hook and we are in the --- process of collecting the digits that will make up the --- number that our handset wishes to call. If phoneNumber is null, --- the first digit has yet to be dialed, and the A-side is hearing --- the dial tone; otherwise, phoneNumber is a list of the digits --- that have already been dialed, and no tone is heard. - - receiveWait [ - match (\msg -> case msg of - TmDigitDialed d -> do - maybeStopTone adr phoneNumber - result <- analyse newNumber - case result of - ArGetMoreDigits -> gettingNumber adr newNumber - ArInvalid -> do startTone adr FaultTone - waitForOnHook adr (Just FaultTone) - ArOK pidForB adrsForB -> - do self <- getSelfPid - send pidForB (CmSeize self) - makeCallToB adr pidForB adrsForB - where newNumber = phoneNumber ++ [d] - TmOnHook -> do - maybeStopTone adr phoneNumber - idle adr - _ -> gettingNumber adr phoneNumber ), - match (\msg -> case msg of - CmSeize pidForA -> do - send pidForA CmRejected - gettingNumber adr phoneNumber - _ -> gettingNumber adr phoneNumber ) ] - -makeCallToB :: HardwareAddress -> ProcessId -> HardwareAddress -> ProcessM () - -makeCallToB adr pidOfB adrOfB = --- we have just asked to establish a connectionwith the B side, --- and are waiting for a response - - receiveWait [ - match (\msg -> case msg of - CmSeized -> do startTone adr RingTone - ringingASide adr pidOfB adrOfB - CmRejected -> do startTone adr BusyTone - waitForOnHook adr (Just BusyTone) - CmSeize pid -> do send pid CmRejected - makeCallToB adr pidOfB adrOfB ) ] - - -ringingASide :: HardwareAddress -> ProcessId -> HardwareAddress -> ProcessM () - -ringingASide adr pidOfB adrOfB = --- we have initiated a call; we are hearing the ring tone, and the other phone --- (B-side) is ringing - receiveWait [ - match (\msg -> case msg of - TmOnHook -> do send pidOfB CmCleared - stopTone adr - idle adr - _ -> ringingASide adr pidOfB adrOfB ), - match (\msg -> case msg of - CmAnswered -> do stopTone adr - connect adr adrOfB - speech adr pidOfB adrOfB - CmSeize pid -> do send pid CmRejected - ringingASide adr pidOfB adrOfB - _ -> ringingASide adr pidOfB adrOfB ) ] - - - -ringingBSide :: HardwareAddress -> ProcessId -> ProcessM () - -ringingBSide adr pidOfA = --- We have accepted a sieze request from the A-side, and our handset is ringing - receiveWait [ - match (\msg -> case msg of - CmCleared -> do stopRing adr - idle adr - CmSeize pidOfA -> do send pidOfA CmRejected - ringingBSide adr pidOfA - _ -> ringingBSide adr pidOfA ), - match (\msg -> case msg of - TmOffHook -> do stopRing adr - send pidOfA CmAnswered - speech adr pidOfA notAnAddress - _ -> ringingBSide adr pidOfA ) ] - -speech :: HardwareAddress -> ProcessId -> HardwareAddress -> ProcessM () - -speech myAdr otherPid otherAdr = --- Both sides of this call enter this state, and the parties can talk --- When the A-side enters this state, otherAdr is the HW address of the B-side. --- When the B-side enters this state, otherAdr is notAnAddress - receiveWait [ - match (\msg -> case msg of - TmOnHook -> do send otherPid CmCleared - maybeDisconnect myAdr otherAdr - idle myAdr - _ -> speech myAdr otherPid otherAdr ) , - match (\msg -> case msg of - CmCleared -> do maybeDisconnect myAdr otherAdr - waitForOnHook myAdr Nothing - _ -> speech myAdr otherPid otherAdr ) ] - - -waitForOnHook :: HardwareAddress -> Maybe ToneType -> ProcessM () - -waitForOnHook adr toneOpt = --- we are waiting for adr to hang up - receiveWait [ - match (\msg -> case msg of - TmOnHook -> do case toneOpt of - Nothing -> return () - _ -> stopTone adr - idle adr - _ -> waitForOnHook adr toneOpt ) ] - -- a control message should also be ignored. How should we do that? - - - --- auxiliary functions - -maybeStopTone adr [] = stopTone adr -maybeStopTone _ _ = return () - - -maybeDisconnect adr1 adr2 = if adr2 == notAnAddress - then return () - else disconnect adr1 adr2 - - - - - diff --git a/README b/README new file mode 100644 index 0000000..24337f5 --- /dev/null +++ b/README @@ -0,0 +1,107 @@ +-- * Introduction + +Many programming languages expose concurrent programming as a shared memory model, wherein multiple, concurrently executing programs, or threads, can examine and manipulate variables common to them all. Coordination between threads is achieved with locks, mutexes, and other synchronization mechanisms. In Haskell, these facilities are available as MVars. + +In contrast, languages like Erlang eschew shared data and require that concurrent threads communicate only by message-passing. The key insight of Erlang and languages like it is that reasoning about concurrency is much easier without shared memory. Under a message-passing scheme, a thread provides a recipient, given as a thread identifier, and a unit of data; that data will be transferred to the recipient's address space and placed in a queue, where it can be retrieved by the recipient. Because data is never shared implicitly, this is a particularly good model for distributed systems. + +This framework presents a combined approach to distributed framework. While it provides an Erlang-style message-passing system, it lets the programmer use existing paradigms from Concurrent Haskell. + +-- * Terminology + +Location is represented by a /node/. Usually, a node corresponds to an instance of the Haskell runtime system; that is, each independently executed Haskell program exists in its own node. Multiple nodes may run concurrently on a single physical host system, but the intention is that nodes run on separate hosts, to take advantage of more hardware. + +The basic unit of concurrency is the /process/ (as distinct from the same term as used at the OS level, applied to an instance of an executing program). A process can be considered a thread with a message queue, and is implemented as a lightweight GHC forkIO thread. There is little overhead involved in starting and executing processes, so programmers can start as many as they need. Processes can send message to other processes and receive messages from them. + +The state associated with process management is wrapped up in the Haskell monad ProcesssM. All framework functions for managing and communicating with processes run in this monad, and most distributed user code will, as well. + +-- * Process management + +Processes are created with the 'spawnRemote' and 'forkProcess' functions. Their type signatures help explain their operation: + +> forkProcess :: ProcessM () -> ProcessM ProcessId +> spawnRemote :: NodeId -> Closure (ProcessM ()) -> ProcessM ProcessId + +'forkProcess' takes a function in the ProcessM monad, starts it concurrently as a process on the same node as the caller, and gives a ProcessId that can be used to send messages to it. 'spawnRemote' works analogously, but also takes a NodeId, indicating where to run the process. This lets the programmer start arbitrary functions on other nodes, which may be running on other hosts. Actual code is not transmitted to the other node; instead, a function identifier is sent. This works on the assumption that all connected nodes are running identical copies of the compiled Haskell binary (unlike Erlang, which allows new code to be sent to remote nodes at runtime). + +We encode the function identifier used to start remote processes as a Closure. Closures may identify only top-level functions, without free variables. Since 'spawnRemote' is the only way to run a process on a remote node, functions run remotely cannot capture local mutable variables. This is the other key distinction between 'spawnRemote' and 'forkProcess': processes run locally with forkProcess share memory with each other, but processes started with 'spawnRemote' cannot (even if the target node is in fact the local node). + +The following code shows how local variable captures works with 'forkProcess'. There is no analogous code for 'spawnRemote'. + +> do m <- liftIO $ newEmptyMVar +> forkProcess (liftIO $ putMVar m ()) +> liftIO $ takeMVar m + +Whether a process is running locally or remotely, and whether or not it can share memory, sending messages to it works the same: the 'send' function, which corresponds to Erlang's ! operator. + +> send :: (Binary a) => ProcessId -> a -> ProcessM () + +Given a ProcessId (from 'forkProcess' or 'spawnRemote') and a chunk of serializable data (implementing Haskell's 'Data.Binary.Binary' type class), we can send a message to the given process. The message will transmitted across the network if necessary and placed in the process's message queue. Note that 'send' will accept any type of data, as long as it implements Binary. Initially, all basic Haskell types implement binary, including tuples and arrays, and it's easy to implement Binary for user-defined types. How then does the receiving process know the type of message to extract from its queue? A message can receive processes by distinguishing their type using the 'receiveWait' function, which corresponds to Erlang's receive clause. The process can provide a distinct handler for each type of process that it knows how to deal with; unmatched messages remain on the queue, where they may be retrieved by later invocations of 'receiveWait'. + +-- * Channels + +A /channel/ provides an alternative to message transmission with 'send' and 'receiveWait'. While 'send' and 'receiveWait' allow sending messages of any type, channels require messages to be of uniform type. Channels must be explicitly created with a call to 'makeChannel': + +> makeChannel :: (Binary a) => ProcessM (SendChannel a, ReceiveChannel a) + +The resulting 'SendChannel' can be used with the 'sendChannel' function to insert messages into the channel, and the 'ReceiveChannel' can be used with 'receiveChannel'. The SendChannel can be serialized and sent as part of messages to other processes, which can then write to it; the ReceiveChannel, though, cannot be serialized, although it can be read from multiple threads on the same node by variable capture. + +-- * Setup and walkthrough + +Here I'll provide a basic example of how to get started with your first project on this framework. + +Here's the overall strategy: We'll be running a program that will estimate pi, making use of available computing resources potentially on remote systems. There will be an arbitrary number of nodes, one of which will be designated the master, and the remaining nodes will be slaves. The slaves will estimate pi in such a way that their results can be combined by the master, and an approximation will be output. The more nodes, and the longer they run, the more precise the output. + +In more detail: the master will assign each slave a region of the Halton sequence, and the slaves will use elements of the sequence to estimate the ratio of points in a unit square that fall within a unit circle, and that the master will sum these ratios. + +Here's the procedure, step by step. + +1. Compile Pi6.hs. If you have the framework installed correctly, it should be sufficient to run: + +> ghc --make Pi6 + +2. Select the machines you want to run the program on, and select one of them to be the master. All hosts must be connected on a local area network. For the purposes of this explanation, we'll assume that you will run your master node on a machine named @masterhost@ and you will run two slave nodes each on machines named @slavehost1@ and @slavehost2@. + +3. Copy the compiled executable Pi6 to some location on each of the three hosts. + +4. For each node, we need to create a configuration file. This is plain text file, usually named @config@ and usually placed in the same directory with the executable. There are many possible settings that can be set in the configuration file, but only a few are necessary for this example; the rest have sensible defaults. On @masterhost@, create a file named @config@ with the following content: + +> cfgRole MASTER +> cfgHostName masterhost +> cfgKnownHosts masterhost slavehost1 slavehost2 + +On @slavehost1@, create a file named @config@ with the following content: + +> cfgRole SLAVE +> cfgHostName slavehost1 +> cfgKnownHosts masterhost slavehost1 slavehost2 + +On @slavehost2@, create a file named @config@ with the following content: + +> cfgRole SLAVE +> cfgHostName slavehost2 +> cfgKnownHosts masterhost slavehost1 slavehost2 + +A brief discussion of these settings and what they mean: + +The @cfgRole@ setting determines the node's initial behavior. This is a string which is used to differentiate the two kinds of nodes in this example. More complex distributed systems might have more different kinds of roles. In this case, SLAVE nodes do nothing on startup, but just wait from a command from a master, whereas MASTER nodes seek out slave nodes and issue them commands. + +The @cfgHostName@ setting indicates to each node the name of the host it's running on. + +The @cfgKnownHosts@ setting provides a list of hosts that form part of this distributed execution. This is necessary so that the master node can find its subservient slave nodes. + +Taken together, these three settings tell each node (a) it's own name, (b) the names of other nodes and (c) their behavioral relationship. + +5. Now, run the Pi6 program twice in each of the slave nodes. There should now be four slave nodes awaiting instructions. + +6. To start the execution, run Pi6 on the master node. You should see output like this: + +> 2011-02-10 11:14:38.373856 UTC 0 pid://masterhost:48079/6/ SAY Starting... +> 2011-02-10 11:14:38.374345 UTC 0 pid://masterhost:48079/6/ SAY Telling slave nid://slavehost1:33716/ to look at range 0..1000000 +> 2011-02-10 11:14:38.376479 UTC 0 pid://masterhost:48079/6/ SAY Telling slave nid://slavehost1:45343/ to look at range 1000000..2000000 +> 2011-02-10 11:14:38.382236 UTC 0 pid://masterhost:48079/6/ SAY Telling slave nid://slavehost2:51739/ to look at range 2000000..3000000 +> 2011-02-10 11:14:38.384613 UTC 0 pid://masterhost:48079/6/ SAY Telling slave nid://slavehost2:44756/ to look at range 3000000..4000000 +> 2011-02-10 11:14:56.720435 UTC 0 pid://masterhost:48079/6/ SAY Done: 3141606141606141606141606141606141606141606141606141606141606141606141606141606141606141606141606141 + +Let's talk about what's going on here. + +This output is generated by the framework's logging facility. Each line of output has the following fields, left-to-right: the date and time that the log entry was generated; the importance of the message (in this case 0); the process ID of the generating process; the subsystem or component that generated this message (in this case, SAY indicates that these messages were output by a call to the 'say' function); and the body of the message. From these messages, we can see that the master node discovered four nodes running on two remote hosts; for each of them, the master emits a "Telling slave..." message. Note that although we had to specify the host names where the nodes were running in the config file, the master found all nodes running on each of those hosts. The log output also tells us which range of indices of the Halton sequence were assigned to each node. Each slave, having performed its calculation, sends its results back to the master, and when the master has received responses from all slaves, it prints out its estimate of pi and ends. The slave nodes continue running, waiting for another request. At this point, we could run the master again, or we can terminate the slaves manually with Ctrl-C or the kill command. diff --git a/RegServ.hs b/RegServ.hs new file mode 100644 index 0000000..a0f781d --- /dev/null +++ b/RegServ.hs @@ -0,0 +1,5 @@ +module Main where + +import Remote.Process (standaloneLocalRegistry) + +main = standaloneLocalRegistry "config" diff --git a/Remote/Call.hs b/Remote/Call.hs new file mode 100644 index 0000000..6e423aa --- /dev/null +++ b/Remote/Call.hs @@ -0,0 +1,241 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Remote.Call ( +-- * Compile-time metadata + remoteCall, + RemoteCallMetaData, +-- * Runtime metadata + registerCalls, + Lookup, + Identifier, + putReg, + getEntryByIdent, + empty, +-- * Re-exports + Payload, + Closure(..), + liftIO,serialEncodePure,serialDecode + ) where + +import Language.Haskell.TH +import Data.Maybe (maybe) +import Data.List (intercalate,null) +import qualified Data.Map as Map (Map,insert,lookup,empty,toList) +import Data.Dynamic (toDyn,Dynamic,fromDynamic,dynTypeRep) +import Data.Generics (Data) +import Data.Typeable (typeOf,Typeable) +import Data.IORef (newIORef,atomicModifyIORef,IORef,readIORef,writeIORef) +import Control.Concurrent.MVar (newMVar,takeMVar,putMVar,readMVar,MVar) +import Data.Binary (Binary) +import Remote.Encoding (Payload,serialDecode,serialEncodePure,Serializable) +import Control.Monad.Trans (liftIO) +import Remote.Closure (Closure(..)) + +import Debug.Trace + +---------------------------------------------- +-- * Compile-time metadata +---------------------------------------------- + +type RemoteCallMetaData = Lookup -> IO Lookup + +-- Language.Haskell.TH.recover seems to not work at all for syntax errors +-- during splicing as I would expect. Not really sure what it's good for, +-- then. It would be nice if we could have some kind of try/catch in here. + +-- | This is a compile-time function that automatically generates +-- metadata for declarations passed to it as an argument. It's designed +-- to be used within your source code like this: +-- +-- > remoteCall +-- > [d| +-- > myFun :: Int -> ProcessM () +-- > myFun = ... +-- > |] +-- +-- If you are using an older version of GHC, you may have to use this +-- syntax instead: +-- +-- > $( remoteCall +-- > [d| +-- > myFun :: Int -> ProcessM () +-- > myFun = ... +-- > |] ) +-- +-- Both of these forms require enabling Template Haskell with the +-- @-XTemplateHaskell@ compiler flag or @{-\# LANGUAGE TemplateHaskell \#-}@ +-- pragma. +-- +-- This function generates the following additional identifiers: +-- +-- 1. Each 'remoteCall' block emits a table of function names +-- used to invoke closures. The table is generated under the +-- name '__remoteCallMetaData'. All such tables, one per module, +-- must be registered when creating a node with 'Remote.Process.initNode' +-- by calling 'registerCalls'. For example: +-- +-- > lookup <- registerCalls [Main.__remoteCallMetaData, Foobar.__remoteCallMetaData] +-- +-- It is the programmer's responsibility to ensure that these names +-- are visible at the location where they will be used. +-- +-- 2. If a function has an explicit type signature and is in the +-- 'ProcessM' monad, a function is created to generate a closure +-- for that function, suitable for use with 'Remote.Process.spawnRemote'. +-- This function has the name of the original function combined with +-- the suffix @__closure@, and a return value equal to the original enclosed +-- in a 'Remote.Closure.Closure'. For example, given the above definition +-- of 'myFun', the closure-generation function would have the +-- the following signature: +-- +-- > myFun__impl :: Int -> Closure (ProcessM ()) +-- +-- You can use 'remoteCall' only once per module. All declarations +-- for which metadata is to be generated must be contained within a +-- single block. +remoteCall :: Q [Dec] -> Q [Dec] +remoteCall a = recover (errorMessage) (buildMeta $ buildMeta2 a) + +buildMeta2 :: Q [Dec] -> Q [Dec] +buildMeta2 qdecs = do decs <- qdecs + loc <- location + res <- mapM (fixDec loc) decs + return $ concat res + where fixDec loc dec = + case dec of + SigD name typ -> let implName = mkName (nameBase name ++ "__impl") + implFqn = loc_module loc ++"." ++ nameBase name ++ "__impl" + closureName = mkName (nameBase name ++ "__closure") + closurearglist = init arglist ++ [processmtoclosure (last arglist)] +-- processmtoclosure (AppT (ConT _) x) = (AppT (ConT (mkName "Remote.Process.ProcessM")) (AppT (ConT (mkName "Remote.Closure.Closure")) x)) + processmtoclosure (x) = (AppT (ConT (mkName "Remote.Call.Closure")) x) +-- processmtoclosure _ = error "Unexpected type in closure" + paramnames = map (\x -> 'a' : show x) [1..(length (init arglist))] + paramnamesP = (map (varP . mkName) paramnames) + paramnamesE = (map (varE . mkName) paramnames) + payload = [t| Remote.Encoding.Payload |] + just a = conP (mkName "Just") [a] + liftio = [e| Control.Monad.Trans.liftIO |] + decodecall = [e| Remote.Encoding.serialDecode |] + encodecall = [e| Remote.Encoding.serialEncodePure |] + closurecall = conE (mkName "Remote.Call.Closure") + returncall = [e| return |] + arglist = getParams typ + errorcall = [e| error |] + applyargs f [] = f + applyargs f (l:r) = applyargs (appE f l) r + closuredec = sigD closureName (return $ putParams closurearglist) + closuredef = funD closureName [clause paramnamesP + (normalB (appE (appE closurecall (litE (stringL implFqn))) (appE encodecall (tupE paramnamesE)))) + [] + ] + impldec = sigD implName (appT (appT arrowT payload) (return $ last arglist)) + impldef = funD implName [clause [varP (mkName "a")] + (normalB (doE [bindS (varP (mkName "res")) (appE liftio (appE decodecall (varE (mkName "a")))), + noBindS (caseE (varE (mkName "res")) + [match (just (tupP paramnamesP)) (normalB (applyargs (varE name) paramnamesE)) [], + match wildP (normalB (appE (errorcall) (litE (stringL ("Bad decoding in closure splice of "++nameBase name))))) []]) + ])) + []] + in case (last arglist) of +-- TODO this line currently prevents (in a very hacky way) generation of closure code +-- for non-ProcessM functions. I can imagine that you might want closures for +-- pure code, which might be invoked remotely with an auto-restart clause, though. + (AppT (ConT process) _) | show process == "Remote.Process.ProcessM" -> + do v <- sequence [closuredec,closuredef,impldec,impldef] + return $ dec:v + _ -> return [dec] + _ -> return [dec] + putParams (fst:lst:[]) = AppT (AppT ArrowT fst) lst + putParams (fst:[]) = fst + putParams (fst:lst) = AppT (AppT ArrowT fst) (putParams lst) + getParams typ = case typ of + AppT (AppT ArrowT b) c -> b : getParams c + b -> [b] + +errorLocation :: Loc -> String +errorLocation loc = let file = loc_filename loc + (x1,y1) = loc_start loc + (x2,y2) = loc_end loc in + (intercalate ":" [file,show x1,show y1]) ++ "-" ++ (intercalate ":" [show x2,show y2]) + +errorMessage :: Q a +errorMessage = do + loc <- location + error ("Remote.Call.remoteCall failed at compile-time at "++errorLocation loc++". Is it possible that you've invoked this function more than once per module in module "++loc_module loc++"?") + +collectNames :: [Dec] -> [Name] +collectNames decs = foldl each [] decs + where each entries dec = case dec of + (FunD name _) -> name:entries + (ValD (VarP name) _ _) -> name:entries + otherwise -> entries + +patchDecls :: Q [Dec] -> Q [Dec] +patchDecls decls = decls + +buildMeta :: Q [Dec] -> Q [Dec] +buildMeta qdecls = do decls <- patchDecls qdecls + loc <- location + let names = collectNames decls + let bind = [e| (=<<) |] +-- let register = [e| Remote.Call.register |] + let mkentry = [e| Remote.Call.putReg |] + let thetype = [t| RemoteCallMetaData |] + let iovoid = [t| IO () |] + let patq = (mkName "__remoteCallMetaData") + let reasonableNameModule name = maybe (loc_module loc++".") ((++)".") (nameModule name) + let app2Ei op l r = infixE (Just l) op (Just r) + let app2E op l r = appE (appE op l) r + param <- newName "x" + let applies [] = varE param + applies [h] = appE (app2E mkentry (varE h) (litE $ stringL (reasonableNameModule h++nameBase h))) (varE param) + applies (h:t) = app2Ei bind (app2E mkentry (varE h) (litE $ stringL (reasonableNameModule h++nameBase h))) (applies t) + let bodyq = normalB (applies names) + + sig <- sigD patq thetype + dec <- funD patq [clause [varP param] bodyq []] + if null decls + then return decls + else return (sig:dec:decls) + +---------------------------------------------- +-- * Run-time metadata +---------------------------------------------- + +type Identifier = String + +data Entry = Entry { + entryName :: Identifier, + entryFunRef :: Dynamic + } + +registerCalls :: [RemoteCallMetaData] -> IO Lookup +registerCalls [] = return empty +registerCalls (h:rest) = do registered <- registerCalls rest + h registered + +makeEntry :: (Typeable a) => Identifier -> a -> Entry +makeEntry ident funref = Entry {entryName=ident, entryFunRef=toDyn funref} + +type IdentMap = Map.Map Identifier Entry +data Lookup = Lookup { identMap :: IdentMap } + +putReg :: (Typeable a) => a -> Identifier -> Lookup -> IO Lookup +putReg a i l = putEntry l a i + +putEntry :: (Typeable a) => Lookup -> a -> Identifier -> IO Lookup +putEntry amap value name = do + return $ Lookup { + identMap = Map.insert name entry (identMap amap) + } + where + entry = makeEntry name value + + +getEntryByIdent :: (Typeable a) => Lookup -> Identifier -> Maybe a +getEntryByIdent amap ident = (Map.lookup ident (identMap amap)) >>= (\x -> fromDynamic (entryFunRef x)) + +empty :: Lookup +empty = Lookup {identMap = Map.empty} + diff --git a/Remote/Channel.hs b/Remote/Channel.hs new file mode 100644 index 0000000..9f5dc9a --- /dev/null +++ b/Remote/Channel.hs @@ -0,0 +1,102 @@ +{-# LANGUAGE ExistentialQuantification,DeriveDataTypeable #-} +module Remote.Channel (SendPort,ReceivePort,makeChannel,sendChannel,receiveChannel, + CombinedChannelAction,combinedChannelAction, + combineChannelsBiased,combineChannelsRR,mergeChannelsBiased,mergeChannelsRR, + terminateChannel) where + +import Remote.Process (ProcessM,send,setDaemonic,getProcess,prNodeRef,getNewMessageLocal,localFromPid,isPidLocal,pteChannel,TransmitException(..),TransmitStatus(..),Message,msgPayload,spawn,ProcessId,Node,UnknownMessageException(..)) +import Remote.Encoding (getPayloadType,serialDecodePure,Serializable) + +import Data.List (foldl') +import Data.Binary (Binary,get,put) +import Data.Typeable (Typeable) +import Control.Exception (throw) +import Control.Monad (when) +import Control.Monad.Trans (liftIO) +import Control.Concurrent.MVar (MVar,newEmptyMVar,takeMVar,readMVar,putMVar) +import Control.Concurrent.STM (STM,atomically,retry,orElse) +import Control.Concurrent.STM.TVar (TVar,newTVarIO,readTVar,writeTVar) + +---------------------------------------------- +-- * Channels +---------------------------------------------- + +newtype SendPort a = SendPort ProcessId deriving (Typeable) +data ReceivePort a = ReceivePortSimple ProcessId (MVar ()) + | ReceivePortBiased [Node -> STM a] + | ReceivePortRR (TVar [Node -> STM a]) + +instance Binary (SendPort a) where + put (SendPort pid) = put pid + get = get >>= return . SendPort + +makeChannel :: (Serializable a) => ProcessM (SendPort a, ReceivePort a) +makeChannel = do mv <- liftIO $ newEmptyMVar + pid <- spawn (body mv) + return (SendPort pid, + ReceivePortSimple pid mv) + where body mv = setDaemonic >> liftIO (takeMVar mv) + +sendChannel :: (Serializable a) => SendPort a -> a -> ProcessM () +sendChannel (SendPort pid) a = send pid a + +receiveChannel :: (Serializable a) => ReceivePort a -> ProcessM a +receiveChannel rc = do p <- getProcess + node <- liftIO $ readMVar (prNodeRef p) + channelCheckPids [rc] + liftIO $ atomically $ receiveChannelImpl node rc + +receiveChannelImpl :: (Serializable a) => Node -> ReceivePort a -> STM a +receiveChannelImpl node rc = + case rc of + ReceivePortBiased l -> foldl' orElse retry (map (\x -> x node) l) + ReceivePortRR mv -> do tv <- readTVar mv + writeTVar mv (rotate tv) + foldl' orElse retry (map (\x -> x node) tv) + ReceivePortSimple _ _ -> receiveChannelSimple node rc + where rotate [] = [] + rotate (h:t) = t ++ [h] + +data CombinedChannelAction b = forall a. (Serializable a) => CombinedChannelAction (ReceivePort a) (a -> b) + +combinedChannelAction :: (Serializable a) => ReceivePort a -> (a -> b) -> CombinedChannelAction b +combinedChannelAction = CombinedChannelAction + +combineChannelsBiased :: Serializable b => [CombinedChannelAction b] -> ProcessM (ReceivePort b) +combineChannelsBiased chns = do mapM_ (\(CombinedChannelAction chn _ ) -> channelCheckPids [chn]) chns + return $ ReceivePortBiased [(\node -> receiveChannelImpl node chn >>= return . fun) | (CombinedChannelAction chn fun) <- chns] + +combineChannelsRR :: Serializable b => [CombinedChannelAction b] -> ProcessM (ReceivePort b) +combineChannelsRR chns = do mapM_ (\(CombinedChannelAction chn _ ) -> channelCheckPids [chn]) chns + tv <- liftIO $ newTVarIO [(\node -> receiveChannelImpl node chn >>= return . fun) | (CombinedChannelAction chn fun) <- chns] + return $ ReceivePortRR tv + +mergeChannelsBiased :: (Serializable a) => [ReceivePort a] -> ProcessM (ReceivePort a) +mergeChannelsBiased chns = do channelCheckPids chns + return $ ReceivePortBiased [(\node -> receiveChannelImpl node chn) | chn <- chns] + +mergeChannelsRR :: (Serializable a) => [ReceivePort a] -> ProcessM (ReceivePort a) +mergeChannelsRR chns = do channelCheckPids chns + tv <- liftIO $ newTVarIO [(\node -> receiveChannelImpl node chn) | chn <- chns] + return $ ReceivePortRR tv + +channelCheckPids :: (Serializable a) => [ReceivePort a] -> ProcessM () +channelCheckPids chns = mapM_ checkPid chns + where checkPid (ReceivePortSimple pid _) = do islocal <- isPidLocal pid + when (not islocal) + (throw $ TransmitException QteUnknownPid) + checkPid _ = return () + +receiveChannelSimple :: (Serializable a) => Node -> ReceivePort a -> STM a +receiveChannelSimple node (ReceivePortSimple chpid _) = + do mmsg <- getNewMessageLocal (node) (localFromPid chpid) + case mmsg of + Nothing -> badPid + Just msg -> case serialDecodePure (msgPayload msg) of + Nothing -> throw $ UnknownMessageException (getPayloadType $ msgPayload msg) + Just q -> return q + where badPid = throw $ TransmitException QteUnknownPid + +terminateChannel :: (Serializable a) => ReceivePort a -> ProcessM () +terminateChannel (ReceivePortSimple _ term) = liftIO $ putMVar (term) () +terminateChannel _ = throw $ TransmitException QteUnknownPid diff --git a/Remote/Closure.hs b/Remote/Closure.hs new file mode 100644 index 0000000..279e32f --- /dev/null +++ b/Remote/Closure.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE DeriveDataTypeable #-} +module Remote.Closure (Closure(..)) where + +import Data.Binary (Binary,get,put) +import Data.Typeable (Typeable) +import Remote.Encoding (Payload) + +data Closure a = Closure String Payload + deriving (Typeable) +{- + In spirit, this is actually: +> data Closure a where +> Closure :: Serializable v => (v -#> a) -> v -> Closure a + Where "funny arrow" (-#>) identifies a function with no free variables. + We simulate this behavior by identifying top-level functions as strings. +-} + +instance Show (Closure a) where + show a = case a of + (Closure fn pl) -> show fn + +instance Binary (Closure a) where + get = do s <- get + v <- get + return $ Closure s v + put (Closure s v) = put s >> put v + diff --git a/Remote/Encoding.hs b/Remote/Encoding.hs new file mode 100644 index 0000000..6378527 --- /dev/null +++ b/Remote/Encoding.hs @@ -0,0 +1,142 @@ +{-# LANGUAGE DeriveDataTypeable,CPP,FlexibleInstances,UndecidableInstances #-} + +module Remote.Encoding ( + Serializable, + serialEncode, + serialEncodePure, + serialDecode, + serialDecodePure, + Payload, + PayloadLength, + hPutPayload, + hGetPayload, + payloadLength, + getPayloadType, + genericPut, + genericGet) where + +import Data.Binary (Binary,encode,decode,Put,Get,put,get,putWord8,getWord8) +import Control.Monad (liftM) +import Data.ByteString.Lazy (ByteString) +import qualified Data.ByteString.Lazy as B (hPut,hGet,length) +import Control.Exception (try,evaluate,SomeException) +import Data.Int (Int64) +import System.IO (Handle) +import Data.Typeable (typeOf,Typeable) +import Data.Generics (Data,gfoldl,gunfold, toConstr,constrRep,ConstrRep(..),repConstr,extQ,extR,dataTypeOf) + +class (Binary a,Typeable a) => Serializable a +instance (Binary a,Typeable a) => Serializable a + +data Payload = Payload + { + payloadType :: ByteString, + payloadContent :: ByteString + } deriving (Typeable) +type PayloadLength = Int64 + +instance Binary Payload where + put pl = put (payloadType pl) >> put (payloadContent pl) + get = get >>= \a -> get >>= \b -> return $ Payload {payloadType = a,payloadContent=b} + +payloadLength :: Payload -> PayloadLength +payloadLength (Payload t c) = B.length t + B.length c + +getPayloadType :: Payload -> String +getPayloadType pl = decode $ payloadType pl + +hPutPayload :: Handle -> Payload -> IO () +hPutPayload h (Payload t c) = B.hPut h (encode (B.length t :: PayloadLength)) >> + B.hPut h t >> + B.hPut h (encode (B.length c :: PayloadLength)) >> + B.hPut h c + +hGetPayload :: Handle -> IO Payload +hGetPayload h = do tl <- B.hGet h (fromIntegral baseLen) + t <- B.hGet h (fromIntegral (decode tl :: PayloadLength)) + cl <- B.hGet h (fromIntegral baseLen) + c <- B.hGet h (fromIntegral (decode cl :: PayloadLength)) + return $ Payload {payloadType = t,payloadContent = c} + where baseLen = B.length (encode (0::PayloadLength)) + +serialEncodePure :: (Serializable a) => a -> Payload +serialEncodePure a = Payload {payloadType = encode $ show $ typeOf a, + payloadContent = encode a} + +serialEncode :: (Serializable a) => a -> IO Payload +serialEncode a = do encoded <- evaluate $ encode a -- this evaluate is actually necessary, it turns out; it might be better to just use strict ByteStrings + return $ Payload {payloadType = encode $ show $ typeOf a, + payloadContent = encoded} + + +serialDecodePure :: (Serializable a) => Payload -> Maybe a +serialDecodePure a = (\id -> + if (decode $ payloadType a) == + show (typeOf $ id undefined) + then + let res = decode (payloadContent a) + in Just (id res) + else Nothing ) id + + +serialDecode :: (Serializable a) => Payload -> IO (Maybe a) +serialDecode a = (\id -> + if (decode $ payloadType a) == + show (typeOf $ id undefined) + then do + res <- try (evaluate $ decode (payloadContent a)) + :: (Serializable a) => IO (Either SomeException a) + case res of + Left _ -> return $ Nothing + Right v -> return $ Just $ id v + else return Nothing ) id + + +{- By default, gfoldl will try to store a String as a list of Chars, + which is pretty inefficient. So, we special-case string serialization + to use the serialization provided by Binary. Other types could + also be easily special-cased +-} +genericPut :: (Data a) => a -> Put +genericPut = generic `extQ` genericString + where generic what = fst $ gfoldl + (\(before, a_to_b) a -> (before >> genericPut a, a_to_b a)) + (\x -> (serializeConstr (constrRep (toConstr what)), x)) + what + genericString :: String -> Put + genericString = put.encode + +genericGet :: Data a => Get a +genericGet = generic `extR` genericString + where generic = (\id -> liftM id $ deserializeConstr $ \constr_rep -> + gunfold (\n -> do n' <- n + g' <- genericGet + return $ n' g') + (return) + (repConstr (dataTypeOf (id undefined)) constr_rep)) id + genericString :: Get String + genericString = do q <- get + return $ decode q + +serializeConstr :: ConstrRep -> Put +serializeConstr (AlgConstr ix) = putWord8 1 >> put ix +serializeConstr (IntConstr i) = putWord8 2 >> put i +serializeConstr (FloatConstr r) = putWord8 3 >> put r +#if __GLASGOW_HASKELL__ >= 611 +serializeConstr (CharConstr c) = putWord8 4 >> put c +#else +serializeConstr (StringConstr c) = putWord8 4 >> put (head c) +#endif + +deserializeConstr :: (ConstrRep -> Get a) -> Get a +deserializeConstr k = + do constr_ix <- getWord8 + case constr_ix of + 1 -> get >>= \ix -> k (AlgConstr ix) + 2 -> get >>= \i -> k (IntConstr i) + 3 -> get >>= \r -> k (FloatConstr r) +#if __GLASGOW_HASKELL__ >= 611 + 4 -> get >>= \c -> k (CharConstr c) +#else + 4 -> get >>= \c -> k (StringConstr (c:[])) +#endif diff --git a/Remote/Init.hs b/Remote/Init.hs new file mode 100644 index 0000000..b7cb414 --- /dev/null +++ b/Remote/Init.hs @@ -0,0 +1,40 @@ +module Remote.Init (remoteInit) where + +import Remote.Peer (startDiscoveryService) +import Remote.Process (pbracket,localRegistryRegisterNode,localRegistryHello,localRegistryUnregisterNode,startGlobalService,startLoggingService,startSpawnerService,ProcessM,readConfig,initNode,startLocalRegistry,forkAndListenAndDeliver,waitForThreads,roleDispatch,Node,runLocalProcess) +import Remote.Call (registerCalls,RemoteCallMetaData) + +import System.FilePath (FilePath) +import Control.Concurrent (threadDelay) +import Control.Monad.Trans (liftIO) +import Control.Concurrent.MVar (MVar,takeMVar,putMVar,newEmptyMVar) + +startServices :: ProcessM () +startServices = + do + startGlobalService + startLoggingService + startDiscoveryService + startSpawnerService + +dispatchServices :: MVar Node -> IO () +dispatchServices node = do mv <- newEmptyMVar + runLocalProcess node (startServices >> liftIO (putMVar mv ())) + takeMVar mv + +remoteInit :: FilePath -> [RemoteCallMetaData] -> (String -> ProcessM ()) -> IO () +remoteInit config metadata f = do + lookup <- registerCalls metadata + cfg <- readConfig True (Just config) + -- TODO sanity-check cfg + node <- initNode cfg lookup + startLocalRegistry cfg False + forkAndListenAndDeliver node cfg + dispatchServices node + roleDispatch node userFunction + waitForThreads node + threadDelay 500000 -- make configurable, or something + where userFunction s = pbracket + (localRegistryHello >> localRegistryRegisterNode) + (\_ -> localRegistryUnregisterNode) -- TODO this is not correct -- it belongs after waitForThreads + (\_ -> f s) diff --git a/Remote/Peer.hs b/Remote/Peer.hs new file mode 100644 index 0000000..c3fdf9b --- /dev/null +++ b/Remote/Peer.hs @@ -0,0 +1,128 @@ +{-# LANGUAGE DeriveDataTypeable #-} +module Remote.Peer (PeerInfo,startDiscoveryService,getPeersStatic,getPeersDynamic,findPeerByRole) where + +import Network.Socket (defaultHints,sendTo,recvFrom,sClose,Socket,getAddrInfo,AddrInfoFlag(..),setSocketOption,addrFlags,addrSocketType,addrFamily,SocketType(..),Family(..),addrProtocol,SocketOption(..),AddrInfo,bindSocket,addrAddress,SockAddr(..),socket) +import Network.BSD (getProtocolNumber) +import Control.Concurrent.MVar (takeMVar, newMVar, modifyMVar_) +import Remote.Process (PeerInfo,spawnAnd,setDaemonic,TransmitStatus(..),TransmitException(..),PayloadDisposition(..),ptimeout,getSelfNode,sendSimple,cfgRole,cfgKnownHosts,cfgPeerDiscoveryPort,match,receiveWait,getSelfPid,getConfig,NodeId(..),PortId,ProcessM,ptry,localRegistryQueryNodes) +import Control.Monad.Trans (liftIO) +import Data.Typeable (Typeable) +import Data.Maybe (catMaybes) +import Data.Binary (Binary,get,put) +import Control.Exception (bracket,ErrorCall(..),throw) +import Data.List (nub) +import qualified Data.Map as Map (keys,Map,unionsWith,insertWith,empty,lookup) + +data DiscoveryInfo = DiscoveryInfo + { + discNodeId :: NodeId, + discRole :: String + } deriving (Typeable,Eq) + +instance Binary DiscoveryInfo where + put (DiscoveryInfo nid role) = put nid >> put role + get = get >>= \nid -> get >>= \role -> return $ DiscoveryInfo nid role + +getUdpSocket :: PortId -> IO (Socket,AddrInfo) -- mostly copied from Network.Socket +getUdpSocket port = do + proto <- getProtocolNumber "udp" + let hints = defaultHints { addrFlags = [AI_PASSIVE,AI_ADDRCONFIG] + , addrSocketType = Datagram + , addrFamily = AF_INET -- only INET supports broadcast + , addrProtocol = proto } + addrs <- getAddrInfo (Just hints) Nothing (Just (show port)) + let addr = head addrs + s <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr) + return (s,addr) + +maxPacket :: Int +maxPacket = 1024 + +listenUdp :: PortId -> IO String +listenUdp port = + bracket + (getUdpSocket port) + (\(s,_) -> sClose s) + (\(sock,addr) -> do + setSocketOption sock ReuseAddr 1 + bindSocket sock (addrAddress addr) + (msg,_,_) <- recvFrom sock maxPacket + return msg + ) + +sendBroadcast :: PortId -> String -> IO () +sendBroadcast port str + | length str > maxPacket = throw $ TransmitException $ QteOther $ "sendBroadcast: Specified packet is too big for UDP broadcast, having a length of " ++ (show $ length str) + | otherwise = bracket + (getUdpSocket port >>= return . fst) + (sClose) + (\sock -> do + setSocketOption sock Broadcast 1 + res <- sendTo sock str (SockAddrInet (toEnum port) (-1)) + return () + ) + +-- | Returns a PeerInfo, containing a list of known nodes ordered by role. +-- This information is acquired by querying the local node registry on +-- each of the hosts in the cfgKnownHosts entry in this node's config. +-- Hostnames that don't respond are assumed to be down and nodes running +-- on them won't be included in the results. +getPeersStatic :: ProcessM PeerInfo +getPeersStatic = do cfg <- getConfig + let peers = cfgKnownHosts cfg + peerinfos <- mapM (localRegistryQueryNodes . hostToNodeId) peers + return $ Map.unionsWith (\a b -> nub $ a ++ b) (catMaybes peerinfos) + where hostToNodeId host = NodeId host 0 + +-- | Returns a PeerInfo, containing a list of known nodes ordered by role. +-- This information is acquired by sending out a UDP broadcast on the +-- local network; active nodes running the discovery service +-- should respond with their information. +-- If nodes are running outside of the local network, or if UDP broadcasts +-- are disabled by firewall configuration, this won't return useful +-- information; in that case, use getPeersStatic. +-- This function takes a parameter indicating how long in microseconds +-- to wait for hosts to respond. A number like 50000 is usually good enough, +-- unless your network is highly congested or with high latency. +getPeersDynamic :: Int -> ProcessM PeerInfo +getPeersDynamic t = + do pid <- getSelfPid + cfg <- getConfig + -- TODO should send broacast multiple times in case of packet loss + liftIO $ sendBroadcast (cfgPeerDiscoveryPort cfg) (show pid) + responses <- liftIO $ newMVar [] + ptimeout t (receiveInfo responses) + res <- liftIO $ takeMVar responses + let all = map (\di -> (discRole di,[discNodeId di])) (nub res) + return $ foldl (\a (k,v) -> Map.insertWith (++) k v a ) Map.empty all + where receiveInfo responses = let matchInfo = match (\x -> liftIO $ modifyMVar_ responses (\m -> return (x:m))) in + receiveWait [matchInfo] >> receiveInfo responses + +-- | Given a PeerInfo returned by getPeersDynamic or getPeersStatic, +-- give a list of nodes registered as a particular role. If no nodes of +-- that role are found, the empty list is returned. +findPeerByRole :: PeerInfo -> String -> [NodeId] +findPeerByRole disc role = maybe [] id (Map.lookup role disc) + +findRoles :: PeerInfo -> [String] +findRoles disc = Map.keys disc + +waitForDiscovery :: Int -> ProcessM Bool +waitForDiscovery delay + | delay <= 0 = doit + | otherwise = ptimeout delay doit >>= (return . maybe False id) + where doit = + do cfg <- getConfig + msg <- liftIO $ listenUdp (cfgPeerDiscoveryPort cfg) + nodeid <- getSelfNode + res <- ptry $ sendSimple (read msg) (DiscoveryInfo {discNodeId=nodeid,discRole=cfgRole cfg}) PldUser + :: ProcessM (Either ErrorCall TransmitStatus) + case res of + Right QteOK -> return True + _ -> return False + +-- | Starts the discovery process, allowing this node to respond to +-- queries from getPeersDynamic. +startDiscoveryService :: ProcessM () +startDiscoveryService = spawnAnd service setDaemonic >> return () + where service = waitForDiscovery 0 >> service diff --git a/Remote/Process.hs b/Remote/Process.hs new file mode 100644 index 0000000..0c2eb34 --- /dev/null +++ b/Remote/Process.hs @@ -0,0 +1,1740 @@ +{-# LANGUAGE DeriveDataTypeable #-} +module Remote.Process where + +import Control.Concurrent (forkIO,ThreadId,threadDelay) +import Control.Concurrent.MVar (MVar,newMVar, newEmptyMVar,isEmptyMVar,takeMVar,putMVar,modifyMVar_,readMVar) +import Control.Exception (ErrorCall(..),throwTo,bracket,try,Exception,throw,evaluate,finally,SomeException,PatternMatchFail(..)) +import Control.Monad (foldM,when,liftM) +import Control.Monad.Trans (MonadTrans,lift,MonadIO,liftIO) +import Data.Binary (Binary,put,get,putWord8,getWord8) +import Data.Char (isSpace,isDigit) +import Data.Generics (Data) +import Data.List (foldl', isPrefixOf,nub) +import Data.Maybe (listToMaybe,catMaybes,fromJust,isNothing) +import Data.Typeable (Typeable) +import Data.Unique (newUnique,hashUnique) +import System.IO (Handle,hClose,hSetBuffering,hGetChar,hPutChar,BufferMode(..),hFlush) +import System.IO.Error (isEOFError,isDoesNotExistError,isUserError) +import Network.BSD (HostEntry(..),getHostName,getHostEntries) +import Network (HostName,PortID(..),PortNumber(..),listenOn,accept,sClose,connectTo,Socket) +import Network.Socket (PortNumber(..),setSocketOption,SocketOption(..),socketPort,aNY_PORT ) +import qualified Data.Map as Map (Map,fromList,elems,member,map,empty,adjust,alter,insert,delete,lookup,toList,size,insertWith') +import Remote.Call (getEntryByIdent,Lookup,empty) +import Remote.Encoding (serialEncode,serialDecode,serialEncodePure,serialDecodePure,Payload,Serializable,PayloadLength,genericPut,genericGet,hPutPayload,hGetPayload,payloadLength,getPayloadType) +import System.Environment (getArgs) +import qualified System.Timeout (timeout) +import System.FilePath (FilePath) +import Data.Time (getCurrentTime,diffUTCTime,UTCTime(..),utcToLocalZonedTime) +import Remote.Closure (Closure (..)) +import Control.Concurrent.STM (STM,atomically,retry,orElse) +import Control.Concurrent.STM.TChan (TChan,isEmptyTChan,readTChan,newTChanIO,writeTChan) +import Control.Concurrent.STM.TVar (TVar,newTVarIO,readTVar,writeTVar) + + + +import Debug.Trace + +---------------------------------------------- +-- * Closures +---------------------------------------------- + +decode :: (Serializable a) => Payload -> ProcessM (Maybe a) +decode pl = do + liftIO $ serialDecode pl + +-- TODO makeClosure should verify that the given fun argument actually exists +makeClosure :: (Typeable a,Serializable v) => String -> v -> ProcessM (Closure a) +makeClosure fun env = do + enc <- liftIO $ serialEncode env + return $ Closure fun enc + +invokeClosure :: (Typeable a) => Closure a -> ProcessM (Maybe a) +invokeClosure (Closure name arg) = (\id -> + do node <- getLookup + res <- sequence [pureFun node,ioFun node,procFun node] + case catMaybes res of + (a:b) -> return $ Just a + _ -> return Nothing ) id + where pureFun node = case getEntryByIdent node name of + Nothing -> return Nothing + Just x -> return $ Just $ (x arg) + ioFun node = case getEntryByIdent node name of + Nothing -> return Nothing + Just x -> liftIO (x arg) >>= (return.Just) + procFun node = case getEntryByIdent node name of + Nothing -> return Nothing + Just x -> (x arg) >>= (return.Just) + + +---------------------------------------------- +-- * Simple functional queue +---------------------------------------------- + +data Queue a = Queue [a] [a] + +queueMake :: Queue a +queueMake = Queue [] [] +queueEmpty :: Queue a -> Bool +queueEmpty (Queue [] []) = True +queueEmpty _ = False + +queueInsert :: Queue a -> a -> Queue a +queueInsert (Queue incoming outgoing) a = Queue (a:incoming) outgoing + +queueInsertMulti :: Queue a -> [a] -> Queue a +queueInsertMulti (Queue incoming outgoing) a = Queue (a++incoming) outgoing + +queueRemove :: Queue a -> (Maybe a,Queue a) +queueRemove (Queue incoming (a:outgoing)) = (Just a,Queue incoming outgoing) +queueRemove (Queue l@(_:_) []) = queueRemove $ Queue [] (reverse l) +queueRemove q@(Queue [] []) = (Nothing,q) + +queueToList :: Queue a -> [a] +queueToList (Queue incoming outgoing) = outgoing ++ reverse incoming +queueFromList :: [a] -> Queue a +queueFromList l = Queue [] l + +queueEach :: Queue a -> [(a,Queue a)] +queueEach q = case queueToList q of + [] -> [] + a:rest -> each [] a rest + where each before it [] = [(it,queueFromList before)] + each before it following@(next:after) = (it,queueFromList (before++following)):(each (before++[it]) next after) + +---------------------------------------------- +-- * Process monad +---------------------------------------------- + +type PortId = Int +type LocalProcessId = Int + +data Config = Config + { + cfgRole :: !String, + cfgHostName :: !HostName, + cfgListenPort :: !PortId, + cfgLocalRegistryListenPort :: !PortId, + cfgPeerDiscoveryPort :: !PortId, + cfgNetworkMagic :: !String, + cfgKnownHosts :: ![String], +-- cfgRoundtripTimeout :: Int -- currently unused +-- logConfig :: LogConfig + cfgArgs :: ![String] + } deriving (Show) + +type ProcessTable = Map.Map LocalProcessId ProcessTableEntry + +type AdminProcessTable = Map.Map ServiceId LocalProcessId + +data Node = Node + { +-- ndNodeId :: NodeId + ndProcessTable :: ProcessTable, + ndAdminProcessTable :: AdminProcessTable, +-- connectionTable :: Map.Map HostName Handle -- outgoing connections only +-- ndHostEntries :: [HostEntry], + ndHostName :: HostName, + ndListenPort :: PortId, + ndConfig :: Config, + ndLookup :: Lookup, + ndLogConfig :: LogConfig + --conncetion table -- each one is MVared + -- also contains time info about last contact + } + + +data NodeId = NodeId HostName PortId deriving (Data,Typeable,Eq) + +data ProcessId = ProcessId NodeId LocalProcessId deriving (Data,Typeable,Eq) -- TODO OR find-by-processname + +instance Binary NodeId where + put (NodeId h p) = put h >> put p + get = get >>= \h -> + get >>= \p -> + return (NodeId h p) + +instance Show NodeId where + show (NodeId hostname portid) = concat ["nid://",hostname,":",show portid,"/"] + +instance Read NodeId where + readsPrec _ s = if isPrefixOf "nid://" s + then let a = drop 6 s + hostname = takeWhile ((/=) ':') a + b = drop (length hostname+1) a + port= takeWhile ((/=) '/') b + c = drop (length port+1) b + result = NodeId hostname (read port) in + if (not.null) hostname && (not.null) port + then [(result,c)] + else error "Bad parse looking for NodeId" + else error "Bad parse looking for NodeId" + +instance Binary ProcessId where + put (ProcessId n p) = put n >> put p + get = get >>= \n -> + get >>= \p -> + return (ProcessId n p) + +instance Show ProcessId where + show (ProcessId (NodeId hostname portid) localprocessid) = concat ["pid://",hostname,":",show portid,"/",show localprocessid,"/"] + +instance Read ProcessId where + readsPrec _ s = if isPrefixOf "pid://" s + then let a = drop 6 s + hostname = takeWhile ((/=) ':') a + b = drop (length hostname+1) a + port= takeWhile ((/=) '/') b + c = drop (length port+1) b + pid = takeWhile ((/=) '/') c + d = drop (length pid+1) c + result = ProcessId (NodeId hostname (read port)) (read pid) in + if (not.null) hostname && (not.null) pid && (not.null) port + then [(result,d)] + else error "Bad parse looking for ProcessId" + else error "Bad parse looking for ProcessId" + +data PayloadDisposition = PldUser | + PldAdmin + deriving (Typeable,Read,Show,Eq) + +data Message = Message + { + msgDisposition :: PayloadDisposition, + msgHeader :: Maybe Payload, + msgPayload :: Payload + } + +data ProcessTableEntry = ProcessTableEntry + { + pteThread :: ThreadId, + pteChannel :: TChan Message, + pteDeathT :: TVar Bool, + pteDeath :: MVar (), + pteDaemonic :: Bool + } + +data ProcessState = ProcessState + { + prQueue :: Queue Message + } + +data Process = Process + { + prPid :: ProcessId, + prSelf :: LocalProcessId, + prNodeRef :: MVar Node, + prThread :: ThreadId, + prChannel :: TChan Message, + prState :: TVar ProcessState, + prLogConfig :: Maybe LogConfig + } + + +data ProcessM a = ProcessM {runProcessM :: Process -> IO (Process,a)} deriving Typeable + +instance Monad ProcessM where + m >>= k = ProcessM $ (\p -> (runProcessM m) p >>= (\(news,newa) -> runProcessM (k newa) news)) + return x = ProcessM $ \s -> return (s,x) + +instance Functor ProcessM where + fmap f v = ProcessM $ (\p -> (runProcessM v) p >>= (\x -> return $ fmap f x)) + +instance MonadIO ProcessM where + liftIO arg = ProcessM $ \pr -> (arg >>= (\x -> return (pr,x))) + +getProcess :: ProcessM (Process) +getProcess = ProcessM $ \x -> return (x,x) + +getConfig :: ProcessM (Config) +getConfig = do p <- getProcess + node <- liftIO $ readMVar (prNodeRef p) + return $ ndConfig node + +getLookup :: ProcessM (Lookup) +getLookup = do p <- getProcess + node <- liftIO $ readMVar (prNodeRef p) + return $ ndLookup node + +putProcess :: Process -> ProcessM () +putProcess p = ProcessM $ \_ -> return (p,()) + +---------------------------------------------- +-- * Message pattern matching +---------------------------------------------- + +data MatchM q a = MatchM { runMatchM :: MatchBlock -> STM ((MatchBlock,Maybe (ProcessM q)),a) } + +instance Monad (MatchM q) where + m >>= k = MatchM $ \mbi -> do + (mb,a) <- runMatchM m mbi + (mb',a2) <- runMatchM (k a) (fst mb) + return (mb',a2) + + return x = MatchM $ \mb -> return $ ((mb,Nothing),x) +-- fail _ = MatchM $ \_ -> return (False,Nothing) + +returnHalt :: a -> ProcessM q -> MatchM q a +returnHalt x invoker = MatchM $ \mb -> return $ ((mb,Just (invoker)),x) + +liftSTM :: STM a -> MatchM q a +liftSTM arg = MatchM $ \mb -> do a <- arg + return ((mb,Nothing),a) + + +data MatchBlock = MatchBlock + { + mbMessage :: Message + } + +getMatch :: MatchM q MatchBlock +getMatch = MatchM $ \x -> return ((x,Nothing), x) + + +getNewMessage :: Process -> STM Message +getNewMessage p = readTChan $ prChannel p + +getNewMessageLocal :: Node -> LocalProcessId -> STM (Maybe Message) +getNewMessageLocal node lpid = do mpte <- getProcessTableEntry (node) lpid + case mpte of + Just pte -> do msg <- readTChan (pteChannel pte) + return $ Just msg + Nothing -> return Nothing + + +getCurrentMessages :: Process -> STM [Message] +getCurrentMessages p = do + msgs <- cleanChannel (prChannel p) [] + ps <- readTVar (prState p) + let q = (prQueue ps) + let newq = queueInsertMulti q msgs + writeTVar (prState p) ps {prQueue = newq} + return $ queueToList newq + where cleanChannel c m = do empty <- isEmptyTChan c + if empty + then return m + else do item <- readTChan c + cleanChannel c (item:m) + +matchMessage :: [MatchM q ()] -> Message -> STM (Maybe (ProcessM q)) +matchMessage matchers msg = do (mb,r) <- (foldl orElse (retry) (map executor matchers)) `orElse` (return (theMatchBlock,Nothing)) + return r + where executor x = do + (ok@(mb,matchfound),_) <- runMatchM x theMatchBlock + case matchfound of + Nothing -> retry + n -> return ok + theMatchBlock = MatchBlock {mbMessage = msg} + +matchMessages :: [MatchM q ()] -> [(Message,STM ())] -> STM (Maybe (ProcessM q)) +matchMessages matchers msgs = (foldl orElse (retry) (map executor msgs)) `orElse` (return Nothing) + where executor (msg,acceptor) = do + res <- matchMessage matchers msg + case res of + Nothing -> retry + Just pmq -> acceptor >> return (Just pmq) + +receive :: [MatchM q ()] -> ProcessM (Maybe q) +receive m = do p <- getProcess + res <- liftIO $ atomically $ + do + msgs <- getCurrentMessages p + matchMessages m (mkMsgs p msgs) + case res of + Nothing -> return Nothing + Just n -> do q <- n + return $ Just q + where mkMsgs p msgs = map (\(m,q) -> (m,do ps <- readTVar (prState p) + writeTVar (prState p) ps {prQueue = queueFromList q})) (exclusionList msgs) + +receiveWait :: [MatchM q ()] -> ProcessM q +receiveWait m = + do p <- getProcess + v <- attempt1 p + attempt2 p v + where mkMsgs p msgs = map (\(m,q) -> (m,do ps <- readTVar (prState p) + writeTVar (prState p) ps {prQueue = queueFromList q})) (exclusionList msgs) + attempt2 p v = + case v of + Just n -> n + Nothing -> do ret <- liftIO $ atomically $ + do + msg <- getNewMessage p + ps <- readTVar (prState p) + let oldq = prQueue ps + writeTVar (prState p) ps {prQueue = queueInsert oldq msg} + matchMessages m [(msg,writeTVar (prState p) ps)] + attempt2 p ret + attempt1 p = liftIO $ atomically $ + do + msgs <- getCurrentMessages p + matchMessages m (mkMsgs p msgs) + +receiveTimeout :: Int -> [MatchM q ()] -> ProcessM (Maybe q) +receiveTimeout to m = ptimeout to $ receiveWait m + +matchUnknown :: ProcessM q -> MatchM q () +matchUnknown body = returnHalt () body + +matchUnknownThrow :: MatchM q () +matchUnknownThrow = do mb <- getMatch + returnHalt () (throw $ UnknownMessageException (getPayloadType $ msgPayload (mbMessage mb))) + +match :: (Serializable a) => (a -> ProcessM q) -> MatchM q () +match = matchCoreHeaderless PldUser (const True) + +matchIf :: (Serializable a) => (a -> Bool) -> (a -> ProcessM q) -> MatchM q () +matchIf = matchCoreHeaderless PldUser + +matchCoreHeaderless :: (Serializable a) => PayloadDisposition -> (a -> Bool) -> (a -> ProcessM q) -> MatchM q () +matchCoreHeaderless pld f g = matchCore pld (\(a,b) -> b==(Nothing::Maybe ()) && f a) + (\(a,_) -> g a) + +matchCore :: (Serializable a,Serializable b) => PayloadDisposition -> ((a,Maybe b) -> Bool) -> ((a,Maybe b) -> ProcessM q) -> MatchM q () +matchCore pld cond body = + do mb <- getMatch + doit mb + where + doit mb = + let + decodified = serialDecodePure (msgPayload (mbMessage mb)) + decodifiedh = maybe (Nothing) (serialDecodePure) (msgHeader (mbMessage mb)) + in + case decodified of + Just x -> if cond (x,decodifiedh) + then returnHalt () (body (x,decodifiedh)) + else liftSTM retry + Nothing -> liftSTM retry + + + +---------------------------------------------- +-- * Exceptions and return values +---------------------------------------------- + +data ConfigException = ConfigException String deriving (Show,Typeable) +instance Exception ConfigException + +data TransmitException = TransmitException TransmitStatus deriving (Show,Typeable) +instance Exception TransmitException + +data UnknownMessageException = UnknownMessageException String deriving (Show,Typeable) +instance Exception UnknownMessageException + +data ServiceException = ServiceException String deriving (Show,Typeable) +instance Exception ServiceException + +data TransmitStatus = QteOK + | QteUnknownPid + | QteBadFormat + | QteOther String + | QtePleaseSendBody + | QteBadNetworkMagic + | QteNetworkError String + | QteEncodingError String + | QteDispositionFailed + | QteLoggingError + | QteUnknownCommand deriving (Show,Read) + + +data ProcessMonitorException = ProcessMonitorException ProcessId SignalType SignalReason deriving (Typeable) + +instance Exception ProcessMonitorException + +instance Show ProcessMonitorException where + show (ProcessMonitorException pid typ why) = "ProcessMonitorException propagated from process " ++ show pid ++ " by signal " ++ show typ ++ ", reason "++show why + + +---------------------------------------------- +-- * Node and process spawning +---------------------------------------------- + +initNode :: Config -> Lookup -> IO (MVar Node) +initNode cfg lookup = + do defaultHostName <- getHostName +-- hostEntries <- getHostEntries False + let newHostName = + if null (cfgHostName cfg) + then defaultHostName + else cfgHostName cfg -- TODO it would be nice to check that is name actually makes sense, e.g. try to contact ourselves + theNewHostName <- evaluate newHostName + mvar <- newMVar Node {ndHostName=theNewHostName, +-- ndHostEntries=hostEntries, + ndProcessTable=Map.empty, + ndAdminProcessTable=Map.empty, + ndConfig=cfg, + ndLookup=lookup, + ndListenPort=0, + ndLogConfig=defaultLogConfig} + return mvar + +roleDispatch :: MVar Node -> (String -> ProcessM ()) -> IO () +roleDispatch mnode func = readMVar mnode >>= + \node -> + let role = cfgRole (ndConfig node) in + runLocalProcess mnode (func role) >> return () + +spawnAnd :: ProcessM () -> ProcessM () -> ProcessM ProcessId +spawnAnd fun and = do p <- getProcess + v <- liftIO $ newEmptyMVar + pid <- liftIO $ runLocalProcess (prNodeRef p) (myFun v) + liftIO $ takeMVar v + return pid + where myFun mv = (and `pfinally` liftIO (putMVar mv ())) >> fun + +-- | A synonym for 'spawn' +forkProcess :: ProcessM () -> ProcessM ProcessId +forkProcess = spawn + +-- | Create a parallel process sharing the same message queue and PID. +-- Not safe for export, as doing any message receive operation could +-- result in a munged message queue. +forkProcessWeak :: ProcessM () -> ProcessM () +forkProcessWeak f = do p <- getProcess + res <- liftIO $ forkIO (runProcessM f p >> return ()) + return () + +spawn :: ProcessM () -> ProcessM ProcessId +spawn fun = do p <- getProcess + liftIO $ runLocalProcess (prNodeRef p) fun + +runLocalProcess :: MVar Node -> ProcessM () -> IO ProcessId +runLocalProcess node fun = + do + localprocessid <- newLocalProcessId + channel <- newTChanIO + passProcess <- newEmptyMVar + okay <- newEmptyMVar + thread <- forkIO (runner okay node passProcess) + thenodeid <- getNodeId node + pid <- evaluate $ ProcessId thenodeid localprocessid + state <- newTVarIO $ ProcessState {prQueue = queueMake} + putMVar passProcess (mkProcess channel node localprocessid thread pid state) + takeMVar okay + return pid + where + notifyProcessDown p r = do nid <- getNodeId node + let pp = adminGetPid nid ServiceGlobal + let msg = AmSignal (prPid p) SigProcessDown r True + try $ sendBasic node pp (msg) (Nothing::Maybe ()) PldAdmin :: IO (Either SomeException TransmitStatus)--ignore result ok + notifyProcessUp p = do nid <- getNodeId node + let pp = adminGetPid nid ServiceGlobal + let msg = AmSignal (prPid p) SigProcessUp SrNormal True + try $ sendBasic node pp (msg) (Nothing::Maybe ()) PldAdmin :: IO (Either SomeException TransmitStatus)--ignore result ok + exceptionHandler e p = let shown = show e in + notifyProcessDown (p) (SrException shown) >> + (try (logI node (prPid p) "SYS" LoCritical (concat ["Process got unhandled exception ",shown]))::IO(Either SomeException ())) >> return () --ignore error + exceptionCatcher p fun = do notifyProcessUp (p) + res <- try fun -- TODO handle interprocess signals separately + case res of + Left e -> exceptionHandler (e::SomeException) p + Right a -> notifyProcessDown (p) SrNormal >> return a + runner okay node passProcess = do + p <- takeMVar passProcess + let init = do death <- newEmptyMVar + death2 <- newTVarIO False + let pte = (mkProcessTableEntry (prChannel p) (prThread p) death death2) + insertProcessTableEntry node (prSelf p) pte + return pte + let action = runProcessM fun p + bracket (init) + (\pte -> atomically (writeTVar (pteDeathT pte) True) >> putMVar (pteDeath pte) ()) + (\_ -> putMVar okay () >> + (exceptionCatcher p (action>>=return . snd)) >> return ()) + insertProcessTableEntry node processid entry = + modifyMVar_ node (\n -> + return $ n {ndProcessTable = Map.insert processid entry (ndProcessTable n)} ) + mkProcessTableEntry channel thread death death2 = + ProcessTableEntry + { + pteChannel = channel, + pteThread = thread, + pteDeath = death, + pteDeathT = death2, + pteDaemonic = False + } + mkProcess channel noderef localprocessid thread pid state = + Process + { + prPid = pid, + prSelf = localprocessid, + prChannel = channel, + prNodeRef = noderef, + prThread = thread, + prState = state, + prLogConfig = Nothing + } + + +---------------------------------------------- +-- * Roundtrip conversations +---------------------------------------------- + +roundtripQueryAsync :: (Serializable a,Serializable b) => PayloadDisposition -> [ProcessId] -> a -> ProcessM [Either TransmitStatus b] +roundtripQueryAsync pld pids dat = + let + convidsM = mapM (\_ -> liftIO $ newConversationId) pids + in do convids <- convidsM + sender <- getSelfPid + let + convopids = zip convids pids + sending (convid,pid) = + let hdr = RoundtripHeader {msgheaderConversationId = convid, + msgheaderSender = sender, + msgheaderDestination = pid} + in do res <- sendTry pid dat (Just hdr) pld + case res of + QteOK -> return (convid,Nothing) + n -> return (convid,Just (Left n)) + res <- mapM sending convopids + let receiving c = if (any isNothing (Map.elems c)) + then do newmap <- receiveWait [matcher c] + receiving newmap + else return c + matcher c = matchCore pld (\(_,h) -> case h of + Just a -> (msgheaderConversationId a,msgheaderSender a) `elem` convopids + Nothing -> False) + (\(b,Just h) -> + let newmap = Map.adjust (\_ -> (Just (Right b))) (msgheaderConversationId h) c + in return (newmap) ) + m <- receiving (Map.fromList res) + return $ catMaybes (Map.elems m) + +roundtripQuery :: (Serializable a, Serializable b) => PayloadDisposition -> ProcessId -> a -> ProcessM (Either TransmitStatus b) +roundtripQuery pld pid dat = + do convId <- liftIO $ newConversationId + sender <- getSelfPid + res <- sendTry pid dat (Just RoundtripHeader {msgheaderConversationId = convId,msgheaderSender = sender,msgheaderDestination = pid}) pld + case res of + QteOK -> receiveWait [matchCore pld (\(_,h) -> case h of + Just a -> msgheaderConversationId a == convId && msgheaderSender a == pid + Nothing -> False) (return . Right . fst)] + err -> return (Left err) + +roundtripResponse :: (Serializable a, Serializable b) => PayloadDisposition -> (a -> ProcessM (b,q)) -> MatchM q () +roundtripResponse pld f = + matchCore pld (\(_,h)->conditional h) transformer + where conditional :: Maybe RoundtripHeader -> Bool + conditional a = case a of + Just _ -> True + Nothing -> False + transformer (m,Just h) = + do + selfpid <- getSelfPid + (a,q) <- f m -- TODO put this in a try block, maybe send error message to other side + res <- sendTry (msgheaderSender h) a (Just RoundtripHeader {msgheaderSender=msgheaderDestination h,msgheaderDestination = msgheaderSender h, + msgheaderConversationId=msgheaderConversationId h}) PldUser + case res of + QteOK -> return q + _ -> throw $ TransmitException res + +data RoundtripHeader = RoundtripHeader + { + msgheaderConversationId :: Int, + msgheaderSender :: ProcessId, + msgheaderDestination :: ProcessId + } deriving (Typeable) +instance Binary RoundtripHeader where + put (RoundtripHeader a b c) = put a >> put b >> put c + get = get >>= \a -> get >>= \b -> get >>= \c -> return $ RoundtripHeader a b c + +---------------------------------------------- +-- * Message sending +---------------------------------------------- + +send :: (Serializable a) => ProcessId -> a -> ProcessM () +send pid msg = sendSimple pid msg PldUser >>= + (\x -> case x of + QteOK -> return () + _ -> throw $ TransmitException x + ) + +sendSimple :: (Serializable a) => ProcessId -> a -> PayloadDisposition -> ProcessM TransmitStatus +sendSimple pid dat pld = sendTry pid dat (Nothing :: Maybe ()) pld + +sendTry :: (Serializable a,Serializable b) => ProcessId -> a -> Maybe b -> PayloadDisposition -> ProcessM TransmitStatus +sendTry pid msg msghdr pld = getProcess >>= (\p -> + let + tryAction = ptry action >>= + (\x -> case x of + Left l -> return $ QteEncodingError $ show (l::ErrorCall) -- catch errors from encoding + Right r -> return r) + where action = + do p <- getProcess + node <- liftIO $ readMVar (prNodeRef p) + liftIO $ sendBasic (prNodeRef p) pid msg msghdr pld + in tryAction ) + +sendBasic :: (Serializable a,Serializable b) => MVar Node -> ProcessId -> a -> Maybe b -> PayloadDisposition -> IO TransmitStatus +sendBasic mnode pid msg msghdr pld = do + nid <- getNodeId mnode + node <- readMVar mnode + encoding <- liftIO $ serialEncode msg + header <- maybe (return Nothing) (\x -> do t <- liftIO $ serialEncode x + return $ Just t) msghdr + let themsg = Message {msgDisposition=pld,msgHeader = header,msgPayload=encoding} + let islocal = nodeFromPid pid == nid + (if islocal then sendRawLocal else sendRawRemote) mnode pid nid themsg + +sendRawLocal :: MVar Node -> ProcessId -> NodeId -> Message -> IO TransmitStatus +sendRawLocal noderef thepid nodeid msg + | thepid == nullPid = return QteUnknownPid + | otherwise = do node <- readMVar noderef + messageHandler (ndConfig node) noderef (msgDisposition msg) msg (cfgNetworkMagic (ndConfig node)) (localFromPid thepid) + +sendRawRemote :: MVar Node -> ProcessId -> NodeId -> Message -> IO TransmitStatus +sendRawRemote noderef thepid@(ProcessId (NodeId hostname portid) localpid) nodeid msg + | thepid == nullPid = return QteUnknownPid + | otherwise = try setup + >>= (\x -> case x of + Right n -> return n + Left l | isEOFError l -> return $ QteNetworkError (show l) + | isUserError l -> return QteBadFormat + | isDoesNotExistError l -> return $ QteNetworkError (show l) + | otherwise -> return $ QteOther $ show l + ) + where setup = bracket (connectTo hostname (PortNumber $ toEnum portid) >>= + (\h -> hSetBuffering h (BlockBuffering Nothing) >> return h)) + (hClose) + (sender) + sender h = do node <- readMVar noderef + writeMessage h (cfgNetworkMagic (ndConfig node),localpid,nodeid,msg) + +writeMessage :: Handle -> (String,LocalProcessId,NodeId,Message)-> IO TransmitStatus +writeMessage h (magic,dest,nodeid,msg) = + do hPutStrZ h $ unwords ["Rmt!!",magic,show dest,show (msgDisposition msg),show fmt,show nodeid] + hFlush h + response <- hGetLineZ h + resp <- readIO response :: IO TransmitStatus + case resp of + QtePleaseSendBody -> do maybe (return ()) (hPutPayload h) (msgHeader msg) + hPutPayload h $ msgPayload msg + hFlush h + response2 <- hGetLineZ h + resp2 <- readIO response2 :: IO TransmitStatus + return resp2 + QteOK -> return QteBadFormat + n -> return n + where fmt = case msgHeader msg of + Nothing -> (0::Int) + Just _ -> 2 + +---------------------------------------------- +-- * Message delivery +---------------------------------------------- + +forkAndListenAndDeliver :: MVar Node -> Config -> IO () +forkAndListenAndDeliver node cfg = do coord <- newEmptyMVar + forkIO $ listenAndDeliver node cfg (coord) + result <- takeMVar coord + maybe (return ()) throw result + +writeResult :: Handle -> TransmitStatus -> IO () +writeResult h er = hPutStrZ h (show er) >> hFlush h + +readMessage :: Handle -> IO (String,LocalProcessId,NodeId,Message) +readMessage h = + do line <- hGetLineZ h + case words line of + ["Rmt!!",magic,destp,disp,format,nodeid] -> + do adestp <- readIO destp :: IO LocalProcessId + adisp <- readIO disp :: IO PayloadDisposition + aformat <- readIO format :: IO Int + anodeid <- readIO nodeid :: IO NodeId + + writeResult h QtePleaseSendBody + hFlush h + header <- case aformat of + 2 -> do hdr <- hGetPayload h + return $ Just hdr + _ -> return Nothing + body <- hGetPayload h + return (magic,adestp,anodeid,Message { msgHeader = header, + msgDisposition = adisp, + msgPayload = body + }) + _ -> throw $ userError "Bad message format" + +getProcessTableEntry :: Node -> LocalProcessId -> STM (Maybe ProcessTableEntry) +getProcessTableEntry tbl adestp = let res = Map.lookup adestp (ndProcessTable tbl) in + case res of + Just x -> do isdead <- readTVar (pteDeathT x) + if isdead + then return Nothing + else return (Just x) + Nothing -> return Nothing + +deliver :: LocalProcessId -> MVar Node -> Message -> IO (Maybe ProcessTableEntry) +deliver adestp tbl msg = + do node <- readMVar tbl + atomically $ do pte <- getProcessTableEntry node adestp + maybe (return Nothing) (\x -> writeTChan (pteChannel x) msg >> return (Just x)) pte + +listenAndDeliver :: MVar Node -> Config -> MVar (Maybe IOError) -> IO () +listenAndDeliver node cfg coord = +-- this listenon should be replaced with a lower-level listen call that binds +-- to the interface corresponding to the name specified in cfgNodeName + do tryout <- try (listenOn whichPort) :: IO (Either IOError Socket) + case tryout of + Left e -> putMVar coord (Just e) + Right sock -> bracket ( + do + setSocketOption sock KeepAlive 1 + realPort <- socketPort sock + modifyMVar_ node (\a -> return $ a {ndListenPort=fromEnum realPort}) + putMVar coord Nothing + return sock) + (sClose) + (handleConnection) + where + whichPort = if cfgListenPort cfg /= 0 + then PortNumber $ toEnum $ cfgListenPort cfg + else PortNumber aNY_PORT + handleCommSafe h ho po = + try (handleComm h ho po) >>= (\x -> case x of + Left l -> case l of + n | isEOFError n -> writeResult h $ QteNetworkError (show n) + | isUserError n -> writeResult h QteBadFormat + | otherwise -> writeResult h (QteOther $ show n) + Right False -> return () + Right True -> handleCommSafe h ho po) + cleanBody n = reverse $ dropWhile isSpace (reverse (dropWhile isSpace n)) + handleComm h hostname portn = + do (magic,adestp,nodeid,msg) <- readMessage h + nd <- readMVar node + res <- messageHandler cfg node (msgDisposition msg) msg magic adestp + writeResult h res + case res of + QteOK -> return True + _ -> return False + + handleConnection sock = + do + (h,hostname,portn) <- accept sock + hSetBuffering h (BlockBuffering Nothing) + forkIO (handleCommSafe h hostname portn `finally` hClose h) + handleConnection sock + + +messageHandler :: Config -> MVar Node -> PayloadDisposition -> Message -> String -> LocalProcessId -> IO TransmitStatus +messageHandler cfg node pld = case pld of + PldAdmin -> dispositionAdminHandler + PldUser -> dispositionUserHandler + where + validMagic magic = magic == cfgNetworkMagic cfg -- same network + || magic == localRegistryMagicMagic -- message from local magic-neutral registry + || cfgRole cfg == localRegistryMagicRole-- message to local magic-neutral registry + dispositionAdminHandler msg magic adestp + | validMagic magic = + do tbl <- readMVar node + realPid <- adminLookupN (toEnum adestp) node + case realPid of + Left er -> return er + Right p -> do res <- deliver p node msg + case res of + Just _ -> return QteOK + Nothing -> return QteUnknownPid + | otherwise = return QteBadNetworkMagic + + dispositionUserHandler msg magic adestp + | validMagic magic = + do tbl <- readMVar node + res <- deliver adestp node msg + case res of + Nothing -> return QteUnknownPid + Just _ -> return QteOK + | otherwise = return QteBadNetworkMagic + +waitForThreads :: MVar Node -> IO () +waitForThreads mnode = do node <- takeMVar mnode + waitFor (Map.toList (ndProcessTable node)) node + where waitFor lst node= case lst of + [] -> putMVar mnode node + (pn,pte):rest -> if (pteDaemonic pte) + then waitFor rest node + else do putMVar mnode node + -- this take and modify should be atomic to ensure no one squeezes in a message to a zombie process + takeMVar (pteDeath pte) + modifyMVar_ mnode (\x -> return $ x {ndProcessTable=Map.delete pn (ndProcessTable x)}) + waitForThreads mnode + +---------------------------------------------- +-- * Miscellaneous utilities +---------------------------------------------- + +paddedString :: Int -> String -> String +paddedString i s = s ++ (replicate (i-length s) ' ') + +newLocalProcessId :: IO LocalProcessId +newLocalProcessId = do d <- newUnique + return $ hashUnique d + +newConversationId :: IO Int +newConversationId = do d <- newUnique + return $ hashUnique d + +-- TODO this is a huge performance bottleneck. Why? How to fix? +exclusionList :: [a] -> [(a,[a])] +exclusionList [] = [] +exclusionList (a:rest) = each [] a rest + where + each before it [] = [(it,before)] + each before it following@(next:after) = (it,before++following):(each (before++[it]) next after) + +{- +dumpMessageQueue :: ProcessM [String] +dumpMessageQueue = do p <- getProcess + msgs <- cleanChannel + ps <- liftIO $ modifyIORef (prState p) (\x -> x {prQueue = queueInsertMulti (prQueue x) msgs}) + buf <- liftIO $ readIORef (prState p) + return $ map msgPayload (queueToList $ prQueue buf) + + +printDumpMessageQueue :: ProcessM () +printDumpMessageQueue = do liftIO $ putStrLn "----BEGINDUMP------" + dump <- dumpMessageQueue + liftIO $ mapM putStrLn dump + liftIO $ putStrLn "----ENDNDUMP-------" +-} + +duration :: Int -> ProcessM a -> ProcessM (Int,a) +duration t a = + do time1 <- liftIO $ getCurrentTime + result <- a + time2 <- liftIO $ getCurrentTime + return (t - picosecondsToMicroseconds (fromEnum (diffUTCTime time2 time1)),result) + where picosecondsToMicroseconds a = a `div` 1000000 + + +hGetLineZ :: Handle -> IO String +hGetLineZ h = loop [] >>= return . reverse + where loop l = do c <- hGetChar h + case c of + '\0' -> return l + _ -> loop (c:l) + +hPutStrZ :: Handle -> String -> IO () +hPutStrZ h [] = hPutChar h '\0' +hPutStrZ h (c:l) = hPutChar h c >> hPutStrZ h l + +buildPid :: ProcessM () +buildPid = do p <- getProcess + node <- liftIO $ readMVar (prNodeRef p) + let pid=ProcessId (NodeId (ndHostName node) (ndListenPort node)) + (prSelf p) + putProcess $ p {prPid = pid} + +nullPid :: ProcessId +nullPid = ProcessId (NodeId "0.0.0.0" 0) 0 + +getSelfNode :: ProcessM NodeId +getSelfNode = do (ProcessId n p) <- getSelfPid + return n + +getNodeId :: MVar Node -> IO NodeId +getNodeId mnode = do node <- readMVar mnode + return (NodeId (ndHostName node) (ndListenPort node)) + +getSelfPid :: ProcessM ProcessId +getSelfPid = do p <- getProcess + case prPid p of + (ProcessId (NodeId _ 0) _) -> (buildPid >> getProcess >>= return.prPid) + _ -> return $ prPid p + +nodeFromPid :: ProcessId -> NodeId +nodeFromPid (ProcessId nid _) = nid + +localFromPid :: ProcessId -> LocalProcessId +localFromPid (ProcessId _ lid) = lid + +buildPidFromNodeId :: NodeId -> LocalProcessId -> ProcessId +buildPidFromNodeId n lp = ProcessId n lp + +localServiceToPid :: LocalProcessId -> ProcessM ProcessId +localServiceToPid sid = do (ProcessId nid lid) <- getSelfPid + return $ ProcessId nid sid + +isPidLocal :: ProcessId -> ProcessM Bool +isPidLocal pid = do mine <- getSelfPid + return (nodeFromPid mine == nodeFromPid pid) + + +---------------------------------------------- +-- * Exception handling +---------------------------------------------- + +ptry :: (Exception e) => ProcessM a -> ProcessM (Either e a) +ptry f = do p <- getProcess + res <- liftIO $ try (runProcessM f p) + case res of + Left e -> return $ Left e + Right (newp,newanswer) -> ProcessM (\_ -> return (newp,Right newanswer)) + +ptimeout :: Int -> ProcessM a -> ProcessM (Maybe a) +ptimeout t f = do p <- getProcess + res <- liftIO $ System.Timeout.timeout t (runProcessM f p) + case res of + Nothing -> return Nothing + Just (newp,newanswer) -> ProcessM (\_ -> return (newp,Just newanswer)) + +pbracket :: (ProcessM a) -> (a -> ProcessM b) -> (a -> ProcessM c) -> ProcessM c +pbracket before after fun = + do p <- getProcess + (newp2,newanswer2) <- liftIO $ bracket + (runProcessM before p) + (\(newp,newanswer) -> runProcessM (after newanswer) newp) + (\(newp,newanswer) -> runProcessM (fun newanswer) newp) + ProcessM (\_ -> return (newp2, newanswer2)) + +pfinally :: ProcessM a -> ProcessM b -> ProcessM a +pfinally fun after = pbracket (return ()) (\_ -> after) (const fun) + + +---------------------------------------------- +-- * Configuration file +---------------------------------------------- + +emptyConfig :: Config +emptyConfig = Config { + cfgRole = "NODE", + cfgHostName = "", + cfgListenPort = 0, + cfgPeerDiscoveryPort = 38813, + cfgLocalRegistryListenPort = 38813, + cfgNetworkMagic = "MAGIC", + cfgKnownHosts = [], + cfgArgs = [] +-- cfgRoundtripTimeout = 500000 + } + +readConfig :: Bool -> Maybe FilePath -> IO Config +readConfig useargs fp = do a <- safety (processConfigFile emptyConfig) ("while reading config file ") + b <- safety (processArgs a) "while parsing command line " + return b + where processConfigFile from = maybe (return from) (readConfigFile from) fp + processArgs from = if useargs + then readConfigArgs from + else return from + safety o s = do res <- try o :: IO (Either SomeException Config) + either (\e -> throw $ ConfigException $ s ++ show e) (return) res + readConfigFile from afile = do contents <- readFile afile + evaluate $ processConfig (lines contents) from + readConfigArgs from = do args <- getArgs + c <- evaluate $ processConfig (fst $ collectArgs args) from + return $ c {cfgArgs = reverse $ snd $ collectArgs args} + collectArgs the = foldl findArg ([],[]) the + findArg (last,n) ('-':this) | isPrefixOf "cfg" this = ((map (\x -> if x=='=' then ' ' else x) this):last,n) + findArg (last,n) a = (last,a:n) + +processConfig :: [String] -> Config -> Config +processConfig rawLines from = foldl processLine from rawLines + where + processLine cfg line = case words line of + ('#':_):_ -> cfg + [] -> cfg + [option,val] -> updateCfg cfg option val + option:rest | (not . null) rest -> updateCfg cfg option (unwords rest) + _ -> error $ "Bad configuration syntax: " ++ line + updateCfg cfg "cfgRole" role = cfg {cfgRole=clean role} + updateCfg cfg "cfgHostName" hn = cfg {cfgHostName=clean hn} + updateCfg cfg "cfgListenPort" p = cfg {cfgListenPort=(read.isInt) p} + updateCfg cfg "cfgPeerDiscoveryPort" p = cfg {cfgPeerDiscoveryPort=(read.isInt) p} +-- updateCfg cfg "cfgRoundtripTimeout" p = cfg {cfgRoundtripTimeout=(read.isInt) p} + updateCfg cfg "cfgLocalRegistryListenPort" p = cfg {cfgLocalRegistryListenPort=(read.isInt) p} + updateCfg cfg "cfgKnownHosts" m = cfg {cfgKnownHosts=words m} + updateCfg cfg "cfgNetworkMagic" m = cfg {cfgNetworkMagic=clean m} + updateCfg _ opt _ = error ("Unknown configuration option: "++opt) + isInt s | all isDigit s = s + isInt s = error ("Not a good number: "++s) + nonempty s | (not.null) s = s + nonempty b = error ("Unexpected empty item: " ++ b) + clean = filter (not.isSpace) + +---------------------------------------------- +-- * Logging +---------------------------------------------- + +data LogLevel = LoSay + | LoFatal + | LoCritical + | LoImportant + | LoStandard + | LoInformation + | LoTrivial deriving (Eq,Ord,Enum,Show) + +instance Binary LogLevel where + put n = put $ fromEnum n + get = get >>= return . toEnum + +type LogSphere = String + +data LogTarget = LtStdout + | LtForward NodeId + | LtFile FilePath + | LtForwarded -- special value -- don't set this in your logconfig! + deriving (Typeable) + +instance Binary LogTarget where + put LtStdout = putWord8 0 + put (LtForward nid) = putWord8 1 >> put nid + put (LtFile fp) = putWord8 2 >> put fp + put LtForwarded = putWord8 3 + get = do a <- getWord8 + case a of + 0 -> return LtStdout + 1 -> get >>= return . LtForward + 2 -> get >>= return . LtFile + 3 -> return LtForwarded + +data LogFilter = LfAll + | LfOnly [LogSphere] + | LfExclude [LogSphere] deriving (Typeable) + +instance Binary LogFilter where + put LfAll = putWord8 0 + put (LfOnly ls) = putWord8 1 >> put ls + put (LfExclude le) = putWord8 2 >> put le + get = do a <- getWord8 + case a of + 0 -> return LfAll + 1 -> get >>= return . LfOnly + 2 -> get >>= return . LfExclude + +data LogConfig = LogConfig + { + logLevel :: LogLevel, + logTarget :: LogTarget, + logFilter :: LogFilter + } deriving (Typeable) + +instance Binary LogConfig where + put (LogConfig ll lt lf) = put ll >> put lt >> put lf + get = do ll <- get + lt <- get + lf <- get + return $ LogConfig ll lt lf + +data LogMessage = LogMessage UTCTime LogLevel LogSphere String ProcessId LogTarget + | LogUpdateConfig LogConfig deriving (Typeable) + +instance Binary UTCTime where + put (UTCTime a b) = put (fromEnum a) >> put (fromEnum b) + get = do a <- get + b <- get + return $ UTCTime (toEnum a) (toEnum b) + + +instance Binary LogMessage where + put (LogMessage utc ll ls s pid target) = putWord8 0 >> put utc >> put ll >> put ls >> put s >> put pid >> put target + put (LogUpdateConfig lc) = putWord8 1 >> put lc + get = do a <- getWord8 + case a of + 0 -> do utc <- get + ll <- get + ls <- get + s <- get + pid <- get + target <- get + return $ LogMessage utc ll ls s pid target + 1 -> do lc <- get + return $ LogUpdateConfig lc + +instance Show LogMessage where + show (LogMessage utc ll ls s pid _) = concat [paddedString 30 (show utc)," ",(show $ fromEnum ll)," ",paddedString 28 (show pid)," ",ls," ",s] + +showLogMessage :: LogMessage -> IO String +showLogMessage (LogMessage utc ll ls s pid _) = + do gmt <- utcToLocalZonedTime utc + return $ concat [paddedString 30 (show gmt)," ", + (show $ fromEnum ll)," ", + paddedString 28 (show pid)," ",ls," ",s] + + +defaultLogConfig :: LogConfig +defaultLogConfig = LogConfig + { + logLevel = LoStandard, + logTarget = LtStdout, + logFilter = LfAll + } + +logApplyFilter :: LogConfig -> LogSphere -> LogLevel -> Bool +logApplyFilter cfg sph lev = filterSphere (logFilter cfg) + && filterLevel (logLevel cfg) + where filterSphere LfAll = True + filterSphere (LfOnly lst) = elem sph lst + filterSphere (LfExclude lst) = not $ elem sph lst + filterLevel ll = lev <= ll + +say :: String -> ProcessM () +say v = logS "SAY" LoSay v + +getLogConfig :: ProcessM LogConfig +getLogConfig = do p <- getProcess + case (prLogConfig p) of + Just lc -> return lc + Nothing -> do node <- liftIO $ readMVar (prNodeRef p) + return (ndLogConfig node) + +setLogConfig :: LogConfig -> ProcessM () +setLogConfig lc = do p <- getProcess + putProcess (p {prLogConfig = Just lc}) + +setNodeLogConfig :: LogConfig -> ProcessM () +setNodeLogConfig lc = do p <- getProcess + liftIO $ modifyMVar_ (prNodeRef p) (\x -> return $ x {ndLogConfig = lc}) + +setRemoteNodeLogConfig :: NodeId -> LogConfig -> ProcessM () +setRemoteNodeLogConfig nid lc = do res <- sendSimple (adminGetPid nid ServiceLog) (LogUpdateConfig lc) PldAdmin + case res of + QteOK -> return () + n -> throw $ TransmitException $ QteLoggingError + +logI :: MVar Node -> ProcessId -> LogSphere -> LogLevel -> String -> IO () +logI mnode pid sph ll txt = do node <- readMVar mnode + when (filtered (ndLogConfig node)) + (sendMsg (ndLogConfig node)) + where filtered cfg = logApplyFilter cfg sph ll + makeMsg cfg = do time <- getCurrentTime + return $ LogMessage time ll sph txt pid (logTarget cfg) + sendMsg cfg = do msg <- makeMsg cfg + res <- let svc = (adminGetPid nid ServiceLog) + nid = nodeFromPid pid + in sendBasic mnode svc msg (Nothing::Maybe()) PldAdmin + case res of + _ -> return () -- ignore error -- what can I do? + + + +logS :: LogSphere -> LogLevel -> String -> ProcessM () +logS sph ll txt = do lc <- getLogConfig + when (filtered lc) + (sendMsg lc) + where filtered cfg = logApplyFilter cfg sph ll + makeMsg cfg = do time <- liftIO $ getCurrentTime + pid <- getSelfPid + return $ LogMessage time ll sph txt pid (logTarget cfg) + sendMsg cfg = + do msg <- makeMsg cfg + nid <- getSelfNode + res <- let svc = (adminGetPid nid ServiceLog) + in sendSimple svc msg PldAdmin + case res of + QteOK -> return () + n -> throw $ TransmitException $ QteLoggingError + +startLoggingService :: ProcessM () +startLoggingService = serviceThread ServiceLog logger + where logger = receiveWait [matchLogMessage,matchUnknownThrow] >> logger + matchLogMessage = match (\msg -> + do mylc <- getLogConfig + case msg of + (LogMessage _ _ _ _ _ LtForwarded) -> processMessage (logTarget mylc) msg + (LogMessage _ _ _ _ _ _) -> processMessage (targetPreference msg) msg + (LogUpdateConfig lc) -> setNodeLogConfig lc) + targetPreference (LogMessage _ _ _ _ _ a) = a + forwardify (LogMessage a b c d e _) = LogMessage a b c d e LtForwarded + processMessage whereto txt = + do smsg <- liftIO $ showLogMessage txt + case whereto of + LtStdout -> liftIO $ putStrLn smsg + LtFile fp -> (ptry (liftIO (appendFile fp (smsg ++ "\n"))) :: ProcessM (Either IOError ()) ) >> return () -- ignore error - what can we do? + LtForward nid -> do self <- getSelfNode + when (self /= nid) + (sendSimple (adminGetPid nid ServiceLog) (forwardify txt) PldAdmin >> return ()) -- ignore error -- what can we do? + n -> throw $ ConfigException $ "Invalid message forwarded setting" + + +---------------------------------------------- +-- * Global registry +---------------------------------------------- + +data ServiceId = ServiceGlobal + | ServiceLog + | ServiceSpawner + | ServiceNodeRegistry deriving (Ord,Eq,Enum,Show) + +adminGetPid :: NodeId -> ServiceId -> ProcessId +adminGetPid nid sid = ProcessId nid (fromEnum sid) + +adminDeregister :: ServiceId -> ProcessM () +adminDeregister val = do p <- getProcess + pid <- getSelfPid + liftIO $ modifyMVar_ (prNodeRef p) (fun $ localFromPid pid) + where fun pid node = return $ node {ndAdminProcessTable = Map.delete val (ndAdminProcessTable node)} + +adminRegister :: ServiceId -> ProcessM () +adminRegister val = do p <- getProcess + pid <- getSelfPid + node <- liftIO $ readMVar (prNodeRef p) + if Map.member val (ndAdminProcessTable node) + then throw $ ServiceException $ "Duplicate administrative registration at index " ++ show val + else liftIO $ modifyMVar_ (prNodeRef p) (fun $ localFromPid pid) + where fun pid node = return $ node {ndAdminProcessTable = Map.insert val pid (ndAdminProcessTable node)} + +adminLookup :: ServiceId -> ProcessM LocalProcessId +adminLookup val = do p <- getProcess + node <- liftIO $ readMVar (prNodeRef p) + case Map.lookup val (ndAdminProcessTable node) of + Nothing -> throw $ ServiceException $ "Request for unknown administrative service " ++ show val + Just x -> return x + +adminLookupN :: ServiceId -> MVar Node -> IO (Either TransmitStatus LocalProcessId) +adminLookupN val mnode = + do node <- readMVar mnode + case Map.lookup val (ndAdminProcessTable node) of + Nothing -> return $ Left QteUnknownPid + Just x -> return $ Right x + +setDaemonic :: ProcessM () +setDaemonic = do p <- getProcess + pid <- getSelfPid + liftIO $ modifyMVar_ (prNodeRef p) + (\node -> return $ node {ndProcessTable=Map.adjust (\pte -> pte {pteDaemonic=True}) (localFromPid pid) (ndProcessTable node)}) + +serviceThread :: ServiceId -> ProcessM () -> ProcessM () +serviceThread v f = spawnAnd (pbracket (return ()) + (\_ -> adminDeregister v >> logError) + (\_ -> f)) (adminRegister v >> setDaemonic) >> return () + where logError = logS "SYS" LoFatal $ "System process "++show v++" has terminated" -- TODO maybe restart? + +data AmSignal = AmSignal ProcessId SignalType SignalReason Bool deriving (Typeable) +instance Binary AmSignal where + put (AmSignal pid st sr p) = put pid >> put st >> put sr >> put p + get = do pid <- get + st <- get + sr <- get + p <- get + return $ AmSignal pid st sr p + +data AmMonitor = AmMonitor SignalType MonitorAction ProcessId ProcessId Bool deriving(Typeable,Show) +instance Binary AmMonitor where + put (AmMonitor st ma pid1 pid2 p) = put st >> put ma >> put pid1 >> put pid2 >> put p + get = do st <- get + ma <- get + pid1 <- get + pid2 <- get + p <- get + return $ AmMonitor st ma pid1 pid2 p + +data SignalType = SigProcessUp + | SigProcessDown + | SigUser String deriving (Typeable,Eq,Show) +instance Binary SignalType where + put SigProcessUp = putWord8 0 + put SigProcessDown = putWord8 1 + put (SigUser n) = putWord8 2 >> put n + get = getWord8 >>= \x -> + case x of + 0 -> return SigProcessUp + 1 -> return SigProcessDown + 2 -> get >>= \q -> return $ SigUser q + +data MonitorAction = MaMonitor + | MaLink + | MaLinkError deriving (Typeable,Show) +instance Binary MonitorAction where + put MaMonitor = putWord8 0 + put MaLink = putWord8 1 + put MaLinkError = putWord8 2 + get = getWord8 >>= \x -> + case x of + 0 -> return MaMonitor + 1 -> return MaLink + 2 -> return MaLinkError + +data SignalReason = SrNormal | SrException String deriving (Typeable,Data,Show) +instance Binary SignalReason where + put SrNormal = putWord8 0 + put (SrException s) = putWord8 1 >> put s + get = do a <- getWord8 + case a of + 0 -> return SrNormal + 1 -> get >>= return . SrException + +data AmSpawn = AmSpawn (Closure (ProcessM ())) AmSpawnOptions deriving (Typeable) +instance Binary AmSpawn where + put (AmSpawn c o) =put c >> put o + get=get >>= \c -> get >>= \o -> return $ AmSpawn c o + +data AmSpawnOptions = AmSpawnNormal | AmSpawnPaused | AmSpawnMonitor (ProcessId,SignalType,MonitorAction) | AmSpawnLinked ProcessId +instance Binary AmSpawnOptions where + put AmSpawnNormal = putWord8 0 + put AmSpawnPaused = putWord8 1 + put (AmSpawnMonitor a) = putWord8 2 >> put a + put (AmSpawnLinked a) = putWord8 3 >> put a + get = getWord8 >>= + \w -> case w of + 0 -> return AmSpawnNormal + 1 -> return AmSpawnPaused + 2 -> get >>= \a -> return $ AmSpawnMonitor a + 3 -> get >>= \a -> return $ AmSpawnLinked a + +data ProcessMonitorMessage = ProcessMonitorMessage ProcessId SignalType SignalReason deriving (Typeable) +instance Binary ProcessMonitorMessage where + put (ProcessMonitorMessage pid st sr) = put pid >> put st >> put sr + get = do pid <- get + st <- get + sr <- get + return $ ProcessMonitorMessage pid st sr + +data AmSpawnUnpause = AmSpawnUnpause deriving (Typeable) +instance Binary AmSpawnUnpause where + put AmSpawnUnpause = return () + get = return AmSpawnUnpause + +spawnRemote :: NodeId -> Closure (ProcessM ()) -> ProcessM ProcessId +spawnRemote node clo = spawnRemoteAnd node clo AmSpawnNormal + +spawnRemoteUnpause :: ProcessId -> ProcessM () +spawnRemoteUnpause pid = send pid AmSpawnUnpause + +spawnRemoteAnd :: NodeId -> Closure (ProcessM ()) -> AmSpawnOptions -> ProcessM ProcessId +spawnRemoteAnd node clo opt = + do res <- roundtripQuery PldAdmin (adminGetPid node ServiceSpawner) (AmSpawn clo opt) + case res of + Left e -> throw $ TransmitException e + Right pid -> return pid + +startSpawnerService :: ProcessM () +startSpawnerService = serviceThread ServiceSpawner spawner + where spawner = receiveWait [matchSpawnRequest,matchUnknownThrow] >> spawner + matchSpawnRequest = roundtripResponse PldAdmin + (\(AmSpawn c opt) -> + case opt of + AmSpawnNormal -> do pid <- spawn (spawnWorker c) + return (pid,()) + AmSpawnPaused -> do pid <- spawn ((receiveWait [match (\AmSpawnUnpause -> return ())]) >> spawnWorker c) + return (pid,()) + AmSpawnMonitor (monitorpid,typ,ac) -> + do pid <- spawnAnd (spawnWorker c) + (getSelfPid >>= \pid -> monitorProcess monitorpid pid typ ac) -- TODO this needs to be wrapped in case of error + return (pid,()) + AmSpawnLinked monitorpid -> + do pid <- spawnAnd (spawnWorker c) + (linkProcess monitorpid) + return (pid,())) + spawnWorker c = do a <- invokeClosure c + case a of + Nothing -> (logS "SYS" LoCritical $ "Failed to invoke closure "++(show c)) --TODO it would be nice if this error could be propagated to the caller of spawnRemote, at the very least it should throw an exception so a linked process will be notified + Just q -> q + +linkProcess :: ProcessId -> ProcessM () +linkProcess p = do me <- getSelfPid + monitorProcess me p SigProcessDown MaLinkError + monitorProcess p me SigProcessDown MaLinkError + +monitorProcess :: ProcessId -> ProcessId -> SignalType -> MonitorAction -> ProcessM () +monitorProcess monitor monitee which how = + let msg = (AmMonitor which how monitor monitee True) in + do islocal <- isPidLocal monitor + res <- roundtripQuery PldAdmin (adminGetPid (nodeFromPid (if islocal then monitor else monitee)) ServiceGlobal) msg + case res of + Right True -> return () + Right False -> throw $ TransmitException $ QteOther $ "Unknown error setting monitor in monitorProcess" + Left t -> throw $ TransmitException $ t + +data GlobalProcessEntry = GlobalProcessEntry + { + gpeMonitoring :: [(ProcessId,SignalType,MonitorAction)], + gpeMonitoredBy :: [(ProcessId,SignalType)] + } deriving (Show) + +type GlobalProcessRegistry = Map.Map LocalProcessId GlobalProcessEntry + +data Global = Global + { + processRegistry :: GlobalProcessRegistry + } deriving (Show) + +startGlobalService :: ProcessM () +startGlobalService = serviceThread ServiceGlobal (service emptyGlobal) + where + emptyGlobal = Global {processRegistry=Map.empty} + service global = + let + matchMonitor2 = matchCoreHeaderless PldAdmin (const True) + (\a -> case a of + (AmMonitor sigtype how monitor monitee propogate) -> setMonitor sigtype how monitor monitee propogate >>= return . snd) + + matchMonitor = roundtripResponse PldAdmin + (\a -> case a of + (AmMonitor sigtype how monitor monitee propogate) -> setMonitor sigtype how monitor monitee propogate) + matchSignal = + matchCoreHeaderless PldAdmin (const True) + (\a -> case a of + (AmSignal pid sigtype why propogate) -> processSignal pid sigtype why propogate) + getAllPids g = concat $ map (\x -> map (\(p,_) -> p) (gpeMonitoredBy x)) (Map.elems (processRegistry g)) + getInterestedPids g pid = + case Map.lookup (localFromPid pid) (processRegistry g) of + Nothing -> [] + Just gpe -> map (\(pp,_,_) -> pp) (gpeMonitoring gpe) ++ map (\(pp,_) -> pp) (gpeMonitoredBy gpe) + installMonitee (bb,g) pid f@(monitor,sigtype) = + let adjustor (Just (GlobalProcessEntry a b)) = Just $ GlobalProcessEntry a (adjustor1 b) + adjustor Nothing = adjustor (Just $ GlobalProcessEntry [] []) + adjustor1 [] = [f] + adjustor1 (q@(hmon,hsig):r) = if hmon==monitor && hsig==sigtype + then q:r + else q:(adjustor1 r) + in (bb && ( Map.member (localFromPid pid) (processRegistry g)),g {processRegistry = Map.alter adjustor (localFromPid pid) (processRegistry g)}) + installMonitor (bb,g) pid f@(monitee,sigtype,how) = + let adjustor (Just (GlobalProcessEntry a b)) = Just $ GlobalProcessEntry (adjustor1 a) b + adjustor Nothing = adjustor (Just $ GlobalProcessEntry [] []) + adjustor1 [] = [f] + adjustor1 (q@(hmon,hsig,how):r) = if hmon==monitee && hsig==sigtype + then f:r -- or throw + else q:(adjustor1 r) + in (bb && ( Map.member (localFromPid pid) (processRegistry g)), g {processRegistry = Map.alter adjustor (localFromPid pid) (processRegistry g)}) + + setMonitor sigtype how monitor monitee False = + do isMonitorLocal <- isPidLocal monitor + isMoniteeLocal <- isPidLocal monitee + if (not (isMonitorLocal || isMoniteeLocal)) + then return (False,global) + else let t1 = if isMonitorLocal + then installMonitor (True,global) monitor (monitee,sigtype,how) + else (True,global) + t2 = if isMoniteeLocal + then installMonitee t1 monitee (monitor,sigtype) + else t1 + in return t2 + setMonitor sigtype how monitor monitee True = + do (okay1,newg) <- setMonitor sigtype how monitor monitee False + isMonitorLocal <- isPidLocal monitor + isMoniteeLocal <- isPidLocal monitee + if okay1 + then if (not isMonitorLocal || not isMoniteeLocal) + then let othernode = if isMonitorLocal + then nodeFromPid monitee + else nodeFromPid monitor + other = adminGetPid othernode ServiceGlobal + in do res <- roundtripQuery PldAdmin other (AmMonitor sigtype how monitor monitee False) + case res of + Right True -> return (True,newg) + _ -> return (False,global) + else return (okay1,newg) + else return (False,global) + processSignal :: ProcessId -> SignalType -> SignalReason -> Bool -> ProcessM Global + processSignal pid SigProcessUp why False = + do notify pid SigProcessUp why + return $ global { processRegistry = Map.insert (localFromPid pid) (GlobalProcessEntry {gpeMonitoring=[],gpeMonitoredBy=[]}) (processRegistry global) } + processSignal pid SigProcessDown why False = let + removeStuff :: GlobalProcessRegistry -> GlobalProcessRegistry + removeStuff a = Map.map takeOut (Map.delete (localFromPid pid) a) + takeOut q = q {gpeMonitoring = filter (\(x,_,_) -> x /= pid) (gpeMonitoring q), + gpeMonitoredBy = filter (\(x,_) -> x /= pid) (gpeMonitoredBy q)} + in + do notify pid SigProcessDown why + return $ global {processRegistry = removeStuff $ processRegistry global} + processSignal pid sigtype why False = + notify pid sigtype why >> return global + processSignal pid sigtype why True = + let + sign = (AmSignal pid sigtype why False) + pids = if sigtype == SigProcessUp -- special case for process up: monitoring any process on a node will get signals for all new processups on that noe + then getAllPids global + else getInterestedPids global pid + tellwho = filter ((/=) (nodeFromPid pid)) (nub $ map (nodeFromPid) $ pids) + in do + q <- processSignal pid sigtype why False + res <- broadcast sign tellwho -- TODO handle error in some way + return q + monitors x pid = filter (\(p,_,_) -> pid==p) (gpeMonitoring x) + + -- TODO in broadcast, make this concurrent, and log bad results; THIS LINE NEEDS A TIMEOUT OR SOMETHING; get timout value from config, or, even better, integrate directly into roundtripQuery variant + broadcast msg towho = foldM (\p x -> do query <- ptimeout 500000 (roundtripQuery PldAdmin (adminGetPid x ServiceGlobal) msg) :: ProcessM (Maybe (Either TransmitStatus Bool)) + case query of + Nothing -> return False + Just (Left _) -> return False --log error + Just (Right r) -> return r) True towho + + sendNotification towho aboutwho typ how reason = + let + abnormal = case reason of + SrNormal -> False + _ -> True + sendAsynchException = + do p <- getProcess + node <- liftIO $ readMVar (prNodeRef p) + case Map.lookup towho (ndProcessTable node) of + Nothing -> logS "SYS" LoInformation $ "Process linked from " ++ show aboutwho ++ " can't be notified because it's already gone" + Just pte -> liftIO $ throwTo (pteThread pte) + (ProcessMonitorException aboutwho typ reason) + in + case how of + MaLinkError -> when (abnormal) sendAsynchException + MaLink -> sendAsynchException + MaMonitor -> do + towhom <- localServiceToPid towho + res <- sendSimple towhom (ProcessMonitorMessage aboutwho typ reason) PldUser + case res of + QteOK -> return () + o -> logS "SYS" LoInformation $ "Process "++show towhom++" monitoring " ++ show aboutwho ++ " can't be notified: "++show o + notify pid signal reason = + mapM_ (\(localpid,x) -> mapM_ (\(_,typ,how) -> sendNotification localpid pid typ how reason) (monitors x pid) ) (Map.toList (processRegistry global)) + doit = do q <- receiveWait [matchSignal,matchMonitor,matchMonitor2,matchUnknownThrow] +-- liftIO $ print q + service q + in doit >> return () + +---------------------------------------------- +-- * Local registry +---------------------------------------------- + +localRegistryMagicProcess :: LocalProcessId +localRegistryMagicProcess = 38813 + +localRegistryMagicMagic :: String +localRegistryMagicMagic = "__LocalRegistry" + +localRegistryMagicRole :: String +localRegistryMagicRole = "__LocalRegistry" + +type LocalProcessName = String + +type PeerInfo = Map.Map String [NodeId] + +data LocalNodeData = LocalNodeData {ldmRoles :: PeerInfo} + +type RegistryData = Map.Map String LocalNodeData + +data LocalProcessMessage = + LocalNodeRegister String String NodeId + | LocalNodeUnregister String String NodeId + | LocalNodeQuery String + | LocalNodeAnswer PeerInfo + | LocalNodeResponseOK + | LocalNodeResponseError String + | LocalNodeHello + deriving (Typeable) + +instance Binary LocalProcessMessage where + put (LocalNodeRegister m r n) = putWord8 0 >> put m >> put r >> put n + put (LocalNodeUnregister m r n) = putWord8 1 >> put m >> put r >> put n + put (LocalNodeQuery r) = putWord8 3 >> put r + put (LocalNodeAnswer pi) = putWord8 4 >> put pi + put (LocalNodeResponseOK) = putWord8 5 + put (LocalNodeResponseError s) = putWord8 6 >> put s + put (LocalNodeHello) = putWord8 7 + get = do g <- getWord8 + case g of + 0 -> get >>= \m -> get >>= \r -> get >>= \n -> return (LocalNodeRegister m r n) + 1 -> get >>= \m -> get >>= \r -> get >>= \n -> return (LocalNodeUnregister m r n) + 3 -> get >>= return . LocalNodeQuery + 4 -> get >>= return . LocalNodeAnswer + 5 -> return LocalNodeResponseOK + 6 -> get >>= return . LocalNodeResponseError + 7 -> return LocalNodeHello + +remoteRegistryPid :: NodeId -> ProcessM ProcessId +remoteRegistryPid nid = + do let (NodeId hostname _) = nid + cfg <- getConfig + return $ adminGetPid (NodeId hostname (cfgLocalRegistryListenPort cfg)) ServiceNodeRegistry + +localRegistryPid :: ProcessM ProcessId +localRegistryPid = + do (NodeId hostname _) <- getSelfNode + cfg <- getConfig + return $ adminGetPid (NodeId hostname (cfgLocalRegistryListenPort cfg)) ServiceNodeRegistry + +standaloneLocalRegistry :: String -> IO () +standaloneLocalRegistry cfgn = do cfg <- readConfig True (Just cfgn) + res <- startLocalRegistry cfg True + liftIO $ putStrLn $ "Terminating standalone local registry: " ++ show res + +localRegistryRegisterNode :: ProcessM () +localRegistryRegisterNode = localRegistryRegisterNodeImpl LocalNodeRegister + +localRegistryUnregisterNode :: ProcessM () +localRegistryUnregisterNode = localRegistryRegisterNodeImpl LocalNodeUnregister + +localRegistryRegisterNodeImpl :: (String -> String -> NodeId -> LocalProcessMessage) -> ProcessM () +localRegistryRegisterNodeImpl cons = + do cfg <- getConfig + lrpid <- localRegistryPid + nid <- getSelfNode + let regMsg = cons (cfgNetworkMagic cfg) (cfgRole cfg) nid + res <- roundtripQuery PldAdmin lrpid regMsg + case res of + Left ts -> throw $ TransmitException ts + Right LocalNodeResponseOK -> return () + Right (LocalNodeResponseError s) -> throw $ TransmitException $ QteOther s + +localRegistryHello :: ProcessM () +localRegistryHello = do lrpid <- localRegistryPid + -- wait five seconds + res <- ptimeout 5000000 $ roundtripQuery PldAdmin lrpid LocalNodeHello + case res of + Just (Right LocalNodeHello) -> return () + Just (Left n) -> throw $ ConfigException $ "Can't talk to local node registry: " ++ show n + _ -> throw $ ConfigException $ "No response from local node registry" + +localRegistryQueryNodes :: NodeId -> ProcessM (Maybe PeerInfo) +localRegistryQueryNodes nid = + do cfg <- getConfig + lrpid <- remoteRegistryPid nid + let regMsg = LocalNodeQuery (cfgNetworkMagic cfg) + res <- roundtripQuery PldAdmin lrpid regMsg + case res of + Left ts -> return Nothing + Right (LocalNodeAnswer pi) -> return $ Just pi + +-- TODO since local registries are potentially sticky, there is good reason +-- to ensure that they don't get corrupted; there should be some +-- kind of security precaution here, e.g. making sure that registrations +-- only come from local nodes +startLocalRegistry :: Config -> Bool -> IO TransmitStatus +startLocalRegistry cfg waitforever = startit + where + regConfig = cfg {cfgListenPort = cfgLocalRegistryListenPort cfg, cfgNetworkMagic=localRegistryMagicMagic, cfgRole = localRegistryMagicRole} + handler tbl = receiveWait [roundtripResponse PldAdmin (registryCommand tbl)] >>= handler + emptyNodeData = LocalNodeData {ldmRoles = Map.empty} + lookup tbl magic role = case Map.lookup magic tbl of + Nothing -> Nothing + Just ldm -> Map.lookup role (ldmRoles ldm) + remove tbl magic role nid = + let + roler Nothing = Nothing + roler (Just lst) = Just $ filter ((/=)nid) lst + removeFrom ldm = ldm {ldmRoles = Map.alter (roler) role (ldmRoles ldm)} + remover Nothing = Nothing + remover (Just ldm) = Just (removeFrom ldm) + in Map.alter remover magic tbl + insert tbl magic role nid = + let + roler Nothing = Just [nid] + roler (Just lst) = if elem nid lst + then (Just lst) + else (Just (nid:lst)) + insertTo ldm = ldm {ldmRoles = Map.alter (roler) role (ldmRoles ldm)} + + inserter Nothing = Just (insertTo emptyNodeData) + inserter (Just ldm) = Just (insertTo ldm) + in Map.alter inserter magic tbl + registryCommand tbl (LocalNodeQuery magic) = + case Map.lookup magic tbl of + Nothing -> return (LocalNodeAnswer Map.empty,tbl) + Just pi -> return (LocalNodeAnswer (ldmRoles pi),tbl) + registryCommand tbl (LocalNodeUnregister magic role nid) = -- check that given entry exists? + return (LocalNodeResponseOK,remove tbl magic role nid) + registryCommand tbl (LocalNodeRegister magic role nid) = + case lookup tbl magic role of + Nothing -> return (LocalNodeResponseOK,insert tbl magic role nid) + Just nids -> if elem nid nids + then return (LocalNodeResponseError ("Multiple node registration for " ++ show nid ++ " as " ++ role),tbl) + else return (LocalNodeResponseOK,insert tbl magic role nid) + registryCommand tbl (LocalNodeHello) = return (LocalNodeHello,tbl) + registryCommand tbl _ = return (LocalNodeResponseError "Unknown command",tbl) + startit = do node <- initNode regConfig Remote.Call.empty + res <- try $ forkAndListenAndDeliver node regConfig :: IO (Either SomeException ()) + case res of + Left e -> return $ QteNetworkError $ show e + Right () -> runLocalProcess node (startLoggingService >> + adminRegister ServiceNodeRegistry >> + handler Map.empty) >> + if waitforever + then waitForThreads node >> + (return $ QteOther "Local registry process terminated") + else return QteOK + + diff --git a/Test-Channel-Global.hs b/Test-Channel-Global.hs new file mode 100644 index 0000000..adf025e --- /dev/null +++ b/Test-Channel-Global.hs @@ -0,0 +1,64 @@ +{-# LANGUAGE TemplateHaskell,DeriveDataTypeable #-} +module Main where + +import Remote.Call +import Remote.Process +import Remote.Channel +import Remote.Encoding +import Remote.Peer +import Remote.Init + +import Data.Binary (Binary,get,put) +import Data.Char (isUpper) +import Data.Generics (Data) +import Data.Typeable (Typeable) +import Prelude hiding (catch) +import Data.Typeable (typeOf) +import System.IO +import Control.Monad.Trans +import Control.Exception +import Control.Monad +import Data.Maybe (fromJust) +import Control.Concurrent + +remoteCall + [d| + + stuff :: Int -> String -> [ProcessId] -> ProcessM String + stuff a b c = return $ show a ++ show b ++ show c + + task :: ProcessM () + task = do mypid <- getSelfPid + say $ "I am " ++ show mypid + liftIO $ threadDelay 50000 + receiveWait [match (\chan -> mapM_ (sendChannel chan) (reverse ([1..50]::[Int])))] + + |] + +initialProcess "SLAVE" = do + receiveWait [] + +initialProcess "MASTER" = do + mypid <- getSelfPid + + (sendchan,recvchan) <- makeChannel + peers <- getPeersDynamic 50000 + say ("Got peers: " ++ show peers) + let slaves = findPeerByRole peers "SLAVE" + + -- The following hack shows the need for an improved "local process registry" + mapM_ (\x -> do + newpid <- spawnRemote x task__closure + send newpid sendchan + ) slaves + liftIO $ threadDelay 500000 + + mapM_ (\_ -> do v <- receiveChannel recvchan + say $ show (v::Int)) [1..50] + + +main = remoteInit "config" [Main.__remoteCallMetaData] initialProcess + + + + diff --git a/Test-Channel-Merge.hs b/Test-Channel-Merge.hs new file mode 100644 index 0000000..672ab24 --- /dev/null +++ b/Test-Channel-Merge.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE TemplateHaskell,DeriveDataTypeable #-} +module Main where + +import Remote.Call +import Remote.Process +import Remote.Channel +import Remote.Encoding +import Remote.Peer +import Remote.Init + +import Data.Binary (Binary,get,put) +import Data.Char (isUpper) +import Data.Generics (Data) +import Data.Typeable (Typeable) +import Prelude hiding (catch) +import Data.Typeable (typeOf) +import System.IO +import Control.Monad.Trans +import Control.Exception +import Control.Monad +import Data.Maybe (fromJust) +import Control.Concurrent + +remoteCall + [d| + + stuff :: Int -> String -> [ProcessId] -> ProcessM String + stuff a b c = return $ show a ++ show b ++ show c + + task :: ProcessM () + task = do mypid <- getSelfPid + say $ "I am " ++ show mypid + liftIO $ threadDelay 50000 + receiveWait [match (\chan -> mapM_ (sendChannel chan) (reverse ([1..50]::[Int])))] + + |] + +channelCombiner cfg = case cfgArgs cfg of + ["biased"] -> combineChannelsBiased + ["rr"] -> combineChannelsRR + _ -> error "Please specify 'biased' or 'rr' on the command line" + +initialProcess "NODE" = do + mypid <- getSelfPid + cfg <- getConfig + + (sendchan,recvchan) <- makeChannel + (sendchan2,recvchan2) <- makeChannel + + spawn $ mapM_ (sendChannel sendchan) [1..(26::Int)] + spawn $ mapM_ (sendChannel sendchan2) ['A'..'Z'] + + merged <- (channelCombiner cfg) [combinedChannelAction recvchan show,combinedChannelAction recvchan2 show] + let go = do item <- receiveChannel merged + say $ "Got: " ++ show item + go + go + +main = remoteInit "config" [Main.__remoteCallMetaData] initialProcess + + + + diff --git a/Test-Channel.hs b/Test-Channel.hs new file mode 100644 index 0000000..8b9ed5d --- /dev/null +++ b/Test-Channel.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE TemplateHaskell,DeriveDataTypeable #-} +module Main where + +import Remote.Call (remoteCall,registerCalls,empty) +import Remote.Process +import Remote.Encoding +import Remote.Channel + +import Data.Binary (Binary,get,put) +import Data.Char (isUpper) +import Data.Generics (Data) +import Data.Typeable (Typeable) +import Prelude hiding (catch) +import Data.Typeable (typeOf) +import Control.Monad.Trans +import Control.Exception +import Control.Monad +import Data.Maybe (fromJust) +import Control.Concurrent + +initialProcess "NODE" = do + startGlobalService + + (sendchan,recvchan) <- makeChannel + + a <- spawn $ do + sendChannel sendchan "hi" + sendChannel sendchan "lumpy" + liftIO $ threadDelay 1000000 + sendChannel sendchan "spatula" + sendChannel sendchan "noodle" + mapM_ (sendChannel sendchan) (map show [1..1000]) + liftIO $ threadDelay 500000 + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + receiveChannel recvchan >>= liftIO . print + + + +testSend = do + lookup <- return empty + cfg <- readConfig True (Just "config") + node <- initNode cfg lookup + startLocalRegistry cfg False + forkAndListenAndDeliver node cfg + roleDispatch node initialProcess + waitForThreads node + threadDelay 500000 + +main = testSend + + diff --git a/Test-Dialog.hs b/Test-Dialog.hs new file mode 100644 index 0000000..4d6a9db --- /dev/null +++ b/Test-Dialog.hs @@ -0,0 +1,59 @@ +{-# LANGUAGE TemplateHaskell,DeriveDataTypeable #-} +module Main where + +import Remote.Call (remoteCall,registerCalls,empty,Serializable) +import Remote.Process +import Remote.Encoding + +import Data.Binary (Binary,get,put) +import Data.Char (isUpper) +import Data.Generics (Data) +import Data.Typeable (Typeable) +import Prelude hiding (catch) +import Data.Typeable (typeOf) +import Control.Monad.Trans +import Control.Exception +import Control.Monad +import Data.Maybe (fromJust) +import Control.Concurrent +import Control.Concurrent.Chan + +remoteCall [d| + myPutStrLn=putStrLn + |] + + +while :: (Monad m) => m Bool -> m () +while a = do f <- a + when (f) + (while a >> return ()) + return () + + +receiveWait2 m = do a <- receiveTimeout 1000000 m + return $ fromJust a + +initialProcess "NODE" = do + a <- spawn $ while $ receiveWait2 [roundtripResponse PldUser (\x -> if x == "halt" then return ("halted",False) else return (reverse x,True))] + + mapM_ (doit a) ["hi there","spatula","halt","noreturn"] + +doit pid s = do + res <- roundtripQuery PldUser pid s + liftIO $ print (res :: Either TransmitStatus String) + liftIO $ threadDelay 900000 + send pid ["fuck"] + +testSend = do + lookup <- registerCalls [Main.__remoteCallMetaData] + cfg <- readConfig True (Just "config") + node <- initNode cfg lookup + startLocalRegistry cfg False + forkAndListenAndDeliver node cfg PldUser + roleDispatch node initialProcess + waitForThreads node + threadDelay 400000 + +main = testSend + + diff --git a/Test-Discover.hs b/Test-Discover.hs new file mode 100644 index 0000000..cc0638e --- /dev/null +++ b/Test-Discover.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE DeriveDataTypeable #-} +import Remote.Call (remoteCall,registerCalls,empty,Serializable) +import Remote.Process +import Remote.Encoding +import Remote.Channel +import Control.Concurrent +import Control.Exception +import Network hiding (recvFrom,sendTo) +import Network.Socket hiding (send,accept) +import System.IO (hClose) +import Data.Typeable +import Control.Monad.Trans +import Control.Monad +import Remote.Peer +import Debug.Trace + +initialProcess "SLAVE" = spawn slaveProcess >> startDiscoveryService + where slaveProcess = receiveWait [match (\s -> liftIO $ putStrLn ("I received a " ++ s))] >> slaveProcess + +initialProcess "MASTER" = do peers <- queryDiscovery 500000 + liftIO $ print "I found the following SLAVE nodes on this network:" + liftIO $ print $ getPeerByRole peers "SLAVE" + +initialProcess _ = liftIO $ putStrLn "Please start the test with flag -cfgRole=SLAVE or -cfgRole=MASTER" + +testSend = do + lookup <- return empty + cfg <- readConfig True (Just "config") + node <- initNode cfg lookup + startLocalRegistry cfg False + forkAndListenAndDeliver node cfg + roleDispatch node initialProcess + waitForThreads node + threadDelay 500000 + +main = testSend + + diff --git a/Test-Global.hs b/Test-Global.hs new file mode 100644 index 0000000..a9c8585 --- /dev/null +++ b/Test-Global.hs @@ -0,0 +1,75 @@ +{-# LANGUAGE TemplateHaskell,DeriveDataTypeable #-} +module Main where + +import Remote.Init +import Remote.Call +import Remote.Process +import Remote.Peer + +import Data.Generics (Data) +import Data.Maybe (fromJust) +import Data.Typeable (Typeable) +import Control.Monad (when,forever) +--import Control.Monad.Trans (liftIO) +import Data.Char (isUpper) +import Control.Concurrent (threadDelay) + +remoteCall [d| + + + sayHi :: ProcessId -> ProcessM () + sayHi s = do liftIO $ threadDelay 500000 + say $ "Hi there, " ++ show s ++ "!" + + shiftLetter :: Int -> Char -> Char + shiftLetter n c = let alphabet = if isUpper c then ['A'..'Z'] else ['a'..'z'] + letters = cycle alphabet + sletters = take (length alphabet) $ drop n letters + in maybe c id (lookup c (zip letters sletters)) + + + rot13 :: String -> String + rot13 = map (shiftLetter 13) + |] + +while :: (Monad m) => m Bool -> m () +while a = do f <- a + when (f) + (while a >> return ()) + return () + +initialProcess "MASTER" = do + mypid <- getSelfPid + mynode <- getSelfNode + cfg <- getConfig + + say $ "I am " ++ show mypid ++ " -- " ++ show (cfgArgs cfg) + + peers <- getPeersStatic + let slaves = findPeerByRole peers "SLAVE" + say $ "Found these slave nodes: " ++ show slaves + + say "Making slaves say Hi to me" + mapM_ (\x -> setRemoteNodeLogConfig x (LogConfig LoTrivial (LtForward mynode) LfAll)) slaves + mapM_ (\x -> spawnRemoteAnd x (sayHi__closure mypid) (AmSpawnMonitor (mypid,SigProcessDown,MaMonitor))) slaves + + let matchMonitor = match (\(ProcessMonitorMessage aboutwho typ reason) -> say $ "Got completion messages: " ++ show (aboutwho,typ,reason)) + let waiting i = when (i>0) ((receiveWait [matchMonitor]) >> waiting (i-1)) + + say "Waiting for completion messages from slaves..." + waiting (length slaves) + + say "All slaves done, terminating master..." + + return () +initialProcess "SLAVE" = do + + receiveWait [] + + return () +initialProcess _ = liftIO $ putStrLn "Role must be MASTER or SLAVE" + +testSend = remoteInit "config" [Main.__remoteCallMetaData] initialProcess + +main = testSend + diff --git a/config b/config new file mode 100644 index 0000000..cd303fe --- /dev/null +++ b/config @@ -0,0 +1,4 @@ + # This is a config file + + cfgListenPort 0 + diff --git a/examples/kmeans1/KMeans.hs b/examples/kmeans1/KMeans.hs new file mode 100644 index 0000000..0ff4155 --- /dev/null +++ b/examples/kmeans1/KMeans.hs @@ -0,0 +1,207 @@ +{-# LANGUAGE TemplateHaskell,DeriveDataTypeable #-} +module Main where + +import Remote.Process +import Remote.Encoding +import Remote.Init +import Remote.Call +import Remote.Peer + +import System.Random (randomR,getStdRandom) +import Data.Typeable (Typeable) +import Data.Data (Data) +import Data.Binary (Binary,get,put) +import Data.Maybe (fromJust) +import Data.List (minimumBy,sortBy) +import Data.Either (rights) +import qualified Data.Map as Map +import System.IO +import Debug.Trace + +{- +maptask + reads in all clusters + and some subset of points + and for each point, determines which cluster it is nearest + +reducetask + reads in assignments of points to clusters -- all assignments to$ + generates new clusters + returns if points are converged +-} + + + +data Vector = Vector Double Double deriving (Show,Read,Typeable,Data,Eq) +instance Binary Vector where put = genericPut; get = genericGet + +data Cluster = Cluster + { + clId :: Int, + clCount :: Int, + clSum :: Vector + } deriving (Show,Read,Typeable,Data,Eq) +instance Binary Cluster where put = genericPut; get = genericGet + +clusterCenter :: Cluster -> Vector +clusterCenter cl = let (Vector a b) = clSum cl + in Vector (op a) (op b) + where op = (\d -> d / (fromIntegral $ clCount cl)) + +sqDistance :: Vector -> Vector -> Double +sqDistance (Vector x1 y1) (Vector x2 y2) = (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) + + +getPoints :: FilePath -> IO [Vector] +getPoints fp = do c <- readFile fp + return $ read c + +getClusters :: FilePath -> IO [Cluster] +getClusters fp = do c <- readFile fp + return $ read c + +split :: Int -> [a] -> [[a]] +split numChunks l = splitSize (length l `div` numChunks) l -- this might leave some off + where + splitSize _ [] = [] + splitSize i v = take i v : splitSize i (drop i v) + +broadcast :: (Serializable a) => [ProcessId] -> a -> ProcessM () +broadcast pids dat = mapM_ (\pid -> send pid dat) pids + +multiSpawn :: [NodeId] -> Closure (ProcessM ()) -> ProcessM [ProcessId] +multiSpawn nodes f = mapM (\node -> spawnRemote node f) nodes + +mapperProcess :: ProcessM () +mapperProcess = + let mapProcess :: (Maybe [Vector],Maybe [ProcessId]) -> + ProcessM (Maybe [Vector],Maybe [ProcessId]) + mapProcess (mvecs,mreducers) = + receiveWait + [ + match (\vec -> return (Just vec,mreducers)), + match (\reducers -> return (mvecs,Just reducers)), + roundtripResponse PldUser + (\clusters -> let tbl = analyze mvecs clusters + reducers = fromJust mreducers + target clust = reducers !! (clust `mod` length reducers) + sendout (clustid,pts) = send (target clustid) (Map.singleton clustid pts) + in mapM_ sendout (Map.toList tbl) + >> return ((),(mvecs,mreducers))), + matchUnknownThrow + ] >>= mapProcess + analyze (Just vectors) clusters = + let assignments = map (assignToCluster clusters) vectors + changetoLists l = map (\(id,pt) -> (id,[pt])) l + toMap l = Map.fromListWith (++) l + in toMap (changetoLists assignments) + assignToCluster clusters vector = + let distances = map (\x -> (clId x,sqDistance (clusterCenter x) vector)) clusters + in (fst (minimumBy (\(_,a) (_,b) -> compare a b) distances),vector) + doit = mapProcess (Nothing,Nothing) + in doit >> return () + +reducerProcess :: ProcessM () +reducerProcess = let reduceProcess :: (Maybe [ProcessId],Map.Map Int [Vector],Map.Map Int [Vector]) -> ProcessM (Maybe [ProcessId],Map.Map Int [Vector],Map.Map Int [Vector]) + reduceProcess (mmappers,oldclusters,clusters) = + receiveWait [match (\mappers -> return (Just mappers,oldclusters,clusters)), + roundtripResponse PldUser (\"" -> return (oldclusters,(mmappers,clusters,Map.empty))), + roundtripResponse PldUser (\() -> + let cl = toClusters (Map.toList clusters) + (Just mappers) = mmappers + in return (cl,(mmappers,clusters,Map.empty))), + match (\x -> return (mmappers,oldclusters,Map.unionWith (++) clusters x)), + matchUnknownThrow] >>= reduceProcess + toClusters cls = map (\(clsid,v) -> Cluster {clId=clsid,clCount=length v,clSum = addup v}) cls + addup v = let (a,b) = foldl (\(x1,y1) (Vector x2 y2) -> (x1+x2,y1+y2)) (0,0) v + in Vector a b + in reduceProcess (Nothing,Map.empty,Map.empty) >> return () + +$(remoteCall [d| + mapperProcessRemote :: ProcessM () + mapperProcessRemote = mapperProcess + + reducerProcessRemote :: ProcessM () + reducerProcessRemote = reducerProcess + |] ) + +readableShow [] = [] +readableShow ((_,one):rest) = (concat $ map (\(Vector x y) -> show x ++ " " ++ show y ++ "\n") one)++"\n\n"++readableShow rest + +initialProcess "MASTER" = + do peers <- getPeersDynamic 50000 + let mappers = findPeerByRole peers "MAPPER" + let reducers = findPeerByRole peers "REDUCER" + let numreducers = length reducers + let nummappers = length mappers + points <- liftIO $ getPoints "kmeans-points" + clusters <- liftIO $ getClusters "kmeans-clusters" + mypid <- getSelfPid + + mapperPids <- multiSpawn mappers mapperProcessRemote__closure + + reducerPids <- multiSpawn reducers reducerProcessRemote__closure + broadcast mapperPids reducerPids + broadcast reducerPids mapperPids + mapM_ (\(pid,chunk) -> send pid chunk) (zip (mapperPids) (split (length mapperPids) points)) + + let loop howmany clusters = do + roundtripQueryAsync PldUser mapperPids clusters :: ProcessM [Either TransmitStatus ()] + res <- roundtripQueryAsync PldUser reducerPids () :: ProcessM [Either TransmitStatus [Cluster]] + let newclusters = rights res + let newclusters2 = (sortBy (\a b -> compare (clId a) (clId b)) (concat newclusters)) + if newclusters2 == clusters + then do + liftIO $ putStrLn $ "------------------Converged in " ++ show howmany ++ " iterations" + liftIO $ print $ newclusters2 + pointmaps <- mapM (\pid -> do (Right m) <- roundtripQuery PldUser pid "" + return (m::Map.Map Int [Vector])) reducerPids + let pointmap = Map.unionsWith (++) pointmaps + liftIO $ writeFile "kmeans-converged" $ readableShow (Map.toList pointmap) + else + loop (howmany+1) newclusters2 + loop 0 clusters + +initialProcess "MAPPER" = receiveWait [] +initialProcess "REDUCER" = receiveWait [] +initialProcess _ = error "Role must be MAPPER or REDUCER or MASTER" + +makeData :: Int -> Int -> IO () +makeData n k = + let getrand = getStdRandom (randomR (1,1000)) + randvect = do a <- getrand + b <- getrand + return $ Vector a b + vects :: IO [Vector] + vects = sequence (replicate n randvect) + clusts :: IO [Cluster] + clusts = mapM (\clid -> do v <- randvect + return $ Cluster {clId = clid,clCount=1,clSum=v}) [1..k] + in do v <- vects + c <- clusts + writeFile "kmeans-points" (show v) + writeFile "kmeans-clusters" (show c) + + +makeMouse :: IO () +makeMouse = let getrand a b = getStdRandom (randomR (a,b)) + randvectr (x,y) r = do rr <- (getrand 0 r) + ra <- (getrand 0 (2*3.1415)) + let xo = cos ra * rr + let yo = sin ra * rr + return $ Vector (x+xo) (y+yo) + randvect (xmin,xmax) (ymin,ymax) = do x <- (getrand xmin xmax) + y <- (getrand ymin ymax) + return $ Vector x y + area count size (x,y) = sequence (replicate count (randvectr (x,y) size)) + clusts = mapM (\clid -> do v <- randvect (0,1000) (0,1000) + return $ Cluster {clId = clid,clCount=1,clSum=v}) [1..3] + in do ear1 <- area 200 100 (200,200) + ear2 <- area 450 100 (800,200) + face <- area 1000 350 (500,600) + c <- clusts + writeFile "kmeans-points" (show (ear1++ear2++face)) + writeFile "kmeans-clusters" (show c) + + +main = remoteInit "config" [Main.__remoteCallMetaData] initialProcess diff --git a/examples/kmeans1/kmeans b/examples/kmeans1/kmeans new file mode 100755 index 0000000..c19c57e --- /dev/null +++ b/examples/kmeans1/kmeans @@ -0,0 +1,26 @@ +#!/bin/bash + +nmappers=2 +nreducers=2 + +pids="" + +# ghc --make KMeans || exit 1 + +for i in {1..nmappers} +do + ./KMeans -cfgRole=MAPPER & + pid=$! + pids="$pids $pid" +done + +for i in {1..nreducers} +do + ./KMeans -cfgRole=REDUCER & + pid=$! + pids="$pids $pid" +done + +./KMeans -cfgRole=MASTER + +kill $pids diff --git a/examples/kmeans1/kmeans-viewer b/examples/kmeans1/kmeans-viewer new file mode 100755 index 0000000..425588c --- /dev/null +++ b/examples/kmeans1/kmeans-viewer @@ -0,0 +1,74 @@ +#!/usr/bin/python + +import pygtk +pygtk.require('2.0') +import gtk +import cairo +from gtk import gdk +import random +from math import pi +from sys import argv + +class ViewerWidget(gtk.DrawingArea): + __gsignals__ = {"expose-event":"override"} + def do_expose_event(self,event): + cr=self.window.cairo_create() + cr.rectangle(event.area.x,event.area.y,event.area.width,event.area.height) + cr.clip() + self.draw(cr,*self.window.get_size()) + def setdata(self,data): + self.data=data + def draw(self,cr,width,height): + cr.set_source_rgb(0.5,0.5,0.5) + cr.rectangle(0,0,width,height) + cr.fill() + for group in self.data: + cr.set_source_rgb(*group[0]) + for point in group[1:]: + #cr.move_to(point[0],point[1]) + cr.arc(point[0], point[1], 2, 0, 2 * pi) + cr.fill() + #cr.line_to(point[0]+0.5,point[1]+0.5) + #cr.stroke() + +class Viewer(object): + def destroy(self,widget): + gtk.main_quit() + def filename(self): + if len(argv)>1: + return argv[1] + else: + return "kmeans-converged" + def newcolor(self): + return (random.random(),random.random(),random.random()) + def getdata(self): + with open(self.filename()) as f: + groups=[] + group=[] + for line in f: + if len(group)==0: + group.append(self.newcolor()) + if len(line.split())==0: + if len(group) > 1: + groups.append(group) + group=[] + else: + words = line.split() + pts = [] + for word in words: + pts.append(float(word)) + group.append(pts) + return groups + def __init__(self): + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.connect("destroy",self.destroy) + self.window.set_default_size(900,900) + widget = ViewerWidget() + widget.setdata(self.getdata()) + widget.show() + self.window.add(widget) + self.window.show() + +if __name__ == "__main__": + viewer = Viewer() + gtk.main() diff --git a/examples/kmeans1/mouse.png b/examples/kmeans1/mouse.png new file mode 100644 index 0000000000000000000000000000000000000000..c2425f3e33a6239456966c2cc8750a536ba3ce93 GIT binary patch literal 112938 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%0A*XoLmszjxa$ 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 Integer -> Ratio Integer +halton i base = h i (1/fromIntegral base) + where + h :: Integer -> Ratio Integer -> Ratio Integer + h 0 _ = 0 + h i half = let digit = i `mod` base + in (h (floor(fromIntegral(i-digit)/fromIntegral base)) (half/fromIntegral base)) + fromIntegral digit*half + +-- Gives a pair of coordinates based on Halton in base 2 and 3 +haltonPair :: Integer -> (Ratio Integer,Ratio Integer) +haltonPair i = (halton i 2,halton i 3) + +-- Given a starting index and a count, returns the number of values +-- in that area of the Halton sequence that fall within and without +-- the unit circle. (Note that because of my bad implementation +-- of the Halton sequence, we end up computing the whole sequence +-- from the beginning even if we only use higher indices. Result: slow.) +getSums count offset = let outof = foldl' (\count coord -> count + if outCircle coord then 1 else 0) 0 sequence + inof = count-outof + in (inof,outof) + where sequence = [ haltonPair i | i <- [offset..count+offset] ] + outCircle (x,y) = let fx=x-0.5 + fy=y-0.5 + in fx*fx + fy*fy > 0.25 + +-- We do computation using ratios of arbitrary-precision integers. Here +-- we divide them and get output of a big string as a result. +longdiv :: Integer -> Integer -> Integer -> String +longdiv _ 0 _ = "" +longdiv numer denom places = let attempt = numer `div` denom in + if places==0 + then "" + else shows attempt (longdiv2 (numer - attempt*denom) denom (places -1)) + where longdiv2 numer denom places | numer `rem` denom == 0 = "0" + | otherwise = longdiv (numer * 10) denom places + +-- * THE INTERESTING PART OF THE PROGRAM + +-- Code that will be called by a closure on a remote system needs to be enclosed +-- in a remoteCall block to generate appropriate metadata. In particular, this +-- block will, in addition to the mapper function below, generate a mapper__closure +-- function which returns a closure of a call to mapper. In both cases these +-- functions take a count and index of a Halton sequence to analyze, and a PID +-- of a Haskell process to send the results to. In the code below, the 'send' +-- function is the main message-sending primitive of my library. +$(remoteCall [d| + mapper :: Integer -> Integer -> ProcessId -> ProcessM () + mapper count offset master = let (numin,numout) = getSums count offset + in send master (numin::Integer,numout::Integer) + |]) + +-- A "node" in this system probably corresponds to a computer. Each node +-- is assigned a role (by its config file) and based on its role will take +-- different action at startup. In this program, we distinguish two roles: +-- multiple MAPPER nodes, and a single REDUCER node. Here, we show the +-- initial action of MAPPER nodes, which is to do nothing at all until +-- they are told otherwise. +initialProcess "MAPPER" = do + receiveWait [] + +-- And here is the code for the REDUCER node. Basically: it find all nearby +-- MAPPER nodes, and gives them each a chunk of the Halton sequence to look, +-- then waits for them to send back their responses, adds up the results, +-- prints an approximation of pi, and ends. +initialProcess "REDUCER" = do + + -- This gives us a list of all nearby MAPPER nodes, discovered + -- with a UDP broadcast. + peers <- queryDiscovery 50000 + let slaves = getPeerByRole peers "MAPPER" + + -- Here, interval is the number of number of samples to be + -- processed by each mapper node. + let interval = 10000 + + -- mypid is now the process identifier of this, the main + -- process running on the REDUCER node. A PID contains the + -- the name of the host the process is running on and so + -- should uniquely identify a thread in a network of + -- multiple nodes. + mypid <- getSelfPid + say "Starting..." + + -- On each slave, spawn a new process that invoke the mapper function + -- (given above), giving it as arguments the starting index and number + -- of samples to process, as well as the process ID of the REDUCER process. + -- 'spawnRemote' is my library's way of starting a new process on a node, + -- and mapper__closure is the automagically generated closure function + -- for mapper. + mapM_ (\(offset,nid) -> + do say $ "Telling slave " ++ show nid ++ " to look at range " ++ show offset ++ ".." ++ show (offset+interval-1) + spawnRemote nid (mapper__closure (interval-1) offset mypid)) (zip [0,interval..] slaves) + + -- Wait for all the MAPPERs to respond. + (x,y) <- receiveLoop (0,0) (length slaves) + + -- Given the number of samples in the unit circle and number out, + -- estimate pi. + let est = estimatePi x y + say $ "Done: " ++ longdiv (numerator est) (denominator est) 100 + where estimatePi ni no | ni+no==0 = 0 + | otherwise = (4 * ni) % (ni+no) + receiveLoop a 0 = return a + receiveLoop (numIn,numOut) n = + let + resultMatch = match (\(x,y) -> return (x::Integer,y::Integer)) + in do (newin,newout) <- receiveWait [resultMatch] + -- receiveWait is my library's primitive function to wait + -- for a message of a particular type. In this case, we're + -- waiting for a tuple of integers sent by mapper, containing the + -- the number of points in a circle and out of it. + let x = numIn + newin + let y = numOut + newout + receiveLoop (x,y) (n-1) + +initialProcess _ = error "Role must be MAPPER or REDUCER" + +-- Entry point. Reads config file, starts node, invokes initialProcess (given above). +main = remoteInit "config" [Main.__remoteCallMetaData] initialProcess + + +{- Sample run, with three MAPPERs: + +2011-01-27 19:00:36.324802 UTC 0 pid://velikan:58565/6/ SAY Starting... +2011-01-27 19:00:36.325401 UTC 0 pid://velikan:58565/6/ SAY Telling slave nid://velikan:48599/ to look at range 0..9999 +2011-01-27 19:00:36.327816 UTC 0 pid://velikan:58565/6/ SAY Telling slave nid://velikan:52089/ to look at range 10000..19999 +2011-01-27 19:00:36.3301 UTC 0 pid://velikan:58565/6/ SAY Telling slave nid://velikan:37615/ to look at range 20000..29999 +2011-01-27 19:00:37.595462 UTC 0 pid://velikan:58565/6/ SAY Done: 3141380804747141380804747141380804747141380804747141380804747141 + +-} diff --git a/examples/pi/Pi6.hs b/examples/pi/Pi6.hs new file mode 100644 index 0000000..22f6de6 --- /dev/null +++ b/examples/pi/Pi6.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE TemplateHaskell,BangPatterns #-} +module Main where + +import Remote.Call +import Remote.Peer +import Remote.Process +import Remote.Init + +import Data.Ratio +import Debug.Trace +import Control.Monad +import Prelude hiding (break) +import Data.List hiding (break) +import Data.IORef +import Data.Array.IO +import Data.Array +import Control.Monad.State + +type Number = Double + +data Seq = Seq {k::Int, x::Number, base::Int, q::Array Int Number, break::Bool} + +--shold maybe be Integer to allow for really big ranges +getSeq :: Int -> Int -> [Number] +getSeq offset thebase = let + digits = 64::Int + seqSetup :: Seq -> Int -> Number -> (Seq, Number) + seqSetup s j _ = + let dj = (k s `mod` base s) + news = s {k = (k s - dj) `div` fromIntegral (base s), x = x s + (fromIntegral dj * ((q s) ! (j+1)))} + in + (news,fromIntegral dj) + seqContinue :: Seq -> Int -> Number -> (Seq, Number) + seqContinue s j dj = + if break s + then (s,dj) + else + let newdj = dj+1 + newx = x s + (q s) ! (j+1) + in + if newdj < fromIntegral (base s) + then (s {x=newx,break=True},newdj) + else (s {x = newx - if j==0 then 1 else (q s) ! j},0) + + initialState base = let q = array (0,digits*2) [(i,v) | i <- [0..digits*2], let v = if i == 0 then 1 else ((q ! ((i)-1))/fromIntegral base)] + in Seq {k=fromIntegral offset,x=0,break=False,base=base,q=q} + theseq base = let + first :: (Int,[Number],Seq) + first = foldl' (\(n,li,s) _ -> let (news,r) = seqSetup s n 0 + in (n+1,r:li,news)) (0,[],initialState base) [0..digits] + second :: [Number] -> Seq -> (Int,[Number],Seq) + second d s = foldl' (\(n,li,s) dj -> let (news,r) = seqContinue s n dj + in (n+1,r:li,news)) (0,[],s {break=False}) d + in let (_,firstd,firsts) = first + therest1 :: [([Number],Seq)] + therest1 = iterate (\(d,s) -> let (_,newd,news) = second (reverse d) s in (newd,news)) (firstd,firsts) + therest :: [Number] + therest = map (\(_,s) -> x s) therest1 + in therest + in (theseq thebase) + +pairs :: Int -> [(Number,Number)] +pairs offset = zip (getSeq offset 2) (getSeq offset 3) + +countPairs :: Int -> Int -> (Int,Int) +countPairs offset count = let range = take count $ pairs offset + numout = foldl' (\i coord -> if outCircle coord then i+1 else i) 0 range + in (count-numout,numout) + where + outCircle (x,y) = let fx=x-0.5 + fy=y-0.5 + in fx*fx + fy*fy > 0.25 + +$(remoteCall [d| + mapper :: Int -> Int -> ProcessId -> ProcessM () + mapper count offset master = let (numin,numout) = countPairs offset count in + send master (numin,numout) + |]) + + +longdiv :: Integer -> Integer -> Integer -> String +longdiv _ 0 _ = "" +longdiv numer denom places = let attempt = numer `div` denom in + if places==0 + then "" + else shows attempt (longdiv2 (numer - attempt*denom) denom (places -1)) + where longdiv2 numer denom places | numer `rem` denom == 0 = "0" + | otherwise = longdiv (numer * 10) denom places + +initialProcess "SLAVE" = do + receiveWait [] + +initialProcess "MASTER" = do + peers <- getPeersDynamic 50000 + let slaves = findPeerByRole peers "SLAVE" + let interval = 1000000 + mypid <- getSelfPid + say "Starting..." + mapM_ (\(offset,nid) -> + do say $ "Telling slave " ++ show nid ++ " to look at range " ++ show offset ++ ".." ++ show (offset+interval) + spawnRemote nid (mapper__closure (interval-1) offset mypid)) (zip [0,interval..] slaves) + (x,y) <- receiveLoop (0,0) (length slaves) + let est = estimatePi (fromIntegral x) (fromIntegral y) + say $ "Done: " ++ longdiv (numerator est) (denominator est) 100 + where estimatePi ni no | ni+no==0 = 0 + | otherwise = (4 * ni) % (ni+no) + receiveLoop a 0 = return a + receiveLoop (numIn,numOut) n = + let + resultMatch = match (\(x,y) -> return (x::Int,y::Int)) + in do (newin,newout) <- receiveWait [resultMatch] + let x = numIn + newin + let y = numOut + newout + -- say $ longdiv (numerator $ estimatePi x y) (denominator $ estimatePi x y) 10 + receiveLoop (x,y) (n-1) + +initialProcess _ = error "Role must be SLAVE or MASTER" + +main = remoteInit "config" [Main.__remoteCallMetaData] initialProcess + + diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..2d55c2d --- /dev/null +++ b/notes.txt @@ -0,0 +1,251 @@ + +compilng as dynamic requires + cabal install --enable-shared --reinstall binary +compilng as profiling requires + cabal install -p --reinstall binary +for each dependency and + ghc --make Test -dynamic +for the main. +run profiler as +RTS -p -RTS + +global registration: +each process keeps connections open indefinitely +each node knows all the other nodes it's connected to +process registration traverses the whole node connection graph, registering it with each neighbor node until it is registered everywhere +similar for de-registrations +Upon creating a new connection to a node to which there has been no previous connection, +look at source for erlang:global? + +to investigate: CML's first-class events; dynamics in Clean; "death by accidental complexity"; state charts; linear types; talk to Calvert about cost model of distributed computing; "session types" by Honda, AKA "communication types" by Nielson +investigate LinearML, Idris, + +setting up to use amazon EC2 or other cloud-systems, demo applications (k-means, convex hull, string alignment,Byzantine generals, GRIP graph reduction, ) + +Rethinking of GLOBAL +-------------------- +CURRENT SYNCHRONOUS GLOBAL UPDATES CAN DEADLOCK -- I think this can be fixed just be doing a forkProcess (or forkWeakProcess) from within the startGlobalService at the point before it does a roundtripQuery to its peers +Explicit call to globalLink connects a node to the global network +Global service +What happens to roundtripQueries that are talking to downed processes? + +roundtripBroadcast :: (Serializable a, Serializable b) => PayloadDisposition -> [ProcessId] -> a -> ProcessM [b] + +Near future +------------- +the nature of roundtripQuery means that we should never do a roundtripQuery on a process that we aren't monitoring. Thus roundtripQuery should automatically start monitoring the other process and should return if it detects failure + +0. maybe we still want local name registration, as a per-node service or per-host services +1. Figure out how to detect if a peer has gone down DONE: hIsEOF should be okay +2. Make Global maintain fully-connected graph: + a. Per-process connection pool, to avoid reconnecting tcp-ly on each send + b. Each connection/disconnect to a remote node (sendRawRemote) and each and from a remote node (listenAndDeliver) will result in an AmSignal of type SigPeerDisconnect/SigPeerConnect + c. When Global gets such a message, it will see if the connection indicates a connection to a previously unknown peer. If so, perform a SYNCH. The synch is an operation performed synchronously on both ends within the the message sending framework, e.g. before the given message has been sent or received. + d. A synch is defined as a unionizing of global data: a union of known-peer sets, global process registry, promise registry, etc + e. during any update of global data, the change is performed synchronously to all peers in the known-peer of the given node + f. changes of global data include: global registering a process, noticing that a node has gone down + g. if a node notices that another node has gone down, it notifies all of its peers. All nodes can thus issue SigProcessDown signals for all processes that were running on the downed node +4. Demo apps, as above +admin services aren't very secure: sending them a bad message can crash them, as can happen when running different versions; more say behavior would be nice +ensure reportage of ProcessDown in case of host downage +make LogConfig Show/Read so we can stick it in the config file +improve logging output, so that timestamp is only output at minute intervals and timestamp is properly aligned +higher-level MapReduce functionality, based on promises, with automagical restart action on failure; also mechanisms for big disk file, that is spread around the cluster +spawnSynch that actually returns a result +use ServiceException for errors in the various service processes +consider that situation that a throwTo ProcessMonitorException happens while the app is waiting for something else. It seems to me that the exception-handling primtives need to be rethought to allow the processmonitorexception, a non-error exception, to pass through unharmed +bidirectional process linkage; have queryDiscovery send its query several times with a delay inbetween +add config options to automatically start system services +roundtripResponse should have a bracket block that sends an "abort waiting" message to roundtripQuery, so it doesn't wait forever if the serve crashes +there should be a MasterMonitor service that sets up a link to all other system processes and logs errors and restarts as necessary +let each Process have a "termination reason" set at exit, which can be retrieved by waitForProcess +create github or darcs (code.haskell.org) repository +keep a per-process table of both incoming and outgoing connections, to be used for name lookup +the registration system ties into the notification system, so that the process notifies the registrar when it dies, which unregisters it. does the global register need to keep track of which process links which, or can it be done on a per-process basis? Likewise, registerProcessName needs to make sure that the given process is still alive; see http://www.erlang.org/doc/man/global.html +my current thoughts on node regsitration are: there should be a config option that will set the node to use for registering; lookupProcessName and registerProcessName will use that node. If the setting is blank, it will look for a local registrar. This same setting can be used for other centralized settings, e.g. Skywriting-style promise-manager. Even better, this setting could be a LIST of nodes, which could be updated synchronously +per Erlang, attempts to re-register a particular name as a different pid, or to re-register the same pid as a different process should cause an exception. How to keep the registry up-to-date? Like this: on startup, generate a random number, put it in Node; use that number as a prefix to all registrations/lookups. Actually, this is a bad idea, as it will break foreign (nonlocal) registration. +sanity check to make sure that hostname and netmagic don't contain spaces +global name lookup +autopinging -- ping each node that we're connected to: if pings fail for a certain period of time, invoke custom user handler or send ProcessDown signals for all processes that were linked to something on that node; centralized pinging? This tendenacy to autoconnect to remote hosts can also be used to synchronize global names; this will need a connectionUp/connectionDown notification in sendRawRemote and listenAndDeliver +when doing a remote spawn, the per-process logging variable should cause logging to be forwarded to caling terminal +performance analysis service + +Tweaks: +------ +move all the random message type definitions and their instances to a separate module +replace all the readMVar prNodeRef with a higher level function +cache results of lookupProcessName +when registering a process under a name, the new name also goes into its mutable state, and is returned as part of its in pid in getSelfPid; the name, if present, does not influence comparison or message sending, but is printed out by show to aid in debugging +instead of using Network.listenOn, I should use the lower level API to bind the particular interface named in Node.ndHostName, rather than the default interface +provide more meaningful error message in matchUnknownThrow; use payloadGetType +readConfig should more elegantly be able to handle errors, missing config files +add exception handling wrapper to listenAndDeliver, as well as within it: e.g. handleCommSafer should catch exceptions and report to logger +The Rmt!! messages should contain complete Pid, not just LocalPid; receive will then check that the message was sent to the right node +use withSocketsDo for Windows compatibility +multiple logging strategies: to local file, by message to process, by message to a given node; use mvar to prevent race condition +timeout-bound variation of roundtrip suite +fail in ProcessM should throw an exception; fail in MatchM undo message acceptance??? +Rename Lookup (and associated stuff) to CompiletimeMetadata or some such + +Performance: +------------ +use foldl'/foldr in receive to make pattern matching faster/stricter: http://www.haskell.org/haskellwiki/Stack_overflow http://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl' +exclusionList, of all things, consumes enormous memory and processor time when list is big; it shouldn't, I should restructure this +threadpool in listenAndDeliver, instead of forkIO on accept; this is probably not necessary +keep connections open and re-use them on a per-process basis; if I do this, maybe turn KeepAlive off +short-circuit sendRaw to deliver directly to queue when sending to localhost; this probably means a new Message type that can store unencoded messages. data Payload = PayloadEncoded ByteString | PayloadUnencoded a + + +Future work: +------------ +make Closure a class with (at least) two instances: BasicClosure (corresponding to current Closure) and CompoundClosure, which is composed of two other closures, to be executed in sequence. Also, could have a BindClosure, correspondig to (>>=) with type like this: data BindClosure b = BC (Closure a) (Closure (a -> b) +STRef-like process-local variables, process-local storage +what happens if a thread dies while it still has unprocessed messages in its queue? Should the sender be signalled? (how does erlang do this?) +support for user-defined signals, beyond SigProcessUp/Down +Logload monitor should be able to keep track of # threads, CPU load, report to central; this can be done in Ping/Echo +higher level abstractions: replace ProcessM with a class that encapsulates its primitives (whatever they may be), so that one could implement a similar system based on other? remote protoocls. Eg an AllocatorM class that depends on ProcessM +testing with Test.QuickCheck and (mainly) HUnit +use ZeroConf/Bonjour to let nodes on a particular network find each other +proper commenting and documenation Haddock +elegant way to handle serializing infinite data structures +ability to serialize recursive data structures, e.g. tie-the-knot +how to serialize GADTs/existentials + + +Alternatives receive/match syntax: +receiveWait handler + where handler msg | "foo" <- msg = putStrLn "got a foo string" + | (Message a b) <- msg = putStrLn "got a message" + | s <- msg = putStrLn ("Got a non-foo string: " ++ s) + | otherwise = putStrLn "got something else" + +ConversationM paradigm: +conversati1 =do Hello <- receive + send Hello2 + (Request filename) <- receive + stuff <- liftIO $ readFile filename + send (Response stuff) + + + + +CLOSURE IS A MONAD +do a <- replicate__call 3 hi -- a :: ClosureM (ClosureResult String) + grom__call -- grom__call :: ClosureM (BasicClosure ()) + return a + 3 --barf +--hmmmm..... +(CompoundClosure (BindClosure "a" (BasicClosure "some__impl" (hi,3))) (BasicClosure "grom__impl" ())) + +--impl questions: +--when is pinging done of remote nodes? +--how does process registry work? how do we synchronously +--notify all nodes? + + +One can invision multi-type message receiving operating under several ways: + +a. The typename could be serialized as well, and checked by the receiver in a case statement +b. Each message would be accompanied by a user-determined string, which would be used in the case statement +c. Or some kind of clever clever SYB-style alteration system, e.g.: (receive (do ...) :: Foobar) |$| (receive (do ...):: (Int,String)) where a failed match would pass on to the next pattern. The corresponding type could be passed directly to the deserializer or could be matched against the typename explicitly transmitted + + +---------------- +Removed code: + +instance Monad ProcessM where + m >>= k = ProcessM $ (runProcessM m) >>= (\x -> runProcessM (k x)) + return x = ProcessM $ return x + + +match :: (Serializable a) => MatchM a +match = (\id -> do (payload,lookup) <- ask + case decode lookup payload of + Nothing -> fail "No match" + Just x -> return x + ) id + + + + + +type ClosureFunction = String + +data ClosureArgs v = ClosureArg v (ClosureArgs v)| ClosureNoArg --this needs to be a GADT, otherwise stuck being homogenous +--how to serialize GADTs? + +data Closure a = Closure ClosureFunction ClosureArgs + deriving (Data,Typeable) + +makeClosure0 :: ClosureFunction -> Closure a +makeClosure0 f = Closure f ClosureNoArg + +makeClosure1 :: (Serializable v) => ClosureFunction -> v -> Closure a +makeClosure1 f v = Closure f (ClosureArg v ClosureNoArg) + +makeClosure2 :: (Serializable v1,Serializable v2) => ClosureFunction -> v1 -> v2 -> Closure a +makeClosure2 f v1 v2 = Closure f (ClosureArg v1 (ClosureArg v2 ClosureNoArg)) + +invokeClosure :: (Typeable a) => Closure a -> ProcessM (Maybe a) +invokeClosure (Closure name arg) = (\id -> + do p <- getProcess + node <- liftIO $ readMVar (prNodeRef p) + res <- sequence [pureFun node,ioFun node,procFun node] + case catMaybes res of + (a:b) -> return $ Just a + _ -> return Nothing ) id + where pureFun node = case getEntryByIdent (ndLookup node) name of + Nothing -> return Nothing + Just x -> return $ Just $ apply x arg + ioFun node = case getEntryByIdent (ndLookup node) name of + Nothing -> return Nothing + Just x -> liftIO (apply x arg) >>= (return.Just) + procFun node = case getEntryByIdent (ndLookup node) name of + Nothing -> return Nothing + Just x -> (apply x arg) >>= (return.Just) + apply fun ClosureNoArg = fun + apply fun (ClosureArg v next) = apply (fun v) next + + + + +Limitations: +------------ + +Polymorphic functions don't work in remoteCall blocks. I recommend writing wrappers for each instance of each polymorphic function e.g. + +myMap :: (Int -> String) -> [Int] -> [String] +myMap = map + +Initialization options: +----------------------- + +Each node should have a configured "role" which will determine what it does when it starts; there could be a mandatory table mapping role names to main procedures for each node; the default behavior would be passive +network cookie (common for all nodes) +listening port, name +logging file and tolerance + +Process options: +--------------- + +forkIO vs forkOS +Erlang-style linkage, ping frequency +process style: synchronized return, pure functional invoke with asynch response, IO invoke, periodic invoke, persistent process (with channel), persistent with single-type channel, persistent with multi-type channel + +Questions +--------- +Why was SYB removed in GHC7? +StableNames stable enough? +What would a non-strict assembly language look like? + + +BIG REDESIGN: add an additional type parameter to ProcessId and ProcessM, so that messages sent must be of one type +this also greatly simplifies receive, since it can only return one type. General distributed code can be of type "ProcessM v a" but code that receives stuff would be "ProcessM Message a" or whatever you want to get. This would cause lots of big design changes. + +Things to talk about in write-up +-------------------------------- +Auto-generated closure stubs. Default generation precludes partial application, but this can be gotten around by using type aliases or doing closure packaging yourself. +Side-by-side comparison of Erlang code and Erlang-like Haskell code +discussion of programming with untyped processes vs typed channels. whither typed pids? +call of sufficiently stable StableName makes automatic mapping of real functions to their names very hard +SPJ's vision of multiple "threads" swimming within each "process", or my vision of a group of processes that must exist on the same node +how to serialize exisential types? + diff --git a/remote.cabal b/remote.cabal new file mode 100644 index 0000000..b99440d --- /dev/null +++ b/remote.cabal @@ -0,0 +1,17 @@ +Name: remote +Version: 0.0 +Description: Fault-tolerant distributed computing framework +synopsis: Fault-tolerant distributed computing framework +License: BSD3 +License-file: LICENSE +Extra-Source-Files: README +Author: Jeff Epstein +Maintainer: Jeff Epstein +Build-Depends: base >= 4, time, filepath, containers, network, syb, binary, mtl, bytestring, template-haskell, stm +Build-Type: Simple +Cabal-Version: >=1.2 +tested-with: GHC ==6.12.1 + +Exposed-Modules: Remote.Process, Remote.Encoding, Remote.Call, Remote.Peer, Remote.Init, Remote.Closure, Remote.Channel +ghc-options: -Wall +Extensions: TemplateHaskell, FlexibleInstances, UndecidableInstances, CPP, ExistentialQuantification, DeriveDataTypeable diff --git a/test-POTS.hs b/test-POTS.hs deleted file mode 100644 index b7e0b9e..0000000 --- a/test-POTS.hs +++ /dev/null @@ -1,81 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} -import Remote.Process -import Remote.Call (registerCalls,remoteCall) -import Remote.Encoding (genericGet, genericPut) - -import Control.Monad (when) -import Control.Monad.Trans (liftIO) -import Control.Concurrent (threadDelay) - -import POTS.StateMachine -import POTS.FakeTeleOS - -remoteCall [d| - f::Int - f=0 - |] -initializeAndRun proc = do - lookup <- registerCalls [__remoteCallMetaData] - cfg <- readConfig True (Just "config") - node <- initNode cfg lookup - startLocalRegistry cfg False - forkAndListenAndDeliver node cfg PldUser - roleDispatch node proc - waitForThreads node - threadDelay 500000 - --- main = initializeAndRun (createControlProcsBusy) -main = initializeAndRun (createControlProcs) - -{- -In the first test, we call our own number; we should get a busy tone - -Started DialTone on 479708 -Stopped tone on 479708 -Started BusyTone on 479708 --} - -createControlProcsBusy "NODE" = do - ph1 <- spawn (idle "479708") - registerProcessName "479708" ph1 - ph2 <- spawn (idle "478637") - registerProcessName "478637" ph2 - let telephoneCmds = TmOffHook: fmap TmDigitDialed "479708" - mapM_ (send ph1) telephoneCmds - --- This is run by: test-POTS.exe -cfgRole=NODEA - -createControlProcs "NODEA" = do - - ph1 <- spawn (idle "479708") - registerProcessName "479708" ph1 - - me <- getSelfPid - registerProcessName "NODEA" me - receiveWait [matchIf ((==)"ok") (const (return ()))] - - let telephoneCmds = TmOffHook: fmap TmDigitDialed "478637" - mapM_ (send ph1) telephoneCmds - liftIO $ threadDelay 1200000 - send ph1 TmOnHook - --- Meanwhile, elsewehere (but at the same IP address) run: --- test-POTS.exe -cfgRole=NODEB - -createControlProcs "NODEB" = do - ph2 <- spawn (idle "478637") - registerProcessName "478637" ph2 - - let trylookup = do - nodeA <- lookupProcessName "NODEA" - if nodeA == nullPid - then do liftIO $ threadDelay 50000 - trylookup - else return nodeA - nodeA <- trylookup - send nodeA "ok" - - liftIO $ threadDelay 500000 - send ph2 TmOffHook - liftIO $ threadDelay 900000 - send ph2 TmOnHook diff --git a/test-POTS1.hs b/test-POTS1.hs deleted file mode 100644 index b92cc62..0000000 --- a/test-POTS1.hs +++ /dev/null @@ -1,55 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} -import Remote.Process -import Remote.Call (registerCalls,remoteCall) -import Remote.Encoding (genericGet, genericPut) - -import Control.Monad (when) -import Control.Monad.Trans (liftIO) -import Control.Concurrent (threadDelay) - -import POTS.StateMachine -import POTS.FakeTeleOS - -remoteCall [d| - f::Int - f=0 - |] -initializeAndRun proc = do - lookup <- registerCalls [__remoteCallMetaData] - cfg <- readConfig True (Just "config") - node <- initNode cfg lookup - startLocalRegistry cfg False - forkAndListenAndDeliver node cfg PldUser - roleDispatch node proc - waitForThreads node - threadDelay 500000 - --- main = initializeAndRun initialProcess - -main = initializeAndRun (createControlProcs) - -{- -Started DialTone on 479708 -Stopped tone on 479708 -Started BusyTone on 479708 --} -createControlProcsBusy "NODE" = do - ph1 <- spawn (idle "479708") - registerProcessName "479708" ph1 - ph2 <- spawn (idle "478637") - registerProcessName "478637" ph2 - let telephoneCmds = TmOffHook: fmap TmDigitDialed "479708" - mapM_ (send ph1) telephoneCmds - - -createControlProcs "NODE" = do - ph1 <- spawn (idle "479708") - registerProcessName "479708" ph1 - ph2 <- spawn (idle "478637") - registerProcessName "478637" ph2 - let telephoneCmds = TmOffHook: fmap TmDigitDialed "478637" - mapM_ (send ph1) telephoneCmds - liftIO $ threadDelay 500000 - send ph2 TmOffHook - liftIO $ threadDelay 900000 - send ph2 TmOnHook \ No newline at end of file diff --git a/test-POTS2.hs b/test-POTS2.hs deleted file mode 100644 index 0ada726..0000000 --- a/test-POTS2.hs +++ /dev/null @@ -1,74 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} -import Remote.Process -import Remote.Call (registerCalls,remoteCall) -import Remote.Encoding (genericGet, genericPut) - -import Control.Monad (when) -import Control.Monad.Trans (liftIO) -import Control.Concurrent (threadDelay) - -import POTS.StateMachine -import POTS.FakeTeleOS - -remoteCall [d| - f::Int - f=0 - |] -initializeAndRun proc = do - lookup <- registerCalls [__remoteCallMetaData] - cfg <- readConfig True (Just "config") - node <- initNode cfg lookup - startLocalRegistry cfg False - forkAndListenAndDeliver node cfg PldUser - roleDispatch node proc - waitForThreads node - threadDelay 500000 - --- main = initializeAndRun initialProcess - -main = initializeAndRun (createControlProcs) - -{- -Started DialTone on 479708 -Stopped tone on 479708 -Started BusyTone on 479708 --} -createControlProcsBusy "NODE" = do - ph1 <- spawn (idle "479708") - registerProcessName "479708" ph1 - ph2 <- spawn (idle "478637") - registerProcessName "478637" ph2 - let telephoneCmds = TmOffHook: fmap TmDigitDialed "479708" - mapM_ (send ph1) telephoneCmds - - -createControlProcs "NODEA" = do - - ph1 <- spawn (idle "479708") - registerProcessName "479708" ph1 - - me <- getSelfPid - registerProcessName "NODEA" me - receiveWait [matchIf ((==)"ok") (const (return ()))] - - let telephoneCmds = TmOffHook: fmap TmDigitDialed "478637" - mapM_ (send ph1) telephoneCmds - - -createControlProcs "NODEB" = do - ph2 <- spawn (idle "478637") - registerProcessName "478637" ph2 - - let trylookup = do - nodeA <- lookupProcessName "NODEA" - if nodeA == nullPid - then do liftIO $ threadDelay 50000 - trylookup - else return nodeA - nodeA <- trylookup - send nodeA "ok" - - liftIO $ threadDelay 500000 - send ph2 TmOffHook - liftIO $ threadDelay 900000 - send ph2 TmOnHook