Files
actors/src/Control/Actor.hs
T
dvdrw 6822d6c520 feat: federated learning demo on the actor framework
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>
2026-06-21 12:00:00 +02:00

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 ==="