legion 0.8.0.2 → 0.8.0.3
raw patch · 7 files changed
+136/−69 lines, 7 files
Files
- legion.cabal +1/−1
- src/Network/Legion/ClusterState.hs +18/−10
- src/Network/Legion/Distribution.hs +0/−1
- src/Network/Legion/PowerState/Monad.hs +18/−3
- src/Network/Legion/Runtime.hs +0/−23
- src/Network/Legion/Runtime/ConnectionManager.hs +16/−11
- src/Network/Legion/StateMachine.hs +83/−20
legion.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: legion-version: 0.8.0.2+version: 0.8.0.3 synopsis: Distributed, stateful, homogeneous microservice framework. description: Legion is a framework for writing distributed, homogeneous, stateful microservices in Haskell.
src/Network/Legion/ClusterState.hs view
@@ -47,6 +47,7 @@ import qualified Data.Set as Set import qualified Data.Text as T import qualified Network.Legion.Distribution as D+import qualified Network.Legion.KeySet as KS import qualified Network.Legion.PowerState as PS import qualified Network.Legion.PowerState.Monad as PM @@ -71,12 +72,14 @@ rebalanceOrd = minBound } instance ToJSON ClusterState where- toJSON ClusterState {distribution, peers} = object [- "distribution" .= distribution,+ toJSON (ClusterState distribution_ peers_ updates_ rebalanceOrd_) = object [+ "distribution" .= distribution_, "peers" .= Map.fromList [ (show p, show a)- | (p, a) <- Map.toList peers- ]+ | (p, a) <- Map.toList peers_+ ],+ "updates" .= (show <$> updates_),+ "rebalanceOrd" .= show rebalanceOrd_ ] instance Show ClusterState where show = T.unpack . decodeUtf8 . LBS.toStrict . encode@@ -124,12 +127,17 @@ popUpdate :: ClusterState -> ClusterState popUpdate cs@ClusterState {updates, distribution, peers} = case (updates, rebalanceAction (Map.keysSet peers) distribution) of- (u:moreUpdates, (NoAction, _)) -> popUpdate cs {- peers = case u of- PeerJoined peer addr -> Map.insert peer addr peers- PeerEjected peer -> Map.delete peer peers,- updates = moreUpdates- }+ (u:moreUpdates, (NoAction, _)) -> popUpdate $+ case u of+ PeerJoined peer addr -> cs {+ peers = Map.insert peer addr peers,+ updates = moreUpdates+ }+ PeerEjected peer -> cs {+ distribution = D.modify (Set.delete peer) KS.full distribution,+ peers = Map.delete peer peers,+ updates = moreUpdates+ } _ -> cs
src/Network/Legion/Distribution.hs view
@@ -138,7 +138,6 @@ in unD (modify (Set.\\ defunct) KS.full distribution) - {- | Figure out if there are any under-served partitions and also figure out if this peer is the best candidate to service them . "Under
src/Network/Legion/PowerState/Monad.hs view
@@ -15,7 +15,10 @@ event, merge, acknowledge,+ acknowledgeAs, + getPowerState,+ participate, disassociate, ) where@@ -127,18 +130,30 @@ -} acknowledge :: (Monad m, Ord p, Event e r s, Eq e, Eq o) => PowerStateT o s p e r m ()-acknowledge = PowerStateT $ do+acknowledge = PowerStateT (lift2 ask) >>= acknowledgeAs+++{- | Like 'acknowledge', but for an arbigrary participant. -}+acknowledgeAs :: (Monad m, Ord p, Event e r s, Eq e, Eq o)+ => p+ -> PowerStateT o s p e r m ()+acknowledgeAs p = PowerStateT $ do ps <- get prop <- lift get- self <- lift2 ask let- (ps2, outputs) = PS.acknowledge self ps+ (ps2, outputs) = PS.acknowledge p ps prop2 = if ps2 /= ps then Send else prop put ps2 (lift . put) prop2 (lift3 . tell) outputs+++{- | Return the current value of the power state. -}+getPowerState :: (Monad m, Ord p)+ => PowerStateT o s p e r m (PowerState o s p e r)+getPowerState = PowerStateT get {- |
src/Network/Legion/Runtime.hs view
@@ -399,29 +399,6 @@ handleMessage {- Admin Eject Peer -} (A (Eject peer respond)) = do- {-- TODO: we should attempt to notify the ejected peer that it has- been ejected instead of just cutting it off and washing our hands- of it. I have a vague notion that maybe ejected peers should be- permanently recorded in the cluster state so that if they ever- reconnect then we can notify them that they are no longer welcome- to participate.-- On a related note, we need to think very hard about the split brain- problem. A random thought about that is that we should consider the- extreme case where the network just fails completely and every node- believes that every other node should be or has been ejected. This- would obviously be catastrophic in terms of data durability unless- we have some way to reintegrate an ejected node. So, either we- have to guarantee that such a situation can never happen, or else- implement a reintegration strategy. It might be acceptable for- the reintegration strategy to be very costly if it is characterized- as an extreme recovery scenario.-- Question: would a reintegration strategy become less costly if the- "next state id" for a peer were global across all power states- instead of local to each power state?- -} eject peer lift2 $ respond ()
src/Network/Legion/Runtime/ConnectionManager.hs view
@@ -15,7 +15,7 @@ import Prelude hiding (lookup) import Control.Concurrent (Chan, writeChan, newChan, readChan)-import Control.Exception (try, SomeException)+import Control.Exception (try, SomeException, bracketOnError) import Control.Monad (void) import Control.Monad.Logger (logInfo, logWarn) import Control.Monad.Trans.Class (lift)@@ -102,14 +102,20 @@ handle chan so = lift (readChan chan) >>= sendWithRetry so . encode >>= handle chan - {- |- Open a socket.- -}+ {- | Open a socket. -} openSocket :: IO Socket- openSocket = do- so <- socket (fam addr) Stream defaultProtocol- connect so addr- return so+ openSocket =+ {-+ Make sure to close the socket if an error happens during+ connection, because if not, we could easily run out of file+ descriptors in the case where we rapidly try to send thousands+ of message to the same peer, which could happen when one object+ is a hotspot.+ -}+ bracketOnError+ (socket (fam addr) Stream defaultProtocol)+ close+ (\so -> connect so addr >> return so) {- | Try to send the payload over the socket, and if that fails, then try to@@ -122,7 +128,7 @@ Left err -> do $(logWarn) . pack $ "Can't connect to: " ++ show addr ++ ". Dropping message on "- ++ "the floor: " ++ show payload ++ ". The error was: "+ ++ "the floor. The error was: " ++ show (err :: SomeException) return Nothing Right so -> do@@ -132,8 +138,7 @@ $ "An error happend when trying to send a payload over a socket " ++ "to the address: " ++ show addr ++ ". The error was: " ++ show (err :: SomeException) ++ ". This is the last straw, we "- ++ "are not retrying. The message is being dropped on the floor. "- ++ "The message was: " ++ show payload+ ++ "are not retrying. The message is being dropped on the floor." Right _ -> return () return (Just so) sendWithRetry (Just so) payload =
src/Network/Legion/StateMachine.hs view
@@ -226,9 +226,40 @@ {- | Eject a peer from the cluster. -} eject :: (MonadLogger m, MonadThrow m) => Peer -> SM e o s m ()-eject peer = runClusterPowerStateT (C.eject peer)+eject peer = do+ {-+ We need to think very hard about the split brain problem. A random+ thought about that is that we should consider the extreme case where+ the network just fails completely and every node believes that every+ other node should be or has been ejected. This would obviously be+ catastrophic in terms of data durability unless we have some way to+ reintegrate an ejected node. So, either we have to guarantee that+ such a situation can never happen, or else implement a reintegration+ strategy. It might be acceptable for the reintegration strategy to+ be very costly if it is characterized as an extreme recovery scenario. + Question: would a reintegration strategy become less costly if the+ "next state id" for a peer were global across all power states+ instead of local to each power state?+ -}+ runClusterPowerStateT (C.eject peer)+ {-+ 'runClusterPowerStateT (C.eject peer)' will cause us to attempt to+ notify the peer that they have been ejected, but that notification+ is almost certainly going to go unacknowledged because the peer+ is probably down.+ + This call to 'eject' was presumably invoked as a result of user+ action, and we must therefore trust the user to know that the peer+ is really down and not coming back. This "guarantee" allows us to+ acknowledge the ejection on the peer's behalf. + This call will acknowledge the drop on behalf of the peer, and also+ remove that peer from the keyspace distribution map.+ -}+ runClusterPowerStateTAs peer (return ())++ {- | Handle a peer join request. -} join :: (MonadIO m, MonadThrow m) => BSockAddr@@ -492,40 +523,72 @@ -> PartitionPowerStateT e o s (SM e o s m) a -> SM e o s m (a, PartitionPowerState e o s) runPartitionPowerStateT key m = do+ NodeState {self} <- getNodeState+ partition <- getPartition key+ PM.runPowerStateT self partition (+ m <* (removeObsolete >> PM.acknowledge)+ ) >>= \case+ Left err -> throwM err+ Right (a, action, partition2, _infOutputs) -> do+ case action of+ Send -> pushActions [+ PartitionMerge p key partition2+ | p <- Set.toList (PS.allParticipants partition2)+ , p /= self+ ]+ DoNothing -> return ()+ $(logDebug) . pack+ $ "Partition update: " ++ show partition+ ++ " --> " ++ show partition2 ++ " :: " ++ show action+ savePartition key partition2+ return (a, partition2)+ where+ {- |+ Remove obsolete peers. Obsolete peers are peers that are no longer+ participating in the replication of this partition, due to a+ rebalance. Such peers are removed lazily here at read time.+ -}+ removeObsolete :: (Eq e, Event e o s, Monad m)+ => PartitionPowerStateT e o s (SM e o s m) ()+ removeObsolete = do+ owners <- C.findOwners key . cluster <$> lift getNodeState+ peers <- PS.projParticipants <$> PM.getPowerState+ let obsolete = peers \\ owners+ mapM_+ (\peer -> PM.disassociate peer >> PM.acknowledgeAs peer)+ (Set.toList obsolete)+++{- | Like 'runClusterPowerStateTAs', but run as the local peer. -}+runClusterPowerStateT :: (MonadThrow m)+ => ClusterPowerStateT (SM e o s m) a+ -> SM e o s m a+runClusterPowerStateT m = do NodeState {self} <- getNodeState- partition <- getPartition key- PM.runPowerStateT self partition (m <* PM.acknowledge) >>= \case- Left err -> throwM err- Right (a, action, partition2, _infOutputs) -> do- case action of- Send -> pushActions [- PartitionMerge p key partition2- | p <- Set.toList (PS.allParticipants partition2)- ]- DoNothing -> return ()- $(logDebug) . pack- $ "Partition update: " ++ show partition- ++ " --> " ++ show partition2 ++ " :: " ++ show action- savePartition key partition2- return (a, partition2)+ runClusterPowerStateTAs self m {- | Run a clusterstate-flavored 'PowerStateT' in the 'SM' monad, automatically acknowledging the resulting power state.++ Generalized to run as any peer, in order to support exceptional cases+ like 'eject'. -}-runClusterPowerStateT :: (MonadThrow m)- => ClusterPowerStateT (SM e o s m) a+runClusterPowerStateTAs :: (MonadThrow m)+ => Peer {- ^ The peer to run as. -}+ -> ClusterPowerStateT (SM e o s m) a -> SM e o s m a-runClusterPowerStateT m = do+runClusterPowerStateTAs as m = do NodeState {cluster, self} <- getNodeState- PM.runPowerStateT self cluster (m <* PM.acknowledge) >>= \case+ PM.runPowerStateT as cluster (m <* PM.acknowledge) >>= \case Left err -> throwM err Right (a, action, cluster2, _outputs) -> do case action of Send -> pushActions [ ClusterMerge p cluster2 | p <- Set.toList (PS.allParticipants cluster2)+ , p /= self ] DoNothing -> return () modifyNodeState (\ns -> ns {cluster = cluster2})