6822d6c520
Add a FedAvg fraud-detection application in two topologies: provider mode (coordinator/trainer star) and P2P mode (full mesh with CRDT-gated rounds), backed by an MLP trained on PaySim data with a synthetic fallback, plus demo scripts for both modes. Also harden the actor runtime: - supervisor: ignore stale death messages (OneForAll restart storm), drop normally-exited children instead of respawning, add a restart-rate limit - register actors before their thread starts so UUID dispatch and getSelf cannot race the spawn - killActor now kills the thread so links see Killed - fulfill connection promises with the pool winner on simultaneous connects - clamp the PaySim test split on small datasets and split shared transaction types across trainers in non-IID partitioning Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
207 lines
7.3 KiB
Haskell
207 lines
7.3 KiB
Haskell
module Control.Actor
|
|
( module Control.Actor.Types
|
|
, module Control.Actor.Transport
|
|
, module Control.Actor.Runtime
|
|
, module Control.Actor.Core
|
|
, module Control.Actor.Supervision
|
|
, module Control.Actor.Network
|
|
-- Initialization
|
|
, initRuntime
|
|
-- Demo
|
|
, pingActor
|
|
, forwardActorWithCell
|
|
, repeatActor
|
|
, system
|
|
, networkDemo
|
|
) where
|
|
|
|
import Control.Actor.Core
|
|
import Control.Actor.Network
|
|
import Control.Actor.Runtime
|
|
import Control.Actor.Supervision
|
|
import Control.Actor.Transport
|
|
import Control.Actor.Types
|
|
|
|
import Control.Concurrent (forkIO, threadDelay)
|
|
import Control.Concurrent.STM
|
|
( TMVar, TVar
|
|
, atomically, newEmptyTMVarIO, newTVarIO, readTMVar, readTVarIO, writeTQueue, writeTVar
|
|
)
|
|
import Control.Monad ((<=<), void)
|
|
import Control.Monad.Reader (MonadIO (..))
|
|
import Unsafe.Coerce (unsafeCoerce)
|
|
import Control.Exception (SomeException, try)
|
|
import Control.Actor.Registry (createRegistry, registerActor, lookupRemoteActor)
|
|
|
|
initRuntime :: NodeAddr -> (NodeAddr -> IO (Transport, NodeAddr)) -> IO Runtime
|
|
initRuntime myAddr createTransport = do
|
|
(transport, actualAddr) <- createTransport myAddr
|
|
rt0 <- newRuntime actualAddr transport
|
|
let rt = rt0 { rtSendRemote = \addr nm -> getOrCreateConn addr >>= cast' nm }
|
|
withRuntime rt $ void $ spawnActor sysHandlerFn stopOnDeath ()
|
|
withRuntime rt $ void createRegistry
|
|
tListen transport $ \ch -> do
|
|
result <- try @SomeException $ withRuntime rt $ handleNewConn ch
|
|
case result of
|
|
Left _ -> chClose ch
|
|
Right () -> return ()
|
|
return rt
|
|
|
|
|
|
-- Demo
|
|
|
|
pingActor :: Handler String () String
|
|
pingActor msg = continue ("Hello, " <> msg <> "!")
|
|
|
|
forwardActorWithCell :: TMVar (ActorRef String String) -> Handler String () String
|
|
forwardActorWithCell cell msg = do
|
|
pingRef <- liftIO $ atomically $ readTMVar cell
|
|
reply <- call msg pingRef
|
|
case reply of
|
|
Nothing -> liftIO $ putStrLn "forwardActorWithCell: received empty reply!"
|
|
Just x -> liftIO $ putStrLn ("forwardActorWithCell: received reply - " <> x)
|
|
maybeContinue reply
|
|
|
|
repeatActor :: String -> TMVar (ActorRef String String) -> Handler () () String
|
|
repeatActor r cell () = do
|
|
ref <- liftIO $ atomically $ readTMVar cell
|
|
(SomeActorRef self) <- getSelf
|
|
cast r ref
|
|
castIn 1000 () (unsafeCoerce self)
|
|
pass
|
|
|
|
system :: IO ()
|
|
system = do
|
|
rt <- initRuntime (NodeAddr "localhost" 9000) createTCPTransport
|
|
withRuntime rt $ do
|
|
pingCell <- liftIO newEmptyTMVarIO
|
|
forwardCell <- liftIO newEmptyTMVarIO
|
|
repeatCell <- liftIO newEmptyTMVarIO
|
|
|
|
_ <- liftIO $ forkIO $ do
|
|
repeatRef <- atomically $ readTMVar repeatCell
|
|
withRuntime rt $
|
|
cast' () repeatRef
|
|
|
|
supervise'
|
|
OneForOne
|
|
[ childWithRef pingActor stopOnDeath () pingCell
|
|
, childWithRef (forwardActorWithCell pingCell) stopOnDeath () forwardCell
|
|
, childWithRef (repeatActor "repeaaat" forwardCell) stopOnDeath () repeatCell
|
|
]
|
|
|
|
-- Network demo
|
|
|
|
newNodeActor :: Handler NodeAddr () NodeId
|
|
newNodeActor = continue <=< liftRuntime . connect Nothing
|
|
|
|
-- | Actor on node 2: echo — returns whatever it received.
|
|
echoActor :: Handler String () String
|
|
echoActor = continue
|
|
|
|
-- | Actor on node 2: printer — side-effects only.
|
|
printerActor :: Handler String () ()
|
|
printerActor msg = liftIO (putStrLn $ " [remote/print] " <> msg) >> pass
|
|
|
|
-- | Trivial actor used as a stub when we only care about death notifications.
|
|
noopActor :: Handler () () ()
|
|
noopActor _ = pass
|
|
|
|
-- | Captures the received string into a TVar (used as a node-1 receiver actor).
|
|
recvActorFn :: TVar (Maybe String) -> Handler String () ()
|
|
recvActorFn var msg =
|
|
liftIO (atomically $ writeTVar var (Just msg)) >> pass
|
|
|
|
-- | End-to-end networking demo.
|
|
--
|
|
-- Covers:
|
|
-- 1. Remote cast (node 1 → node 2)
|
|
-- 2. Remote call (node 1 ↔ node 2, request/reply)
|
|
-- 3. Reverse cast (node 2 → node 1)
|
|
-- 4. Cross-node death notification
|
|
networkDemo :: IO ()
|
|
networkDemo = do
|
|
putStrLn "=== networking demo ==="
|
|
|
|
-- Port 0 lets the OS pick a free port each run, avoiding stale-socket
|
|
-- conflicts when re-running in the same GHCi session.
|
|
rt1 <- initRuntime (NodeAddr "localhost" 0) createTCPTransport
|
|
rt2 <- initRuntime (NodeAddr "localhost" 0) createTCPTransport
|
|
let addr1 = rtNodeId rt1
|
|
addr2 = rtNodeId rt2
|
|
|
|
-- connect returns the locally-assigned NodeId for the peer.
|
|
-- NodeId 0 is always self; remotes get ids ≥ 1.
|
|
node2 <- withRuntime rt1 $ connect Nothing addr2
|
|
node1 <- withRuntime rt2 $ connect Nothing addr1
|
|
|
|
-- Spawn and register workers on node 2.
|
|
echoRef <- withRuntime rt2 $ spawnActor echoActor stopOnDeath ()
|
|
printRef <- withRuntime rt2 $ spawnActor printerActor stopOnDeath ()
|
|
withRuntime rt2 $ registerActor "echo" echoRef
|
|
withRuntime rt2 $ registerActor "printer" printRef
|
|
|
|
-- Small delay to let NMRegistryUpdate propagate to node 1.
|
|
threadDelay 100_000
|
|
|
|
-- 1. Remote cast: fire-and-forget from node 1 to node 2.
|
|
putStrLn "\n[1] remote cast node 1 -> node 2"
|
|
Just remotePrint <- withRuntime rt1 $ lookupRemoteActor "printer"
|
|
withRuntime rt1 $ cast' "hello from node 1" remotePrint
|
|
threadDelay 300_000
|
|
|
|
-- 2. Remote call: request/reply across nodes.
|
|
putStrLn "\n[2] remote call node 1 <-> node 2"
|
|
Just remoteEcho' <- withRuntime rt1 $ lookupRemoteActor "echo"
|
|
let
|
|
remoteEcho :: ActorRef String String
|
|
remoteEcho = (unsafeCoerce remoteEcho')
|
|
reply <- withRuntime rt1 $ call' "ping" remoteEcho
|
|
putStrLn $ " reply: " <> show reply
|
|
|
|
-- 3. Reverse cast: actor on node 2 sends to an actor on node 1.
|
|
putStrLn "\n[3] reverse cast node 2 -> node 1"
|
|
recvVar <- newTVarIO (Nothing :: Maybe String)
|
|
recvRef <- withRuntime rt1 $ spawnActor (recvActorFn recvVar) stopOnDeath ()
|
|
withRuntime rt1 $ registerActor "receiver" recvRef
|
|
threadDelay 100_000
|
|
Just remoteRecv <- withRuntime rt2 $ lookupRemoteActor "receiver"
|
|
withRuntime rt2 $ cast' "hello from node 2" remoteRecv
|
|
threadDelay 300_000
|
|
readTVarIO recvVar >>= \v -> putStrLn $ " received: " <> show v
|
|
|
|
-- 4. Cross-node death notification.
|
|
--
|
|
-- The mortal actor lives on node 2. When it dies, it sends NMDeath to
|
|
-- node 1. The watcher actor on node 1 has registered interest in the
|
|
-- mortal's ActorId; routeRemoteDeath finds it and delivers the message.
|
|
putStrLn "\n[4] cross-node death notification"
|
|
diedVar <- newTVarIO False
|
|
watchRef <- withRuntime rt1 $ spawnActor noopActor
|
|
(\dm -> do
|
|
liftIO $ putStrLn $ " [watcher] death of " <> show (dmActorId dm)
|
|
liftIO $ atomically $ writeTVar diedVar True
|
|
return Stop)
|
|
()
|
|
mortalRef <- withRuntime rt2 $ spawnActor noopActor stopOnDeath ()
|
|
|
|
let mortalId = someActorId (SomeActorRef mortalRef)
|
|
watchId = someActorId (SomeActorRef watchRef)
|
|
|
|
-- mortalRef's links: on death, send NMDeath to node 1 (by NodeId).
|
|
withRuntime rt2 $ linkActorTo (RemoteTarget watchId node1) mortalRef
|
|
-- watchRef's links: declare interest in mortalId so routeRemoteDeath
|
|
-- on node 1 can find this actor when NMDeath arrives.
|
|
withRuntime rt1 $ linkActorTo (RemoteTarget mortalId node2) watchRef
|
|
|
|
-- Kill the mortal actor by posting directly to its death queue.
|
|
case mortalRef of
|
|
LocalRef {arDeathQ, arState} ->
|
|
atomically $ writeTQueue arDeathQ (DeathMessage (asId arState) Killed)
|
|
RemoteRef _ -> return ()
|
|
|
|
threadDelay 500_000
|
|
readTVarIO diedVar >>= \d -> putStrLn $ " death delivered: " <> show d
|
|
|
|
putStrLn "\n=== done ==="
|