packages feed

legion (empty) → 0.1.0.0

raw patch · 23 files changed

+3812/−0 lines, 23 filesdep +Ranged-setsdep +attoparsecdep +basesetup-changed

Dependencies added: Ranged-sets, attoparsec, base, binary, binary-conduit, bytestring, conduit, conduit-extra, containers, data-default-class, data-dword, directory, exceptions, monad-logger, network, scotty, scotty-resource, stm, text, time, transformers, unix, uuid, warp

Files

+ LICENSE view
@@ -0,0 +1,202 @@++                                 Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "[]"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright [yyyy] [name of copyright owner]++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ legion.cabal view
@@ -0,0 +1,76 @@+-- Initial keyspace-partition.cabal generated by cabal init.  For further +-- documentation, see http://haskell.org/cabal/users-guide/++name:                legion+version:             0.1.0.0+synopsis:            Distributed, stateful, homogeneous microservice framework.+description:         Legion is a framework for writing distributed,+                     homogeneous, stateful microservices in Haskell.+homepage:            https://github.com/taphu/legion+license:             Apache-2.0+license-file:        LICENSE+author:              Rick Owens+maintainer:          rick@owenssoftware.com+copyright:           2015-2016 Rick Owens+category:            Concurrency, Network+build-type:          Simple+-- extra-source-files:  +cabal-version:       >=1.10++source-repository head+  type: git+  location: git@github.com:taphu/legion.git++library+  exposed-modules:     +    Network.Legion+  other-modules:       +    Network.Legion.Admin+    Network.Legion.Application+    Network.Legion.BSockAddr+    Network.Legion.Basics+    Network.Legion.ClusterState+    Network.Legion.Conduit+    Network.Legion.ConnectionManager+    Network.Legion.Distribution+    Network.Legion.Fork+    Network.Legion.KeySet+    Network.Legion.LIO+    Network.Legion.PartitionKey+    Network.Legion.PartitionState+    Network.Legion.PowerState+    Network.Legion.Propagation+    Network.Legion.Runtime+    Network.Legion.Settings+    Network.Legion.StateMachine+    Network.Legion.UUID+  -- other-extensions:    +  build-depends:+    Ranged-sets >= 0.3.0 && < 0.4,+    attoparsec >= 0.13.0.1 && < 0.14,+    base >= 4.8 && < 4.9,+    binary >= 0.7.5 && < 0.9,+    binary-conduit >= 1.2.3 && < 1.3,+    bytestring >= 0.10.4.0 && < 0.11,+    conduit >= 1.2.4 && < 1.3,+    conduit-extra >= 1.1.9 && < 1.2,+    containers >= 0.5.5.1 && < 0.6,+    data-default-class >= 0.0.1 && < 0.2,+    data-dword >= 0.3 && < 0.4,+    directory >= 1.2.1.0 && < 1.3,+    exceptions >= 0.8 && < 0.9,+    monad-logger >= 0.3.17 && < 0.4,+    network >= 2.6.2.1 && < 2.7,+    scotty >= 0.11.0 && < 0.12,+    scotty-resource >= 0.1 && < 0.2,+    stm >= 2.4.4.1 && < 2.5,+    text >= 1.2.2.0 && < 1.3,+    time >= 1.4.2 && < 1.6,+    transformers >= 0.3.0.0 && < 0.5,+    unix >= 2.7 && < 2.8,+    uuid >= 1.3.11 && < 1.4,+    warp >= 3.2 && < 3.3+  hs-source-dirs:      src+  default-language:    Haskell2010+  ghc-options: -Wall -W -fwarn-incomplete-uni-patterns+
+ src/Network/Legion.hs view
@@ -0,0 +1,173 @@+{- |+  Legion is a mathematically sound framework for writing horizontally+  scalable user applications. Historically, horizontal scalability has+  been achieved via the property of statelessness. Programmers would+  design their applications to be free of any kind of persistent state,+  avoiding the problem of distributed state management. This almost never+  turns out to really be possible, so programmers achieve "statelessness"+  by delegating application state management to some kind of external,+  shared database -- which ends up having its own scalability problems.++  In addition to scalability problems, which modern databases (especially+  NoSQL databases) have done a good job of solving, there is another,+  more fundamental problem facing these architectures: The application+  is not really stateless.++  Legion is a Haskell framework that abstracts state partitioning, data+  replication, request routing, and cluster rebalancing, making it easy+  to implement large and robust distributed data applications.++  Examples of services that rely on partitioning include ElasticSearch,+  Riak, DynamoDB, and others. In other words, almost all scalable+  databases.+-}++module Network.Legion (+  -- * Service Implementation+  -- $service-implementaiton+  Legionary(..),+  LegionConstraints,+  Persistence(..),+  ApplyDelta(..),+  RequestMsg,+  -- * Invoking Legion+  -- $invocation+  forkLegionary,+  runLegionary,+  StartupMode(..),+  -- * Fundamental Types+  PartitionKey(..),+  PartitionPowerState,+  projected,+  infimum,+  -- * Framework Configuration+  -- $framework-config+  LegionarySettings(..),+  -- * Utils+  newMemoryPersistence,+  diskPersistence,+) where++import Prelude hiding (lookup, readFile, writeFile, null)++import Network.Legion.Application (LegionConstraints,+  Persistence(Persistence, getState, saveState, list),+  Legionary(Legionary, persistence, handleRequest), RequestMsg)+import Network.Legion.Basics (newMemoryPersistence, diskPersistence)+import Network.Legion.PartitionKey (PartitionKey(K, unkey))+import Network.Legion.PartitionState (PartitionPowerState, infimum, projected)+import Network.Legion.PowerState (ApplyDelta(apply))+import Network.Legion.Runtime (runLegionary, StartupMode(NewCluster,+  JoinCluster), forkLegionary)+import Network.Legion.Settings (LegionarySettings(LegionarySettings,+  adminHost, adminPort, peerBindAddr, joinBindAddr))++--------------------------------------------------------------------------------++-- $service-implementaiton+-- Whenever you use Legion to develop a distributed application, your+-- application is going to be divided into two major parts, the state/less/+-- part, and the state/ful/ part. The stateless part is going to be the+-- context in which a legion node is running -- probably a web server if you+-- are exposing your application as a web service. Legion itself is focused+-- mainly on the stateful part, and it will do all the heavy lifting on+-- that side of things. However, it is worth mentioning a few things about+-- the stateless part before we move on.+-- +-- The unit of state that Legion knows about is called a \"partition\". Each+-- partition is identified by a 'PartitionKey', and it is replicated across+-- the cluster. Each partition acts as the unit of state for handling+-- stateful user requests which are routed to it based on the `PartitionKey`+-- associated with the request. What the stateful part of Legion is+-- /not/ able to do is figure out what partition key is associated with+-- the request in the first place. This is a function of the stateless+-- part of the application. Generally speaking, the stateless part of+-- your application is going to be responsible for+-- +--   * Starting up the Legion runtime using 'forkLegionary'.+--   * Identifying the partition key to which a request should be applied+--     (e.g.  maybe this is some component of a URL, or else an identifier+--     stashed in a browser cookie).+--   * Marshalling application requests into requests to the Legion runtime.+--   * Marshalling the Legion runtime response into an application response.+-- +-- Legion doesn't really address any of these things, mainly because there+-- are already plenty of great ways to write stateless services. What+-- Legion does provide is a runtime that can be embedded in the stateless+-- part of your application, that transparently handles all of the hard+-- stateful stuff, like replication, rebalancing, request routing, etc.+-- +-- The only thing required to implement a legion service is to+-- provide a request handler and a persistence layer by constructing a+-- 'Legionary' value and passing it to 'forkLegionary'. The stateful+-- part of your application will live mostly within the request handler+-- 'handleRequest'. If you look at 'handleRequest', you will see that+-- it is abstract over the type variables @i@, @o@, and @s@.+--+-- > handleRequest :: PartitionKey -> i -> s -> o+--+-- These are the types your application has to fill in. @i@ stands for+-- "input", which is the type of requests your application accepts; @o@+-- stands for "output", which is the type of responses your application+-- will generate in response to those requests, and @s@ stands for "state",+-- which is the application state that each partition can assume.+-- +-- Implementing a request handler is pretty straight forward, but+-- there is a little bit more to it than meets the eye. If you look at+-- 'forkLegionary', you will see a constraint named @'LegionConstraints'+-- i o s@, which is short-hand for a long list of typeclasses that+-- your @i@, @o@, and @s@ types are going to have to implement. Of+-- particular interest is the 'ApplyDelta' typeclass. If you look at+-- 'handleRequest', you will see that it is defined in terms of an input,+-- an existing state, and an output, but there is no mention of any /new/+-- state that is generated as a result of handling the request.+-- +-- This is where the 'ApplyDelta' typeclass comes in. Where 'handleRequest'+-- takes an input and a state and produces an output, the 'apply' function+-- of the 'ApplyDelta' typeclass takes an input and a state and produces+-- a new state.+--+-- > apply :: i -> s -> s+--+-- The reason that Legion splits the definition of what it means to+-- fully handle an input into two functions like this is because of the+-- approach it takes to solving distributed systems problems. Describing+-- this entirely is beyond the scope of this section of documentation+-- (TODO link to more info) but the TL;DR is that 'handleRequest' will+-- only get called once for each input, but 'apply' has a very good+-- chance of being called more than once for various reasons including+-- re-playing the application of requests to resolve non-determinism.+-- +-- Taking yet another look at 'handleRequest', you will see that it+-- makes no provision for a non-existent partition state (i.e., it is+-- written in terms of @s@, not @Maybe s@. Same goes for 'ApplyDelta').+-- This framework takes the somewhat platonic philosophical view that all+-- mathematical values exist somewhere and that there is no such thing as+-- non-existent partition. When you first spin up a Legion application,+-- all of those partitions are going to have a default value, which is+-- 'Data.Default.Class.def' (Because your partition state must be an+-- instance of the 'Data.Default.Class.Default' typeclass). This doesn't+-- take up infinite disk space because 'Data.Default.Class.def' values+-- are cleverly encoded as a zero-length string of bytes. ;-)+-- +-- The persistence layer provides the framework with a way to store the+-- various partition states. This allows you to choose any number of+-- persistence strategies, including only in memory, on disk, or in some+-- external database.+-- +-- See 'newMemoryPersistence' and 'diskPersistence' if you need to get+-- started quickly with an in-memory persistence layer.++--------------------------------------------------------------------------------++-- $invocation+-- Notes on invocation.++--------------------------------------------------------------------------------++-- $framework-config+-- The legion framework has several operational parameters which can+-- be controlled using configuration. These include the address binding+-- used to expose the cluster management service endpoint and what file+-- to use for cluster state journaling.+
+ src/Network/Legion/Admin.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{- |+  This module contains the admin interface code.+-}+module Network.Legion.Admin (+  runAdmin,+) where++import Control.Concurrent (forkIO, newChan, newEmptyMVar, writeChan,+  putMVar, takeMVar, Chan)+import Control.Monad (void)+import Control.Monad.Logger (askLoggerIO, runLoggingT)+import Control.Monad.Trans.Class (lift)+import Data.Conduit (Source)+import Data.Default.Class (def)+import Data.Text.Lazy (Text, pack)+import Network.Legion.Application (LegionConstraints)+import Network.Legion.Conduit (chanToSource)+import Network.Legion.LIO (LIO)+import Network.Legion.PartitionKey (PartitionKey(K))+import Network.Legion.StateMachine (AdminMessage(GetState, GetPart))+import Network.Wai.Handler.Warp (HostPreference, defaultSettings, Port,+  setHost, setPort)+import Web.Scotty.Resource.Trans (resource, get)+import Web.Scotty.Trans (Options, scottyOptsT, settings, ScottyT, text,+  ActionT, param)++{- |+  Start the admin service in a background thread.+-}+runAdmin :: (LegionConstraints i o s)+  => Port+  -> HostPreference+  -> LIO (Source LIO (AdminMessage i o s))+runAdmin addr host = do+  logging <- askLoggerIO+  chan <- lift newChan+  void . lift . forkIO . (`runLoggingT` logging) $+    let+      website :: ScottyT Text LIO ()+      website = do+        resource "/clusterstate" $+          get $ do+            val <- send chan GetState+            text (pack (show val))+        resource "/propstate/:key" $+          get $ do+            key <- K . read <$> param "key"+            val <- send chan (GetPart key)+            text (pack (show val))+    in scottyOptsT (options addr host) (`runLoggingT` logging) website+  return (chanToSource chan)+  where+    send+      :: Chan (AdminMessage i o s)+      -> ((a -> LIO ()) -> AdminMessage i o s)+      -> ActionT Text LIO a+    send chan msg = lift . lift $ do+      mvar <- newEmptyMVar+      writeChan chan (msg (lift . putMVar mvar))+      takeMVar mvar+++{- |+  Build some warp settings based on the configured socket address.+-}+options :: Port -> HostPreference -> Options+options port host = def {+    settings =+      setPort port+      . setHost host+      $ defaultSettings+  }++
+ src/Network/Legion/Application.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{- |+  This module contains the data types necessary for implementing the+  user application.+-}+module Network.Legion.Application (+  LegionConstraints,+  Legionary(..),+  Persistence(..),+  RequestMsg,+) where++import Data.Binary (Binary)+import Data.Conduit (Source)+import Data.Default.Class (Default)+import Network.Legion.PartitionKey (PartitionKey)+import Network.Legion.PartitionState (PartitionPowerState)+import Network.Legion.PowerState (ApplyDelta)++{- |+  This is a more convenient way to write the somewhat unwieldy set of+  constraints+   +  > (+  >   ApplyDelta i s, Default s, Binary i, Binary o, Binary s, Show i,+  >   Show o, Show s, Eq i+  > )+-}+class (+    ApplyDelta i s, Default s, Binary i, Binary o, Binary s, Show i,+    Show o, Show s, Eq i+  ) => LegionConstraints i o s where+++{- |+  This is the type of a user-defined Legion application. Implement this and+  allow the Legion framework to manage your cluster.+-}+data Legionary i o s = Legionary {+    {- |+      The request handler, implemented by the user to service requests.++      Returns a response to the request, together with the new partition+      state.+    -}+    handleRequest :: PartitionKey -> i -> s -> o,+    {- |+      The user-defined persistence layer implementation.+    -}+    persistence :: Persistence i s+  }+++{- |+  The type of a user-defined persistence strategy used to persist+  partition states. See 'Network.Legion.newMemoryPersistence' or+  'Network.Legion.diskPersistence' if you need to get started quickly.+-}+data Persistence i s = Persistence {+     getState :: PartitionKey -> IO (Maybe (PartitionPowerState i s)),+    saveState :: PartitionKey -> Maybe (PartitionPowerState i s) -> IO (),+         list :: Source IO (PartitionKey, PartitionPowerState i s)+      {- ^+        List all the keys known to the persistence layer. It is important+        that the implementation do the right thing with regard to+        `Data.Conduit.addCleanup`, because there are cases where the+        conduit is terminated without reading the entire list.+      -}+  }+++{- |+  This is how requests are packaged when they are sent to the legion framework+  for handling. It includes the request information itself, a partition key to+  which the request is directed, and a way for the framework to deliver the+  response to some interested party.++  Unless you know exactly what you are doing, you will have used+  'Network.Legion.forkLegionary' instead of 'Network.Legion.runLegionary'+  to run the framework, in which case you can safely ignore the existence+  of this type.+-}+type RequestMsg i o = ((PartitionKey, i), o -> IO ())++
+ src/Network/Legion/BSockAddr.hs view
@@ -0,0 +1,53 @@+{- |+  This module contains the BSockAddr data type.+-}+module Network.Legion.BSockAddr (+  BSockAddr(..)+) where++import Data.Binary (Binary(put, get))+import Data.Word (Word8)+import Network.Socket (SockAddr(SockAddrInet, SockAddrInet6, SockAddrUnix,+  SockAddrCan))+++{- |+  A type useful only for creating a `Binary` instance of `SockAddr`.+-}+newtype BSockAddr = BSockAddr {getAddr :: SockAddr} deriving (Show, Eq)++instance Binary BSockAddr where+  put (BSockAddr addr) =+    case addr of+      SockAddrInet p h -> do+        put (0 :: Word8)+        put (fromEnum p, h)+      SockAddrInet6 p f h s -> do+        put (1 :: Word8)+        put (fromEnum p, f, h, s)+      SockAddrUnix s -> do+        put (2 :: Word8)+        put s+      SockAddrCan a -> do+        put (3 :: Word8)+        put a++  get = BSockAddr <$> do+    c <- get+    case (c :: Word8) of+      0 -> do+        (p, h) <- get+        return (SockAddrInet (toEnum p) h)+      1 -> do+        (p, f, h, s) <- get+        return (SockAddrInet6 (toEnum p) f h s)+      2 -> SockAddrUnix <$> get+      3 -> SockAddrCan <$> get+      _ ->+        fail+          $ "Can't decode BSockAddr because the constructor tag "+          ++ "was not understood. Probably this data is representing "+          ++ "something else."+++
+ src/Network/Legion/Basics.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE NamedFieldPuns #-}+{- |+  This module contains some basis persistence strategies useful for+  testing, or getting started.+-}+module Network.Legion.Basics (+  newMemoryPersistence,+  diskPersistence,+) where++import Prelude hiding (lookup, readFile, writeFile)++import Control.Concurrent.STM (atomically, newTVar, modifyTVar, readTVar,+  TVar)+import Control.Monad.Trans.Class (lift)+import Data.Binary (Binary, encode, decode)+import Data.Bool (bool)+import Data.ByteString (readFile, writeFile)+import Data.ByteString.Lazy (toStrict, fromStrict)+import Data.Conduit (Source, (=$=), awaitForever, yield)+import Data.Conduit.List (sourceList)+import Data.Either (rights)+import Data.Map (Map, insert, lookup)+import Network.Legion.Application (Persistence(Persistence, getState,+  saveState, list))+import Network.Legion.PartitionKey (PartitionKey, toHex, fromHex)+import Network.Legion.PartitionState(PartitionPowerState)+import System.Directory (removeFile, doesFileExist, getDirectoryContents)+import qualified Data.Map as Map+++{- |+  A convenient memory-based persistence layer. Good for testing or for+  applications (like caches) that don't have durability requirements.+-}+newMemoryPersistence :: IO (Persistence i s)++newMemoryPersistence = do+    cacheT <- atomically (newTVar Map.empty)+    return Persistence {+        getState = fetchState cacheT,+        saveState = saveState_ cacheT,+        list = list_ cacheT+      }+  where+    saveState_+      :: TVar (Map PartitionKey (PartitionPowerState i s))+      -> PartitionKey+      -> Maybe (PartitionPowerState i s)+      -> IO ()+    saveState_ cacheT key (Just state) =+      (atomically . modifyTVar cacheT . insert key) state++    saveState_ cacheT key Nothing =+      (atomically . modifyTVar cacheT . Map.delete) key++    fetchState+      :: TVar (Map PartitionKey (PartitionPowerState i s))+      -> PartitionKey+      -> IO (Maybe (PartitionPowerState i s))+    fetchState cacheT key = atomically $+      lookup key <$> readTVar cacheT+    +    list_+      :: TVar (Map PartitionKey (PartitionPowerState i s))+      -> Source IO (PartitionKey, PartitionPowerState i s)+    list_ cacheT =+      sourceList . Map.toList =<< lift (atomically (readTVar cacheT))+++{- | A convenient way to persist partition states to disk.  -}+diskPersistence :: (Binary i, Binary s)+  => FilePath+    -- ^ The directory under which partition states will be stored.+  -> Persistence i s++diskPersistence directory = Persistence {+      getState,+      saveState,+      list+    }+  where+    getState :: (Binary i, Binary s)+      => PartitionKey+      -> IO (Maybe (PartitionPowerState i s))+    getState key =+      let path = toPath key in+      doesFileExist path >>= bool+        (return Nothing)+        ((Just . decode . fromStrict) <$> readFile path)++    saveState :: (Binary i, Binary s)+      => PartitionKey+      -> Maybe (PartitionPowerState i s)+      -> IO ()+    saveState key (Just state) =+      writeFile (toPath key) (toStrict (encode state))+    saveState key Nothing =+      let path = toPath key in+      doesFileExist path >>= bool+        (return ())+        (removeFile path)++    list :: (Binary i, Binary s)+      => Source IO (PartitionKey, PartitionPowerState i s)+    list = do+        keys <- lift $ readHexList <$> getDirectoryContents directory+        sourceList keys =$= fillData+      where +        fillData = awaitForever (\key -> do+            let path = toPath key+            state <- lift ((decode . fromStrict) <$> readFile path)+            yield (key, state)+          )+        readHexList = rights . fmap fromHex . filter notSys+        notSys = not . (`elem` [".", ".."])++    {- |+      Convert a key to a path+    -}+    toPath :: PartitionKey -> FilePath+    toPath key = directory ++ "/" ++ toHex key++
+ src/Network/Legion/ClusterState.hs view
@@ -0,0 +1,225 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-}+{- |+  This module contains the data types related to the distributed cluster state.+-}+module Network.Legion.ClusterState (+  ClusterState,+  ClusterPowerState,+  ClusterPropState,+  claimParticipation,+  new,+  initProp,+  getPowerState,+  getPeers,+  findPartition,+  getDistribution,+  joinCluster,+  mergeEither,+  actions,+  allParticipants,+  heartbeat,+) where++import Data.Binary (Binary)+import Data.Default.Class (Default(def))+import Data.Map (Map)+import Data.Set (Set)+import Data.Time.Clock (UTCTime)+import Data.UUID (UUID)+import GHC.Generics (Generic)+import Network.Legion.BSockAddr (BSockAddr(BSockAddr))+import Network.Legion.Distribution (ParticipationDefaults, modify, Peer)+import Network.Legion.KeySet (KeySet, full, unions)+import Network.Legion.PartitionKey (PartitionKey)+import Network.Legion.PowerState (ApplyDelta(apply))+import Network.Legion.Propagation (PropState, PropPowerState)+import Network.Socket (SockAddr)+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Network.Legion.Distribution as D+import qualified Network.Legion.Propagation as P+++{- |+  An opaque data type, representing the cluster state that is shared+  between all peers.+-}+data ClusterState = ClusterState {+    distribution :: ParticipationDefaults,+           peers :: Map Peer BSockAddr+  }+  deriving (Show, Generic)+instance Binary ClusterState+instance Default ClusterState where+  def = ClusterState {+      distribution = D.empty,+             peers = Map.empty+    }+++{- |+  A representation of all possible cluster states.+-}+newtype ClusterPowerState = ClusterPowerState {+    unPowerState :: PropPowerState UUID ClusterState Peer Update+  } deriving (Show, Binary)+++{- |+  A reification of `PropState`, representing the propagation state of the+  cluster state.+-}+newtype ClusterPropState = ClusterPropState {+    unPropState :: PropState UUID ClusterState Peer Update+  } deriving (Show)+++{- |+  The kinds of updates that can be applied to the cluster state.+-}+data Update+  = PeerJoined Peer BSockAddr+  | Participating Peer KeySet+  deriving (Show, Generic)+instance Binary Update+instance ApplyDelta Update ClusterState where+  apply (PeerJoined peer addr) cs@ClusterState {peers} =+    cs {peers = Map.insert peer addr peers}+  apply (Participating peer ks) cs@ClusterState {distribution} =+    cs {distribution = modify (Set.insert peer) ks distribution}+++{- |+  Helper function, for easily claiming participation in a key set.+-}+claimParticipation+  :: Peer+  -> KeySet+  -> ClusterPropState+  -> ClusterPropState+claimParticipation peer ks = +  ClusterPropState+  . P.delta (Participating peer ks)+  . unPropState+++{- |+  Create the cluster state appropriate for a brand-new cluster.+-}+new :: UUID -> Peer -> SockAddr -> ClusterPropState+new clusterId self addy =+  claimParticipation self full+  . ClusterPropState+  . P.delta (PeerJoined self (BSockAddr addy))+  $ P.new clusterId self (Set.singleton self)+++{- |+  Initialize a `ClusterPropState` based on the initial underlying cluster power+  state.+-}+initProp :: Peer -> ClusterPowerState -> ClusterPropState+initProp self = ClusterPropState . P.initProp self . unPowerState+++{- |+  Return an opaque representation of the underling power state, for transfer+  across the network, or whatever.+-}+getPowerState :: ClusterPropState -> ClusterPowerState+getPowerState = ClusterPowerState . P.getPowerState . unPropState+++{- |+  Get the cluster peers.+-}+getPeers :: ClusterPropState -> Map Peer BSockAddr+getPeers = peers . P.ask . unPropState+++{- |+  get the cluster distribution.+-}+getDistribution :: ClusterPropState -> ParticipationDefaults+getDistribution = distribution . P.ask . unPropState+++{- |+  Find the nodes that own a given partition.+-}+findPartition :: PartitionKey -> ClusterPropState -> Set Peer+findPartition key =+  D.findPartition key . distribution . P.ask . unPropState+++{- |+  Allow a new peer to join the cluster.+-}+joinCluster+  :: Peer+    {- ^ The peer that is joining. -}+  -> BSockAddr+    {- ^ The cluster address of the new peer. -}+  -> ClusterPropState+    {- ^ The current cluster propagation state. -}+  -> ClusterPropState+joinCluster peer addy =+  ClusterPropState+  . P.delta (PeerJoined peer addy)+  . P.participate peer+  . unPropState+++{- |+  Merge a foreign cluster state with our own cluster state. This function+  returns the new cluster propagation state, along with a set of partition keys+  for which the default participation has changed (aka, a rebalance happened),+  indicating that some action should be taken to migrate the indicated+  partitions.+-}+mergeEither+  :: Peer+  -> ClusterPowerState+  -> ClusterPropState+  -> Either String (ClusterPropState, KeySet)+mergeEither otherPeer (ClusterPowerState otherPS) (ClusterPropState prop) =+  let+    self = P.getSelf prop+    divergences = P.divergences self (P.initProp otherPeer otherPS)+    migrating = unions [+        ks+        | (_, Participating _ ks) <- Map.toList divergences+      ]+  in case P.mergeEither otherPeer otherPS prop of+    Left err -> Left err+    Right newProp -> Right (ClusterPropState newProp, migrating)+++{- |+  Get the peers which require action (i.e. Send), if any, and the+  powerstate version to send to those peers, and the new propagation+  state that is applicable after those actions have been taken.+-}+actions :: ClusterPropState -> (Set Peer, ClusterPowerState, ClusterPropState)+actions prop =+  let (peers, ps, newProp) = P.actions (unPropState prop)+  in (peers, ClusterPowerState ps, ClusterPropState newProp)+++{- |+  Return all cluster participants.+-}+allParticipants :: ClusterPropState -> Set Peer+allParticipants = P.allParticipants . unPropState+++{- |+  Move time forward for the propagation state.+-}+heartbeat :: UTCTime -> ClusterPropState -> ClusterPropState+heartbeat now = ClusterPropState . P.heartbeat now . unPropState++
+ src/Network/Legion/Conduit.hs view
@@ -0,0 +1,53 @@+{- |+  This module contains some handy conduit abstractions.+-}+module Network.Legion.Conduit (+  chanToSource,+  chanToSink,+  merge,+) where++import Control.Concurrent (forkIO)+import Control.Concurrent.Chan (Chan, newChan, writeChan, readChan)+import Control.Monad (void, forever)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Conduit (Source, Sink, ($$), await, ($=), yield, await)+import qualified Data.Conduit.List as CL (map)++{- |+  Convert a chanel into a Source.+-}+chanToSource :: (MonadIO io) => Chan a -> Source io a+chanToSource chan = forever $ yield =<< liftIO (readChan chan)+++{- |+ Convert an chanel into a Sink.+-}+chanToSink :: (MonadIO io) => Chan a -> Sink a io ()+chanToSink chan = do+  val <- await+  case val of+    Nothing -> return ()+    Just v -> do+      liftIO (writeChan chan v)+      chanToSink chan+++{- |+  Merge two sources into one source. This is a concurrency abstraction.+  The resulting source will produce items from either of the input sources+  as they become available. As you would expect from a multi-producer,+  single-consumer concurrency abstraction, the ordering of items produced+  by each source is consistent relative to other items produced by+  that same source, but the interleaving of items from both sources+  is nondeterministic.+-}+merge :: (MonadIO io) => Source IO a -> Source IO b -> Source io (Either a b)+merge left right = do+  chan <- liftIO newChan+  (liftIO . void . forkIO) (left $= CL.map Left $$ chanToSink chan)+  (liftIO . void . forkIO) (right $= CL.map Right $$ chanToSink chan)+  chanToSource chan++
+ src/Network/Legion/ConnectionManager.hs view
@@ -0,0 +1,209 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE TemplateHaskell #-}+{- |+  This module manages connections to other nodes in the cluster.+-}+module Network.Legion.ConnectionManager (+  ConnectionManager,+  newConnectionManager,+  send,+  newPeers,+) where++import Prelude hiding (lookup)++import Control.Concurrent (Chan, writeChan, newChan, readChan)+import Control.Exception (try, SomeException)+import Control.Monad (void)+import Control.Monad.Logger (logInfo, logWarn)+import Control.Monad.Trans.Class (lift)+import Data.Binary (Binary, encode)+import Data.ByteString.Lazy (ByteString)+import Data.Map (toList, insert, empty, Map, lookup)+import Data.Text (pack)+import Network.Legion.BSockAddr (BSockAddr(BSockAddr))+import Network.Legion.Distribution (Peer)+import Network.Legion.Fork (forkC)+import Network.Legion.LIO (LIO)+import Network.Legion.StateMachine (PeerMessage)+import Network.Socket (SockAddr, Socket, socket, SocketType(Stream),+  defaultProtocol, connect, close, SockAddr(SockAddrInet, SockAddrInet6,+  SockAddrUnix, SockAddrCan), Family(AF_INET, AF_INET6, AF_UNIX, AF_CAN))+import Network.Socket.ByteString.Lazy (sendAll)++{- |+  A handle on the connection manager+-}+data ConnectionManager i o s = C (Chan (Message i o s))+instance Show (ConnectionManager i o s) where+  show _ = "ConnectionManager"+++{- |+  Create a new connection manager.+-}+newConnectionManager :: (Binary i, Binary o, Binary s)+  => Map Peer BSockAddr+  -> LIO (ConnectionManager i o s)+newConnectionManager initPeers = do+    chan <- lift newChan+    forkC "connection manager thread" $+      manager chan S {connections = empty}+    let cm = C chan+    newPeers cm initPeers+    return cm+  where+    manager :: (Binary s, Binary o, Binary i)+      => Chan (Message i o s)+      -> State i o s+      -> LIO ()+    manager chan state = lift (readChan chan) >>= handle state >>= manager chan++    handle :: (Binary i, Binary o, Binary s)+      => State i o s+      -> Message i o s+      -> LIO (State i o s)+    handle s@S {connections} (NewPeer peer addr) =+      case lookup peer connections of+        Nothing -> do+          conn <- connection addr+          return s {+              connections = insert peer conn connections+            }+        Just _ ->+          return s++    handle s@S {connections} (Send peer msg) = do+      case lookup peer connections of+        Nothing -> $(logWarn) . pack $ "unknown peer: " ++ show peer+        Just conn -> lift $ writeChan conn msg+      return s+++{- |+  Build a new connection.+-}+connection :: (Binary i, Binary o, Binary s)+  => SockAddr+  -> LIO (Chan (PeerMessage i o s))++connection addr = do+    chan <- lift newChan+    forkC ("connection to: " ++ show addr) $+      handle chan Nothing+    return chan+  where+    handle :: (Binary i, Binary o, Binary s)+      => Chan (PeerMessage i o s)+      -> Maybe Socket+      -> LIO ()+    handle chan so =+      lift (readChan chan) >>= sendWithRetry so . encode >>= handle chan++    {- |+      Open a socket.+    -}+    openSocket :: IO Socket+    openSocket = do+      so <- socket (fam addr) Stream defaultProtocol+      connect so addr+      return so++    {- |+      Try to send the payload over the socket, and if that fails, then try to+      create a new socket and retry sending the payload. Return whatever the+      "working" socket is.+    -}+    sendWithRetry :: Maybe Socket -> ByteString -> LIO (Maybe Socket)+    sendWithRetry Nothing payload = do+      result <- (lift . try) openSocket+      case result of+        Left err -> do+          $(logWarn) . pack+            $ "Can't connect to: " ++ show addr ++ ". Dropping message on "+            ++ "the floor: " ++ show payload ++ ". The error was: "+            ++ show (err :: SomeException)+          return Nothing+        Right so -> do+          result2 <- (lift . try) (sendAll so payload)+          case result2 of+            Left err -> $(logWarn) . pack+              $ "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+            Right _ -> return ()+          return (Just so)+    sendWithRetry (Just so) payload = do+      result <- (lift . try) (sendAll so payload)+      case result of+        Left err -> do+          $(logInfo) . pack+            $ "Socket to " ++ show addr ++ " died. Retrying on a new "+            ++ "socket. The error was: " ++ show (err :: SomeException)+          (lift . void) (try (close so) :: IO (Either SomeException ()))+          sendWithRetry Nothing payload+        Right _ ->+          return (Just so)+++{- |+  Send a message to a peer.+-}+send+  :: ConnectionManager i o s+  -> Peer+  -> PeerMessage i o s+  -> LIO ()+send (C chan) peer = lift . writeChan chan . Send peer+++{- |+  Tell the connection manager about a new peer.+-}+newPeer+  :: ConnectionManager i o s+  -> Peer+  -> SockAddr+  -> LIO ()+newPeer (C chan) peer addr = lift $ writeChan chan (NewPeer peer addr)+++{- |+  Tell the connection manager about all the peers known to the cluster state.+-}+newPeers :: ConnectionManager i o s -> Map Peer BSockAddr -> LIO ()+newPeers cm peers =+    mapM_ oneNewPeer (toList peers)+  where+    oneNewPeer (peer, BSockAddr addy) = newPeer cm peer addy+++{- |+  The internal state of the connection manager.+-}+data State i o s = S {+    connections :: Map Peer (Chan (PeerMessage i o s))+  }+++{- |+  The types of messages that the ConnectionManager understands.+-}+data Message i o s+  = NewPeer Peer SockAddr+  | Send Peer (PeerMessage i o s)+++{- |+  Guess the family of a `SockAddr`.+-}+fam :: SockAddr -> Family+fam SockAddrInet {} = AF_INET+fam SockAddrInet6 {} = AF_INET6+fam SockAddrUnix {} = AF_UNIX+fam SockAddrCan {} = AF_CAN++
+ src/Network/Legion/Distribution.hs view
@@ -0,0 +1,189 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{- |+  This module defines the data structures and functions used for handling the+  key space distribution.+-}+module Network.Legion.Distribution (+  ParticipationDefaults,+  Peer,+  empty,+  modify,+  findPartition,+  rebalanceAction,+  RebalanceAction(..),+  newPeer,+) where++import Prelude hiding (null)++import Data.Binary (Binary)+import Data.Function (on)+import Data.List (sort, sortBy)+import Data.Set (Set, toList)+import Data.UUID (UUID)+import GHC.Generics (Generic)+import Network.Legion.KeySet (KeySet, member, (\\), null)+import Network.Legion.LIO (LIO)+import Network.Legion.PartitionKey (PartitionKey)+import Network.Legion.UUID (getUUID)+import qualified Data.Set as Set+import qualified Network.Legion.KeySet as KS+++{- |+  The way to identify a peer.+-}+newtype Peer = Peer UUID deriving (Show, Binary, Eq, Ord)+++{- |+  The distribution of partitions and partition replicas among the cluster.+-}+newtype ParticipationDefaults = D {+    unD :: [(KeySet, Set Peer)]+  } deriving (Show, Binary)+++{- |+  Constuct a distribution that contains no partitions.+-}+empty :: ParticipationDefaults++empty = D []+++{- |+  Find the peers that own the specified partition.+-}+findPartition :: PartitionKey -> ParticipationDefaults -> Set Peer++findPartition k d =+  case [ps | (ks, ps) <- unD d, k `member` ks] of+    [ps] -> ps+    _ -> error+      $ "No exact mach for key in distribution. This means there is a bug in "+      ++ "the module `Network.Legion.Distribution`. Please report this bug "+      ++ "via github: " ++ show (k, d)+++{- |+  Modify the default participation for the key set.+-}+modify+  :: (Set Peer -> Set Peer)+  -> KeySet+  -> ParticipationDefaults+  -> ParticipationDefaults++modify fun keyset =+    {-+      doModify can produce key ranges that contain zero keys, which is+      why the `filter`.+    -}+    D . filter (not . null . fst) . doModify keyset . unD+  where+    doModify ks [] = [(ks, fun Set.empty)]+    doModify ks ((r, ps):dist) =+      let {+        unaffected = r \\ ks;+          affected = r \\ unaffected;+         remaining = ks \\ affected;+      } in+      (unaffected, ps):(affected, fun ps):doModify remaining dist+++{- |+  Return the best action, if any, that the indicated peer should take to+  rebalance an unbalanced distribution.+-}+rebalanceAction+  :: Peer+  -> Set Peer+  -> ParticipationDefaults+  -> Maybe RebalanceAction+rebalanceAction self allPeers (D dist) =+    rebuild+    {- TODO rebalance -}+  where+    _rebalance :: a+    _rebalance = error "rebalance undefined"+    rebuild =+      let+        underserved = [+            (ks, ps)+            | (ks, ps) <- dist+            , Set.size ps < 3+            , not (self `Set.member` ps)+          ]+        mostUnderserved = sortBy (compare `on` Set.size . snd) underserved+      in case mostUnderserved of+        [] -> Nothing+        (ks, ps):_ -> +          let+            candidateHosts = toList (allPeers Set.\\ ps)+            bestHosts = sort [(weightOf p, p) | p <- candidateHosts]+          in case bestHosts of+            {- we are the best host -}+            (_, candidate):_ | candidate == self -> Just (Invite ks)+            _ -> Nothing++    weightOf p = sum [KS.size ks | (ks, ps) <- dist, p `Set.member` ps]++--     -- TODO: first figure out if any replicas need re-building.+--     case sortBy (weight `on` snd) (toList dist) of+--       (p, keyspace):remaining | p == peer ->+--         case reverse remaining of+--           [] -> Nothing+--           (target, targetSpace):_ ->+--             let+--                 {- |+--                   Keys that already exist at the target are not eligible+--                   for being moved.+--                 -}+--                 eligibleSpace = keyspace \\ targetSpace+--                 migrationSize = (size keyspace - size targetSpace) `div` 2+--                 migrants = pickMigrants migrationSize eligibleSpace+--             in+--             case migrants of+--               Just keys -> Just (Move target keys)+--               Nothing -> Nothing+--       _ -> Nothing+--   where+--     weight+--       :: KeySet+--       -> KeySet+--       -> Ordering+--     weight = flip compare `on` size+-- +--     pickMigrants :: Integer -> KeySet -> Maybe KeySet+--     pickMigrants n keyspace =+--       let migrants = take n keyspace in+--       if size migrants > 0+--         then Just migrants+--         else Nothing +++{- |+  The actions that are taken in order to build a balanced cluster.+-}+data RebalanceAction+  = Invite KeySet+  deriving (Show, Generic)+instance Binary RebalanceAction+++{- |+  Create a new peer.+-}+newPeer :: LIO Peer+newPeer = Peer <$> getUUID+++-- {- |+--   Trace helper+-- -}+-- t :: (Show a) => String -> a -> a+-- t msg a = trace (msg ++ ": " ++ show a) a++
+ src/Network/Legion/Fork.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE TemplateHaskell #-}+{- |+  This module holds `forkC`, because we use it in at  least two other modules.+-}+module Network.Legion.Fork (+  forkC+) where++import Control.Concurrent (forkIO)+import Control.Exception (SomeException, try)+import Control.Monad (void)+import Control.Monad.Logger (logError, askLoggerIO, runLoggingT)+import Control.Monad.Trans.Class (lift)+import Data.Text (pack)+import Network.Legion.LIO (LIO)+import System.Exit (ExitCode(ExitFailure))+import System.IO (hPutStrLn, stderr)+import System.Posix.Process (exitImmediately)++{- |+  Forks a critical thread. "Critical" in this case means that if the thread+  crashes for whatever reason, then the program cannot continue correctly, so+  we should crash the program instead of running in some kind of zombie broken+  state.+-}+forkC+  :: String+    -- ^ The name of the critical thread, used for logging.+  -> LIO ()+    -- ^ The IO to execute.+  -> LIO ()+forkC name io = do+  logging <- askLoggerIO+  lift . void . forkIO $ do+    result <- try (runLoggingT io logging)+    case result of+      Left err -> do+        let msg =+              "Exception caught in critical thread " ++ show name+              ++ ". We are crashing the entire program because we can't "+              ++ "continue without this thread. The error was: "+              ++ show (err :: SomeException)+        -- write the message to every place we can think of.+        (`runLoggingT` logging) . $(logError) . pack $ msg+        putStrLn msg+        hPutStrLn stderr msg+        exitImmediately (ExitFailure 1)+      Right v -> return v+
+ src/Network/Legion/KeySet.hs view
@@ -0,0 +1,205 @@+{-# LANGUAGE DeriveGeneric #-}+{- |+  This module contains the `KeySet` data type and operations. `KeySet`,+  conceptually, has the same meaning as @Set PartitionKey@, but it is optimized+  for non-sparse continuous ranges of included values that could never fit into+  memory in an actual @Set PartitionKey@.+-}+module Network.Legion.KeySet (+  KeySet,+  take,+  size,+  union,+  unions,+  member,+  (\\),+  empty,+  null,+  fromRange,+  full+) where++import Prelude hiding (take, null)++import Data.Binary (Binary(put, get))+import Data.Ranged (Range(Range), RSet, rSetEmpty, Boundary(BoundaryBelow,+  BoundaryAbove, BoundaryAboveAll, BoundaryBelowAll), makeRangedSet,+  rSetHas, rSetUnion, (-!-), unsafeRangedSet, rSetRanges)+import GHC.Generics (Generic)+import Network.Legion.PartitionKey (PartitionKey(K, unkey))+++{- |+  Represents a set of partition keys. This type is intended to have set+  semantics, but unlike `Data.Set.Set`, it performs well with dense sets+  because it only stores the set of continuous ranges in memory.+-}+newtype KeySet = S {unS :: RSet PartitionKey} deriving (Show, Eq)++instance Binary KeySet where+  put =+      put . fmap encodeRange . rSetRanges . unS+    where+      encodeRange+        :: Range PartitionKey+        -> (BinBoundary PartitionKey, BinBoundary PartitionKey)+      encodeRange (Range a b) = (boundaryToBin a, boundaryToBin b)++      boundaryToBin :: Boundary a -> BinBoundary a+      boundaryToBin (BoundaryBelow a) = BinBelow a+      boundaryToBin (BoundaryAbove a) = BinAbove a+      boundaryToBin BoundaryBelowAll = BinBelowAll+      boundaryToBin BoundaryAboveAll = BinAboveAll+  get =+      (S . unsafeRangedSet . fmap decodeRange) <$> get+    where+      decodeRange+        :: (BinBoundary PartitionKey, BinBoundary PartitionKey)+        -> Range PartitionKey+      decodeRange (a, b) = Range (binToBoundary a) (binToBoundary b)++      binToBoundary :: BinBoundary a -> Boundary a+      binToBoundary (BinBelow a) = BoundaryBelow a+      binToBoundary (BinAbove a) = BoundaryAbove a+      binToBoundary BinBelowAll = BoundaryBelowAll+      binToBoundary BinAboveAll = BoundaryAboveAll++++{- |+  Construct the set of all partition keys within the specified range. Both the+  start element and the end element are inclusive.+-}+fromRange :: PartitionKey -> PartitionKey -> KeySet+fromRange a b+  | a > b = fromRange b a+  | otherwise = S (makeRangedSet [Range (BoundaryBelow a) (BoundaryAbove b)])+++{- |+  Construct an empty `KeySet`.+-}+empty :: KeySet+empty = S rSetEmpty+++{- |+  Construct a KeySet containing all keys.+-}+full :: KeySet+full = fromRange minBound maxBound+++{- |+  Check if a key range is empty or not.+-}+null :: KeySet -> Bool+null = (0 >=) . size+++{- |+  Take the difference of the two sets.+-}+(\\) :: KeySet -> KeySet -> KeySet+S a \\ S b = S (a -!- b)+++{- |+  Test for set membership.+-}+member :: PartitionKey -> KeySet -> Bool+member k = flip rSetHas k . unS+++{- |+  Take the union of the two sets.+-}+union :: KeySet -> KeySet -> KeySet+union (S a) (S b) = S (a `rSetUnion` b)+++{- |+  Take the union of a list of sets.+-}+unions :: [KeySet] -> KeySet+unions = foldr union empty+++{- |+  Used to help with the Binary instance of KeySet.+-}+data BinBoundary a+  = BinAbove a+  | BinBelow a+  | BinAboveAll+  | BinBelowAll+  deriving (Generic)+instance (Binary a) => Binary (BinBoundary a)+++{- |+  Figure out how large a `KeySet` is.+-}+size :: KeySet -> Integer+size = sum . fmap rangeSize . rSetRanges . unS+++{- |+  Figure out how large a particular range is.+-}+rangeSize :: Range PartitionKey -> Integer+rangeSize (Range BoundaryBelowAll b) = rangeSize (Range (BoundaryBelow minBound) b)+rangeSize (Range BoundaryAboveAll b) = rangeSize (Range (BoundaryAbove maxBound) b)+rangeSize (Range a BoundaryBelowAll) = rangeSize (Range a (BoundaryBelow minBound))+rangeSize (Range a BoundaryAboveAll) = rangeSize (Range a (BoundaryAbove maxBound))+rangeSize (Range (BoundaryAbove a) (BoundaryAbove b)) = toI b - toI a+rangeSize (Range (BoundaryBelow a) (BoundaryBelow b)) = toI b - toI a+rangeSize (Range (BoundaryAbove a) (BoundaryBelow b)) = (toI b - toI a) - 1+rangeSize (Range (BoundaryBelow a) (BoundaryAbove b)) = (toI b - toI a) + 1+++{- |+  To help with `rangeSize`.+-}+toI :: PartitionKey -> Integer+toI = toInteger . unkey+++{- |+  Opposite of `toI`+-}+fromI :: Integer -> PartitionKey+fromI = K . fromInteger+++{- |+  Take the first n values from a KeySet.+-}+take :: Integer -> KeySet -> KeySet+take num set =+    S $ doTake num [] (rSetRanges (unS set))+  where+    doTake 0 acc _ = makeRangedSet acc+    doTake _ acc [] = makeRangedSet acc+    doTake n acc (first:remaining)+        | firstSize <= n =+            doTake (n - firstSize) (acc ++ [first]) remaining+        | otherwise =+            makeRangedSet (acc ++ [takeRange n first])+      where+        firstSize = rangeSize first++    takeRange+      :: Integer+      -> Range PartitionKey+      -> Range PartitionKey+    takeRange n (Range BoundaryBelowAll b) =+      takeRange n (Range (BoundaryBelow minBound) b)+    takeRange n (Range BoundaryAboveAll b) =+      takeRange n (Range (BoundaryAbove minBound) b)+    takeRange n (Range (BoundaryAbove a) _) =+      Range (BoundaryAbove a) (BoundaryAbove (fromI (toI a + n)))+    takeRange n (Range (BoundaryBelow a) _) =+      Range (BoundaryBelow a) (BoundaryBelow (fromI (toI a + n)))++
+ src/Network/Legion/LIO.hs view
@@ -0,0 +1,17 @@+{- |+  This module defines the specialized logging monad in which legion+  opperates.+-}+module Network.Legion.LIO (+  LIO+) where++import Control.Monad.Logger (LoggingT)+++{- |+  The logging monad in wich legion operates.+-}+type LIO = LoggingT IO++
+ src/Network/Legion/PartitionKey.hs view
@@ -0,0 +1,95 @@+{- |+  This module contains the PartitionKey type.+-}+module Network.Legion.PartitionKey (+  PartitionKey(..),+  toHex,+  fromHex+) where+++import Data.Attoparsec.ByteString (parseOnly, atEnd)+import Data.Attoparsec.ByteString.Char8 (hexadecimal)+import Data.Binary (Binary(put, get))+import Data.Bits (testBit)+import Data.Bool (bool)+import Data.ByteString.Char8 (pack)+import Data.DoubleWord (Word256(Word256), Word128(Word128))+import Data.Ranged (DiscreteOrdered(adjacent, adjacentBelow))+import Data.Word (Word64)+++{- |+  This is how partitions are identified and referenced.+-}+newtype PartitionKey = K {unkey :: Word256} deriving (Eq, Ord, Show, Bounded)++instance Binary PartitionKey where+  put (K (Word256 (Word128 a b) (Word128 c d))) = put (a, b, c, d)+  get = do+    (a, b, c, d) <- get+    return (K (Word256 (Word128 a b) (Word128 c d)))++instance DiscreteOrdered PartitionKey where+  adjacent (K a) (K b) = a < b && succ a == b+  adjacentBelow (K k) = if k == minBound then Nothing else Just (K (pred k))+++{- |+  Convert a `PartitionKey` into a hex string+-}+toHex :: PartitionKey -> String+toHex (K (Word256 (Word128 a b) (Word128 c d))) =+  concatMap toHex64 [a, b, c, d]+++{- |+  Convert a `Word64` into a hex string.++  I know I'm going to hell for this, but I just can't abide the+  @hexstring@ package pulling @aeson@ into our dependency tree.+-}+toHex64 :: Word64 -> String+toHex64 w = fmap (digit . quad) [15, 14..0]+  where+    quad :: Int -> (Int, Int, Int, Int)+    quad n = let base = n * 4 in (base + 3, base + 2, base + 1, base)++    digit :: (Int, Int, Int, Int) -> Char+    digit (a, b, c, d) =+      case (testBit w a, testBit w b, testBit w c, testBit w d) of+        (False, False, False, False) -> '0'+        (False, False, False, True)  -> '1'+        (False, False, True,  False) -> '2'+        (False, False, True,  True)  -> '3'+        (False, True,  False, False) -> '4'+        (False, True,  False, True)  -> '5'+        (False, True,  True,  False) -> '6'+        (False, True,  True,  True)  -> '7'+        (True,  False, False, False) -> '8'+        (True,  False, False, True)  -> '9'+        (True,  False, True,  False) -> 'a'+        (True,  False, True,  True)  -> 'b'+        (True,  True,  False, False) -> 'c'+        (True,  True,  False, True)  -> 'd'+        (True,  True,  True,  False) -> 'e'+        (True,  True,  True,  True)  -> 'f'+++{- |+  Maybe convert a hex string into a partition key+-}+fromHex :: String -> Either String PartitionKey+fromHex str+    | length str > 64 =+        Left "trailing characters while parsing hex PartitionKey"+    | otherwise =+        K <$> parseOnly parser (pack str)+  where+    parser = do+      w <- hexadecimal+      atEnd >>= bool+        (fail "not a valid hex string")+        (return w)++
+ src/Network/Legion/PartitionState.hs view
@@ -0,0 +1,182 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{- |+  This module contains types related to the partition state.+-}+module Network.Legion.PartitionState (+  PartitionPropState,+  PartitionPowerState,+  ask,+  mergeEither,+  actions,+  new,+  initProp,+  participating,+  getPowerState,+  delta,+  heartbeat,+  participate,+  projParticipants,+  projected,+  infimum,+) where++import Data.Binary (Binary)+import Data.Default.Class (Default)+import Data.Set (Set)+import Data.Time.Clock (UTCTime)+import Network.Legion.Distribution (Peer)+import Network.Legion.PartitionKey (PartitionKey)+import Network.Legion.PowerState (ApplyDelta)+import Network.Legion.Propagation (PropState, PropPowerState)+import qualified Network.Legion.Propagation as P++{- |+  This is an opaque representation of your application's partition state.+  Internally, this represents the complete, nondeterministic set of states the+  partition can be in as a result of concurrency, eventual consistency, and all+  the other distributed systems reasons your partition state might have more+  than one value.++  You can save these guys to disk in your `Network.Legion.Persistence`+  layer by using its `Binary` instance.+-}+newtype PartitionPowerState i s = PartitionPowerState {+    unPowerState :: PropPowerState PartitionKey s Peer i+  } deriving (Show, Binary)+++{- |+  A reification of `PropState`, representing the propagation state of the+  partition state.+-}+newtype PartitionPropState i s = PartitionPropState {+    unPropState :: PropState PartitionKey s Peer i+  } deriving (Eq, Show)+++-- {- |+--   A convenient alias for the partition state infimum.+-- -}+-- type PartitionInfimum s = Infimum s Peer+++{- |+  Get the projected partition state value.+-}+ask :: (ApplyDelta i s) => PartitionPropState i s -> s+ask = P.ask . unPropState+++{- |+  Try to merge two partition states.+-}+mergeEither :: (Show i, Show s, ApplyDelta i s)+  => Peer+  -> PartitionPowerState i s+  -> PartitionPropState i s+  -> Either String (PartitionPropState i s)+mergeEither peer ps prop =+  PartitionPropState <$>+    P.mergeEither peer (unPowerState ps) (unPropState prop)+++{- |+  Get the peers which require action (i.e. Send), if any, and the+  powerstate version to send to those peers, and the new propagation+  state that is applicable after those actions have been taken.+-}+actions+  :: PartitionPropState i s+  -> (Set Peer, PartitionPowerState i s, PartitionPropState i s)+actions prop =+  let (peers, ps, newProp) = P.actions (unPropState prop)+  in (peers, PartitionPowerState ps, PartitionPropState newProp)+++{- |+  Create a new, default, PartitionPropState.+-}+new :: (Default s)+  => PartitionKey+    {- ^ The power state origin, which is the partition key. -}+  -> Peer+    {- ^ self -}+  -> Set Peer+    {- ^ The default participation. -}+  -> PartitionPropState i s+new key self = PartitionPropState . P.new key self+++{- |+  Initialize a `PartitionPropState` based on the initial underlying+  partition power state.+-}+initProp :: (ApplyDelta i s)+  => Peer+  -> PartitionPowerState i s+  -> PartitionPropState i s+initProp self = PartitionPropState . P.initProp self . unPowerState+++{- |+  Return `True` if the local peer is participating in the partition+  power state.+-}+participating :: PartitionPropState i s -> Bool+participating = P.participating . unPropState+++{- |+  Get an opaque encapsulation of the partition power state, for+  transferring accros the network or whatever.+-}+getPowerState :: PartitionPropState i s -> PartitionPowerState i s+getPowerState = PartitionPowerState . P.getPowerState . unPropState+++{- | Apply a delta to the partition state.  -}+delta :: (ApplyDelta i s)+  => i+  -> PartitionPropState i s+  -> PartitionPropState i s+delta d = PartitionPropState . P.delta d . unPropState+++{- | Move time forward for the propagation state.  -}+heartbeat :: UTCTime -> PartitionPropState i s -> PartitionPropState i s+heartbeat now = PartitionPropState . P.heartbeat now . unPropState+++{- |+  Allow a participant to join in the distributed nature of the power state.+-}+participate :: (ApplyDelta i s)+  => Peer+  -> PartitionPropState i s+  -> PartitionPropState i s+participate peer = PartitionPropState . P.participate peer . unPropState+++{- |+  Return the projected peers which are participating in the partition+  state.+-}+projParticipants :: PartitionPropState i s -> Set Peer+projParticipants = P.projParticipants . unPropState+++{- |+  Get the projected value of a `PartitionPowerState`.+-}+projected :: (ApplyDelta i s) => PartitionPowerState i s -> s+projected = P.projected . unPowerState+++{- |+  Get the infimum value of a `PartitionPowerState`+-}+infimum :: PartitionPowerState i s -> s+infimum = P.infimum . unPowerState++
+ src/Network/Legion/PowerState.hs view
@@ -0,0 +1,392 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-}+{- |+  This module contains the fundamental distributed data object.+-}+module Network.Legion.PowerState (+  PowerState,+  Infimum(..),+  ApplyDelta(..),+  StateId,+  new,+  merge,+  mergeMaybe,+  mergeEither,+  acknowledge,+  participate,+  disassociate,+  projectedValue,+  infimumValue,+  infimumParticipants,+  allParticipants,+  projParticipants,+  divergent,+  divergences,+  delta,+) where++import Prelude hiding (null)++import Data.Binary (Binary(put, get))+import Data.Default.Class (Default(def))+import Data.DoubleWord (Word256(Word256), Word128(Word128))+import Data.Map (Map, filterWithKey, unionWith, minViewWithKey, keys,+  toDescList, toAscList, fromAscList)+import Data.Set (Set, union, (\\), null, member)+import Data.Word (Word64)+import GHC.Generics (Generic)+import qualified Data.Map as Map (insert, empty)+import qualified Data.Set as Set (insert, empty, delete)+++{- |+  This represents the set of all possible future values of @s@, in a+  distributed, monotonically increasing environment.+-}+data PowerState o s p d = PowerState {+     origin :: o,+    infimum :: Infimum s p,+     deltas :: Map (StateId p) (Delta p d, Set p)+  } deriving (Generic, Show, Eq)+instance (Binary o, Binary s, Binary p, Binary d) => Binary (PowerState o s p d)+++{- |+  `Infimum` is the infimum, or greatest lower bound, of the possible+  values of @s@.+-}+data Infimum s p = Infimum {+         stateId :: StateId p,+    participants :: Set p,+      stateValue :: s+  } deriving (Generic, Show)+instance (Binary s, Binary p) => Binary (Infimum s p)+instance (Eq p) => Eq (Infimum s p) where+  Infimum s1 _ _ == Infimum s2 _ _ = s1 == s2+instance (Ord p) => Ord (Infimum s p) where+  compare (Infimum s1 _ _) (Infimum s2 _ _) = compare s1 s2+++{- |+  `StateId` is a monotonically increasing, totally ordered identification+  value which allows us to lend the attribute of monotonicity to state+  operations which would not naturally be monotonic.+-}+data StateId p+  = BottomSid+  | Sid Word256 p+  deriving (Generic, Eq, Ord, Show)+instance (Binary p) => Binary (StateId p) where+  put = put . toMaybe+    where+      toMaybe :: StateId p -> Maybe (Word64, Word64, Word64, Word64, p)+      toMaybe BottomSid =+        Nothing+      toMaybe (Sid (Word256 (Word128 a b) (Word128 c d)) p) =+        Just (a, b, c, d, p)+  get = do+    theThing <- get+    return $ case theThing of+      Nothing -> BottomSid+      Just (a, b, c, d, p) -> Sid (Word256 (Word128 a b) (Word128 c d)) p+instance Default (StateId p) where+  def = BottomSid+++{- |+  `Delta` is how we represent mutations to the power state.+-}+data Delta p d+  = Join p+  | UnJoin p+  | Delta d+  deriving (Generic, Show, Eq)+instance (Binary p, Binary d) => Binary (Delta p d)+++{- |+  The class which allows for delta application.+-}+class ApplyDelta i s where+  {- |+    Apply a delta to a state value. *This function MUST be total!!!*+  -}+  apply :: i -> s -> s+++{- |+  Construct a new PowerState with the given origin and initial participants+-}+new :: (Default s) => o -> Set p -> PowerState o s p d+new origin participants =+  PowerState {+      origin,+      infimum = Infimum {+          stateId = def,+          participants,+          stateValue = def+        },+      deltas = Map.empty+    }+++{- |+  Monotonically merge the information in two power states.  The resulting+  power state may have a higher infimum value, but it will never have+  a lower one. This function is not total. Only `PowerState`s that originated+  from the same `new` call can be merged.+-}+merge :: (Eq o, ApplyDelta d s, Ord p, Show o, Show s, Show p, Show d)+  => PowerState o s p d+  -> PowerState o s p d+  -> PowerState o s p d+merge a b = either error id (mergeEither a b)+++{- |+  Like `merge`, but safe. Returns `Nothing` if the two power states do+  not share the same origin.+-}+mergeMaybe :: (Eq o, ApplyDelta d s, Ord p, Show o, Show s, Show p, Show d)+  => PowerState o s p d+  -> PowerState o s p d+  -> Maybe (PowerState o s p d)+mergeMaybe a b = either (const Nothing) Just (mergeEither a b)+++{- |+  Like `mergeMaybe`, but returns a human-decipherable error message of+  exactly what went wrong.+-}+mergeEither :: (Eq o, ApplyDelta d s, Ord p, Show o, Show s, Show p, Show d)+  => PowerState o s p d+  -> PowerState o s p d+  -> Either String (PowerState o s p d)+mergeEither (PowerState o1 i1 d1) (PowerState o2 i2 d2) | o1 == o2 =+    Right . reduce $ PowerState {origin = o1, infimum, deltas}+  where+    infimum = max i1 i2+    deltas = removeObsolete (unionWith mergeKnowns d1 d2)+    removeObsolete = filterWithKey (\k _ -> k > stateId infimum)+    mergeKnowns (d, s1) (_, s2) = (d, s1 `union` s2)++mergeEither a b = Left+  $ "PowerStates " ++ show a ++ " and " ++ show b ++ " do not share the "+  ++ "same origin, and cannot be merged."+++{- |+  Record the fact that the participant acknowledges the information+  contained in the powerset. The implication is that the participant+  **must** base all future operations on the result of this function.+-}+acknowledge :: (ApplyDelta d s, Ord p)+  => p+  -> PowerState o s p d+  -> PowerState o s p d+acknowledge p ps@PowerState {deltas} =+    reduce ps {deltas = fmap ackOne deltas}+  where+    ackOne (d, acks) = (d, Set.insert p acks)+++{- |+  Allow a participant to join in the distributed nature of the power state.+-}+participate :: (ApplyDelta d s, Ord p)+  => p+  -> PowerState o s p d+  -> PowerState o s p d+participate p ps@PowerState {deltas} = reduce ps {+    deltas = Map.insert (nextId p ps) (Join p, Set.empty) deltas+  }+++{- |+  Indicate that a participant is removing itself from participating in+  the distributed power state.+-}+disassociate :: (ApplyDelta d s, Ord p)+  => p+  -> PowerState o s p d+  -> PowerState o s p d+disassociate p ps@PowerState {deltas} = reduce ps {+    deltas = Map.insert (nextId p ps) (UnJoin p, Set.empty) deltas+  }+++{- |+  Introduce a change to the PowerState on behalf of the participant.+-}+delta :: (ApplyDelta d s, Ord p)+  => p+  -> d+  -> PowerState o s p d+  -> PowerState o s p d+delta p d ps@PowerState {deltas} = reduce ps {+    deltas = Map.insert (nextId p ps) (Delta d, Set.empty) deltas+  }+++{- |+  Return the current projected value of the power state.+-}+projectedValue :: (ApplyDelta d s) => PowerState o s p d -> s+projectedValue PowerState {infimum = Infimum {stateValue}, deltas} =+    foldr apply stateValue changes+  where+    changes = foldr getDeltas [] (toDescList deltas)+    getDeltas (_, (Delta d, _)) acc = d:acc+    getDeltas _ acc = acc+++{- |+  Return the current infimum value of the power state.+-}+infimumValue :: PowerState o s p d -> s+infimumValue PowerState {infimum = Infimum {stateValue}} = stateValue+++{- |+  Gets the known participants at the infimum.+-}+infimumParticipants :: PowerState o s p d -> Set p+infimumParticipants PowerState {infimum = Infimum {participants}} = participants+++{- |+  Get all known participants. This includes participants that are+  projected for removal.+-}+allParticipants :: (Ord p) => PowerState o s p d -> Set p+allParticipants PowerState {+    infimum = Infimum {participants},+    deltas+  } =+    foldr updateParticipants participants (toDescList deltas)+  where+    updateParticipants (_, (Join p, _)) = Set.insert p+    updateParticipants _ = id+++{- |+  Get all the projected participants. This does not include participants that+  are projected for removal.+-}+projParticipants :: (Ord p) => PowerState o s p d -> Set p+projParticipants PowerState {+    infimum = Infimum {participants},+    deltas+  } =+    foldr updateParticipants participants (toDescList deltas)+  where+    updateParticipants (_, (Join p, _)) = Set.insert p+    updateParticipants (_, (UnJoin p, _)) = Set.delete p+    updateParticipants _ = id+++{- |+  Returns the participants that we think might be diverging. In this+  context, a peer is "diverging" if there is a delta that the peer has+  not acknowledged.+-}+divergent :: (Ord p) => PowerState o s p d -> Set p+divergent PowerState {+    infimum = Infimum {participants},+    deltas+  } =+    accum participants Set.empty (toAscList deltas)+  where+    {- |+      `accum` mnemonics:+        j = pro(J)ected participants+        d = (D)ivergent participants+        a = peers that have (A)cknowledged an update.+        p = (P)eer that is joining or unjoining+    -}+    accum _ d [] = d++    accum j d ((_, (Join p, a)):moreDeltas) =+      let+        j2 = Set.insert p j+        d2 = (j2 \\ a) `union` d+      in+        accum j2 d2 moreDeltas++    accum j d ((_, (UnJoin p, a)):moreDeltas) =+      let+        j2 = Set.delete p j+        d2 = (j2 \\ a) `union` d+      in+        accum j2 d2 moreDeltas++    accum j d ((_, (Delta _, a)):moreDeltas) =+      let+        d2 = (j \\ a) `union` d+      in+        accum j d2 moreDeltas+++{- |+  Return the deltas that are unknown to the specified peer.+-}+divergences :: (Ord p) => p -> PowerState o s p d -> Map (StateId p) d+divergences peer PowerState {deltas} =+  fromAscList [+    (sid, d)+    | (sid, (Delta d, p)) <- toAscList deltas+    , not (peer `member` p)+  ]+++{- |+  This helper function is responsible for figuring out if the power state+  has enough information to derive a new infimum value. In other words,+  this is where garbage collection happens.+-}+reduce :: (ApplyDelta d s, Ord p) => PowerState o s p d -> PowerState o s p d+reduce ps@PowerState {+    infimum = infimum@Infimum {participants, stateValue},+    deltas+  } =+    case minViewWithKey deltas of+      Nothing -> ps+      Just ((i, (update, acks)), newDeltas) ->+        if not . null $ participants \\ acks+          then ps+          else case update of+            Join p -> reduce ps {+                infimum = infimum {+                    stateId = i,+                    participants = Set.insert p participants+                  },+                deltas = newDeltas+              }+            UnJoin p -> reduce ps {+                infimum = infimum {+                    stateId = i,+                    participants = Set.delete p participants+                  },+                deltas = newDeltas+              }+            Delta d -> reduce ps {+                infimum = infimum {+                    stateId = i,+                    stateValue = apply d stateValue+                  },+                deltas = newDeltas+              }+++{- |+  A utility function that constructs the next `StateId` on behalf of+  a participant.+-}+nextId :: (Ord p) => p -> PowerState o s p d -> StateId p+nextId p PowerState {infimum = Infimum {stateId}, deltas} =+  case maximum (stateId:keys deltas) of+    BottomSid -> Sid 0 p+    Sid ord _ -> Sid (succ ord) p++
+ src/Network/Legion/Propagation.hs view
@@ -0,0 +1,348 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-}+{- |+  This module defines how to propagate a PowerState amoung its participants.+-}+module Network.Legion.Propagation (+  PropState,+  PropPowerState,+  merge,+  mergeMaybe,+  mergeEither,+  heartbeat,+  delta,+  actions,+  new,+  initProp,+  getPowerState,+  ask,+  participate,+  getSelf,+  divergences,+  participating,+  allParticipants,+  projParticipants,+  projected,+  infimum,+) where++import Prelude hiding (lookup)++import Data.Binary (Binary)+import Data.Default.Class (Default)+import Data.Map (Map, lookup)+import Data.Maybe (fromMaybe)+import Data.Set (member, Set)+import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime)+import Data.Time.Format () -- For `instance Show UTCTime`+import Network.Legion.PowerState (PowerState, divergent, ApplyDelta,+  acknowledge, projectedValue, StateId)+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Network.Legion.PowerState as PS+++{- |+  Internally, we use `Maybe UTCTime` to represent the current time, so that we+  have a convenient way to represent "now" (i.e. `Nothing`) without using `IO`.+  This type aliases gives us a convenient way to spell `Maybe UTCTime`.+-}+type Time = Maybe UTCTime+++{- |+  Opaque Propagation State. Values of this type encapsulate the+  current value of a power state along with state having to do with+  the distribution of that powerstate among its participants. The+  power state is not directly accessible, but rather must be accessed+  through functions provided by this module. In addition to providing+  a more coherent hierarchy of abstraction, this also helps ensure that+  the power state remains consistent with the state of its propagation+  throughout the network.+-}+data PropState o s p d = PropState {+    powerState :: PowerState o s p d,+    peerStates :: Map p PeerStatus,+          self :: p,+           now :: Time+  } deriving (Eq, Show)+++{- |+  This type is an opaque representation of the underlying power state. It+  exists because we sometimes want to pack up the power state and ship+  it over the network, but we don't want any code outside of this module+  to operate on it.+-}+newtype PropPowerState o s p d = PropPowerState {+    unPowerState :: PowerState o s p d+  } deriving (Show, Binary)+++{- |+  Retriev the current projected value of the underlying state.+-}+ask :: (ApplyDelta d s) => PropState o s p d -> s+ask = projectedValue . powerState+++{- |+  Create a new propagation state based on an existing power state.+-}+initProp :: (ApplyDelta d s, Eq p, Ord p)+  => p+  -> PropPowerState o s p d+  -> PropState o s p d+initProp self ps =+  let powerState = acknowledge self (unPowerState ps)+  in PropState {+      powerState = powerState,+      peerStates = Map.fromAscList [+          (p, NeedsSendAt Nothing)+          | p <- Set.toAscList (divergent powerState)+        ],+      self,+      now = Nothing+    }+++{- |+  Return an opaque representation of the power state, for transfer across+  the network, or whatever.+-}+getPowerState :: PropState o s p d -> PropPowerState o s p d+getPowerState = PropPowerState . powerState+++{- |+  The propagation state of a single remote participant.+-}+data PeerStatus+  = NeedsSendAt Time+  | NeedsAck+  deriving (Show, Eq)+++{- |+  Create a new propagation state.+-}+new :: (Default s) => o -> p -> Set p -> PropState o s p d +new origin self participants =+  PropState {+      powerState = PS.new origin participants,+      peerStates = Map.empty,+      self,+      now = Nothing+    }+++{- |+  Like `merge`, but total. `mergeEither` returns a human readable reason why+  the foreign powerstate can't be merged in the event of an error.+-}+{-+  This algorithm is weaksauce. We need to find someone who knows a lot about+  gossip protocols to fix this.+-}+mergeEither :: (Eq o, Ord p, Show o, Show s, Show p, Show d, ApplyDelta d s)+  => p+  -> PropPowerState o s p d+  -> PropState o s p d+  -> Either String (PropState o s p d)+mergeEither source kernel (prop@PropState {powerState, peerStates, self, now}) =+  let ps = unPowerState kernel+  in case acknowledge self <$> PS.mergeEither ps powerState of+    Left err -> Left err+    Right merged -> Right prop {+        powerState = merged,+        peerStates =+          Map.fromList $ [+              (p, ns)+              | p <- Set.toList (divergent merged)+              , let ns = fromMaybe (NeedsSendAt now) (lookup p peerStates)+            ]+          +++            {-+              If the source of the foreign powerstate believes we+              are divergent, then it is going to keep sending updates+              until someone clues it in. That someone is us for now.+            -}+            [(source, NeedsAck) | self `member` divergent ps]+      }+++{- |+  Like `merge`, but total. `mergeMaybe` returns `Nothing` if the foreign power+  state can't be merged.+-}+mergeMaybe :: (Eq o, Ord p, Show o, Show s, Show p, Show d, ApplyDelta d s)+  => p+  -> PropPowerState o s p d+  -> PropState o s p d+  -> Maybe (PropState o s p d)+mergeMaybe source ps prop =+  case mergeEither source ps prop of+    Left _ -> Nothing+    Right v -> Just v+++{- |+  Try to merge a foreign powerstate. The precondition is that the foreign+  powerstate shares the same origin as the local powerstate. If this+  precondition is not met, `error` will be called (making this function+  non-total). Using `mergeMaybe` or `mergeEither` is recommended.+-}+merge :: (Eq o, Ord p, Show o, Show s, Show p, Show d, ApplyDelta d s)+  => p+  -> PropPowerState o s p d+  -> PropState o s p d+  -> PropState o s p d+merge source ps prop =+  case mergeEither source ps prop of+    Left err -> error err+    Right v -> v+++{- |+  Time moves forward.+-}+heartbeat :: UTCTime -> PropState o s p d -> PropState o s p d+heartbeat newNow prop = prop {now = max (now prop) (Just newNow)}+++{- |+  Apply a delta.+-}+delta :: (Ord p, ApplyDelta d s)+  => d+  -> PropState o s p d+  -> PropState o s p d+delta d prop@PropState {self, powerState, now} =+  let newPowerState = acknowledge self (PS.delta self d powerState)+  in prop {+      powerState = newPowerState,+      peerStates = Map.fromAscList [+          (p, NeedsSendAt now)+          | p <- Set.toAscList (divergent newPowerState)+        ]+    }+++{- |+  Get the peers which require action (i.e. Send), if any, and the+  powerstate version to send to those peers, and the new propagation+  state that is applicable after those actions have been taken.+-}+actions :: (Eq p)+  => PropState o s p d+  -> (Set p, PropPowerState o s p d, PropState o s p d)+actions prop@PropState {powerState, peerStates, now} =+    (outOfDatePeers, PropPowerState powerState, newPropState)+  where+    outOfDatePeers = Set.fromAscList [+        p+        | (p, status) <- Map.toAscList peerStates+        , shouldSendNow status+      ]++    shouldSendNow NeedsAck = True+    shouldSendNow (NeedsSendAt time) = now > time++    newPropState = prop {+        peerStates = Map.fromAscList [+            (p, ns)+            {- Careful, this pattern omits `NeedsAck`. This is intentional. -}+            | (p, NeedsSendAt time) <- Map.toAscList peerStates+            , let ns = NeedsSendAt (nextTime time)+          ]+      }++    nextTime :: Time -> Time+    nextTime time =+      if now > time+        then addUTCTime gracePeriod <$> now+        else time+++{- |+  The grace period for receiving some response to an action.+-}+gracePeriod :: NominalDiffTime+gracePeriod = oneMinute+  where+    oneMinute = 60+++{- |+  Allow a participant to join in the distributed nature of the power state.+-}+participate :: (Ord p, ApplyDelta d s)+  => p+  -> PropState o s p d+  -> PropState o s p d+participate peer prop@PropState {powerState, now} =+  let newPowerState = PS.participate peer powerState+  in prop {+      powerState = newPowerState,+      peerStates = Map.fromAscList [+          (p, NeedsSendAt now)+          | p <- Set.toAscList (divergent newPowerState)+        ]+    }+++{- |+  Return the deltas that are unknown to the specified peer.+-}+divergences :: (Ord p) => p -> PropState o s p d -> Map (StateId p) d+divergences peer = PS.divergences peer . powerState+++{- |+  Return self.+-}+getSelf :: PropState o s p d -> p+getSelf = self+++{- |+  Return `True` if the local peer is participating in the underlying+  power state. This will return `True` even if the peer is projected+  for removal, because until the infimum catches up to that projection,+  this peer still has an obligation to participate.+-}+participating :: (Ord p) => PropState o s p d -> Bool+participating PropState{self, powerState} =+  self `member` PS.allParticipants powerState+++{- |+  Get all known participants. This includes participants that are+  projected for removal.+-}+allParticipants :: (Ord p) => PropState o s p d -> Set p+allParticipants = PS.allParticipants . powerState+++{- |+  Get all of the projected participants.+-}+projParticipants :: (Ord p) => PropState o s p d -> Set p+projParticipants = PS.projParticipants . powerState+++{- |+  Get the projected value of a PropPowerState.+-}+projected :: (ApplyDelta d s) => PropPowerState o s p d -> s+projected = PS.projectedValue . unPowerState+++{- |+  Get the infimum value of the PropPowerState.+-}+infimum :: PropPowerState o s p d -> s+infimum = PS.infimumValue . unPowerState++
+ src/Network/Legion/Runtime.hs view
@@ -0,0 +1,358 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+{- |+  This module is responsible for the runtime operation of the legion+  framework. This mostly means opening sockets and piping data around to the+  various connected pieces.+-}+module Network.Legion.Runtime (+  forkLegionary,+  runLegionary,+  StartupMode(..),+) where++import Control.Concurrent (forkIO)+import Control.Concurrent.Chan (writeChan, newChan, Chan)+import Control.Concurrent.MVar (newEmptyMVar, takeMVar, putMVar)+import Control.Monad (void, forever, join)+import Control.Monad.Catch (catchAll, try, SomeException, throwM)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Logger (logWarn, logError, logInfo, LoggingT,+  MonadLoggerIO, runLoggingT, askLoggerIO)+import Control.Monad.Trans.Class (lift)+import Data.Binary (encode)+import Data.Conduit (Source, ($$), (=$=), yield, await, awaitForever,+  transPipe, ConduitM, runConduit)+import Data.Conduit.Network (sourceSocket)+import Data.Conduit.Serialization.Binary (conduitDecode)+import Data.Map (Map)+import Data.Text (pack)+import Network.Legion.Admin (runAdmin)+import Network.Legion.Application (LegionConstraints, Legionary,+  RequestMsg)+import Network.Legion.BSockAddr (BSockAddr(BSockAddr))+import Network.Legion.ClusterState (ClusterPowerState)+import Network.Legion.Conduit (merge, chanToSink, chanToSource)+import Network.Legion.ConnectionManager (newConnectionManager, send,+  newPeers)+import Network.Legion.Distribution (Peer, newPeer)+import Network.Legion.Fork (forkC)+import Network.Legion.LIO (LIO)+import Network.Legion.PartitionKey (PartitionKey)+import Network.Legion.Settings (LegionarySettings(LegionarySettings,+  adminHost, adminPort, peerBindAddr, joinBindAddr))+import Network.Legion.StateMachine (stateMachine, LInput(J, P, R,+  A), JoinRequest(JoinRequest), JoinResponse(JoinOk, JoinRejected),+  LOutput(Send, NewPeers), AdminMessage, NodeState, PeerMessage,+  newNodeState)+import Network.Legion.UUID (getUUID)+import Network.Socket (Family(AF_INET, AF_INET6, AF_UNIX, AF_CAN),+  SocketOption(ReuseAddr), SocketType(Stream), accept, bindSocket,+  defaultProtocol, listen, setSocketOption, socket, SockAddr(SockAddrInet,+  SockAddrInet6, SockAddrUnix, SockAddrCan), connect, getPeerName, Socket)+import Network.Socket.ByteString.Lazy (sendAll)+import qualified Data.Conduit.List as CL+import qualified Network.Legion.ClusterState as C+++{- |+  Run the legion node framework program, with the given user definitions,+  framework settings, and request source. This function never returns+  (except maybe with an exception if something goes horribly wrong).++  For the vast majority of service implementations, you are going to need+  to implement some halfway complex concurrency in order to populate the+  request source, and to handle the responses. Unless you know exactly+  what you are doing, you probably want to use `forkLegionary` instead.+-}+runLegionary :: (LegionConstraints i o s)+  => Legionary i o s+    -- ^ The user-defined legion application to run.+  -> LegionarySettings+    -- ^ Settings and configuration of the legionary framework.+  -> StartupMode+  -> Source IO (RequestMsg i o)+    -- ^ A source of requests, together with a way to respond to the requets.+    {-+      We don't use `LIO` in the type signature here because we don't+      export the `LIO` symbol.+    -}+  -> LoggingT IO ()++runLegionary+    legionary+    settings@LegionarySettings {adminHost, adminPort}+    startupMode+    requestSource+  = do+    peerS <- loggingC =<< startPeerListener settings+    (nodeState, peers) <- makeNodeState settings startupMode+    cm <- newConnectionManager peers+    $(logInfo) . pack+      $ "The initial node state is: " ++ show nodeState+    adminS <- loggingC =<< runAdmin adminPort adminHost+    joinS <- loggingC (joinMsgSource settings)+    runConduit $+      (joinS `merge` (peerS `merge` (requestSource `merge` adminS)))+        =$= CL.map toMessage+        =$= stateMachine legionary nodeState+        =$= handleOutput cm+  where+    handleOutput cm = awaitForever (lift . \case+        Send peer message -> send cm peer message+        NewPeers peers -> newPeers cm peers+      )++    toMessage+      :: Either+          (JoinRequest, JoinResponse -> LIO ())+          (Either+            (PeerMessage i o s)+            (Either (RequestMsg i o) (AdminMessage i o s)))+      -> LInput i o s+    toMessage (Left m) = J m+    toMessage (Right (Left m)) = P m+    toMessage (Right (Right (Left m))) = R m+    toMessage (Right (Right (Right m))) = A m++    {- |+      Turn an LIO-based conduit into an IO-based conduit, so that it+      will work with `merge`.+    -}+    loggingC :: ConduitM i o LIO r -> LIO (ConduitM i o IO r)+    loggingC c = do+      logging <- askLoggerIO+      return (transPipe (`runLoggingT` logging) c)+++{- | This defines the various ways a node can be spun up.  -}+data StartupMode+  = NewCluster+    -- ^ Indicates that we should bootstrap a new cluster at startup. The+    --   persistence layer may be safely pre-populated because the new+    --   node will claim the entire keyspace. Future plans include+    --   implementing some safeguards to make sure only one node in+    --   a cluster was started using this startup mode, but for now,+    --   we are counting on you, the user, to do the right thing.+  | JoinCluster SockAddr+    -- ^ Indicates that the node should try to join an existing cluster,+    --   either by starting fresh, or by recovering from a shutdown+    --   or crash.+  deriving (Show, Eq)+++{- |+  Construct a source of incoming peer messages.  We have to start the+  peer listener first before we spin up the cluster management, which+  is why this is an @LIO (Source LIO PeerMessage)@ instead of a+  @Source LIO PeerMessage@.+-}+startPeerListener :: (LegionConstraints i o s)+  => LegionarySettings+  -> LIO (Source LIO (PeerMessage i o s))++startPeerListener LegionarySettings {peerBindAddr} =+    catchAll (do+        (inputChan, so) <- lift $ do+          inputChan <- newChan+          so <- socket (fam peerBindAddr) Stream defaultProtocol+          setSocketOption so ReuseAddr 1+          bindSocket so peerBindAddr+          listen so 5+          return (inputChan, so)+        forkC "peer socket acceptor" $ acceptLoop so inputChan+        return (chanToSource inputChan)+      ) (\err -> do+        $(logError) . pack+          $ "Couldn't start incomming peer message service, because of: "+          ++ show (err :: SomeException)+        throwM err+      )+  where+    acceptLoop :: (LegionConstraints i o s)+      => Socket+      -> Chan (PeerMessage i o s)+      -> LIO ()+    acceptLoop so inputChan =+        catchAll (+          forever $ do+            (conn, _) <- lift $ accept so+            remoteAddr <- lift $ getPeerName conn+            logging <- askLoggerIO+            let runSocket =+                  sourceSocket conn+                  =$= conduitDecode+                  $$  msgSink+            void+              . lift+              . forkIO+              . (`runLoggingT` logging)+              . logErrors remoteAddr+              $ runSocket+        ) (\err -> do+          $(logError) . pack+            $ "error in peer message accept loop: "+            ++ show (err :: SomeException)+          throwM err+        )+      where+        msgSink = chanToSink inputChan++        logErrors :: SockAddr -> LIO () -> LIO ()+        logErrors remoteAddr io = do+          result <- try io+          case result of+            Left err ->+              $(logWarn) . pack+                $ "Incomming peer connection (" ++ show remoteAddr+                ++ ") crashed because of: " ++ show (err :: SomeException)+            Right v -> return v+++{- | Figure out how to construct the initial node state.  -}+makeNodeState :: (LegionConstraints i o s)+  => LegionarySettings+  -> StartupMode+  -> LIO (NodeState i o s, Map Peer BSockAddr)++makeNodeState LegionarySettings {peerBindAddr} NewCluster = do+  {- Build a brand new node state, for the first node in a cluster. -}+  self <- newPeer+  clusterId <- getUUID+  let cluster = C.new clusterId self peerBindAddr+  nodeState <- newNodeState self cluster+  return (nodeState, C.getPeers cluster)++makeNodeState LegionarySettings {peerBindAddr} (JoinCluster addr) = do+    {-+      Join a cluster by either starting fresh, or recovering from a+      shutdown or crash.+    -}+    $(logInfo) "Trying to join an existing cluster."+    (self, clusterPS) <- joinCluster (JoinRequest (BSockAddr peerBindAddr))+    let cluster = C.initProp self clusterPS+    nodeState <- newNodeState self cluster+    return (nodeState, C.getPeers cluster)+  where+    joinCluster :: JoinRequest -> LIO (Peer, ClusterPowerState)+    joinCluster joinMsg = liftIO $ do+      so <- socket (fam addr) Stream defaultProtocol+      connect so addr+      sendAll so (encode joinMsg)+      {-+        using sourceSocket and conduitDecode is easier than building+        a recive/decode state loop, even though we only read a single+        response.+      -}+      sourceSocket so =$= conduitDecode $$ do+        response <- await+        case response of+          Nothing -> fail +            $ "Couldn't join a cluster because there was no response "+            ++ "to our join request!"+          Just (JoinOk self cps) ->+            return (self, cps)+          Just (JoinRejected reason) -> fail+            $ "The cluster would not allow us to re-join. "+            ++ "The reason given was: " ++ show reason+++{- | A source of cluster join request messages.  -}+joinMsgSource+  :: LegionarySettings+  -> Source LIO (JoinRequest, JoinResponse -> LIO ())++joinMsgSource LegionarySettings {joinBindAddr} = join . lift $+    catchAll (do+        (chan, so) <- lift $ do+          chan <- newChan+          so <- socket (fam joinBindAddr) Stream defaultProtocol+          setSocketOption so ReuseAddr 1+          bindSocket so joinBindAddr+          listen so 5+          return (chan, so)+        forkC "join socket acceptor" $ acceptLoop so chan+        return (chanToSource chan)+      ) (\err -> do+        $(logError) . pack+          $ "Couldn't start join request service, because of: "+          ++ show (err :: SomeException)+        throwM err+      )+  where+    acceptLoop :: Socket -> Chan (JoinRequest, JoinResponse -> LIO ()) -> LIO ()+    acceptLoop so chan =+        catchAll (+          forever $ do+            (conn, _) <- lift $ accept so+            logging <- askLoggerIO+            (void . lift . forkIO . (`runLoggingT` logging) . logErrors) (+                sourceSocket conn+                =$= conduitDecode+                =$= attachResponder conn+                $$  chanToSink chan+              )+        ) (\err -> do+          $(logError) . pack+            $ "error in join request accept loop: "+            ++ show (err :: SomeException)+          throwM err+        )+      where+        logErrors :: LIO () -> LIO ()+        logErrors io = do+          result <- try io+          case result of+            Left err ->+              $(logWarn) . pack+                $ "Incomming join connection crashed because of: "+                ++ show (err :: SomeException)+            Right v -> return v++        attachResponder+          :: Socket+          -> ConduitM JoinRequest (JoinRequest, JoinResponse -> LIO ()) LIO ()+        attachResponder conn = awaitForever (\msg -> do+            mvar <- liftIO newEmptyMVar+            yield (msg, lift . putMVar mvar)+            response <- liftIO $ takeMVar mvar+            liftIO $ sendAll conn (encode response)+          )+++{- | Guess the family of a `SockAddr`.  -}+fam :: SockAddr -> Family+fam SockAddrInet {} = AF_INET+fam SockAddrInet6 {} = AF_INET6+fam SockAddrUnix {} = AF_UNIX+fam SockAddrCan {} = AF_CAN+++{- |+  Forks the legion framework in a background thread, and returns a way to+  send user requests to it and retrieve the responses to those requests.+-}+forkLegionary :: (LegionConstraints i o s, MonadLoggerIO io)+  => Legionary i o s+    {- ^ The user-defined legion application to run. -}+  -> LegionarySettings+    {- ^ Settings and configuration of the legionary framework. -}+  -> StartupMode+  -> io (PartitionKey -> i -> IO o)++forkLegionary legionary settings startupMode = do+  logging <- askLoggerIO+  liftIO . (`runLoggingT` logging) $ do+    chan <- liftIO newChan+    forkC "main legion thread" $+      runLegionary legionary settings startupMode (chanToSource chan)+    return (\ key request -> do+        responseVar <- newEmptyMVar+        writeChan chan ((key, request), putMVar responseVar)+        takeMVar responseVar+      )++
+ src/Network/Legion/Settings.hs view
@@ -0,0 +1,33 @@+{- |+  This module contains the user settings.+-}+module Network.Legion.Settings (+  LegionarySettings(..),+) where++import Network.Socket (SockAddr)+import Network.Wai.Handler.Warp (HostPreference, Port)++{- | Settings used when starting up the legion framework.  -}+data LegionarySettings = LegionarySettings {+    peerBindAddr :: SockAddr,+      {- ^+        The address on which the legion framework will listen for+        rebalancing and cluster management commands.+      -}+    joinBindAddr :: SockAddr,+      {- ^+        The address on which the legion framework will listen for cluster+        join requests.+      -}+    adminHost :: HostPreference,+      {- ^+        The host address on which the admin service should run.+      -}+    adminPort :: Port+      {- ^+        The host port on which the admin service should run.+      -}+  }++
+ src/Network/Legion/StateMachine.hs view
@@ -0,0 +1,644 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{- |+  This module contains the state machine implementation of a legion node.+-}+module Network.Legion.StateMachine (+  stateMachine,+  LInput(..),+  LOutput(..),+  JoinRequest(..),+  JoinResponse(..),+  AdminMessage(..),+  NodeState,+  Forwarded(..),+  PeerMessage(..),+  PeerMessagePayload(..),+  MessageId,+  next,+  newNodeState,+) where++import Prelude hiding (lookup)++import Control.Exception (throw)+import Control.Monad (unless)+import Control.Monad.Catch (try, SomeException, MonadCatch, MonadThrow)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Logger (logDebug, logWarn, logError, logInfo,+  MonadLogger)+import Control.Monad.Trans.Class (MonadTrans, lift)+import Control.Monad.Trans.State (StateT, runStateT, get, put)+import Data.Binary (Binary)+import Data.Conduit (Source, Conduit, ($$), await, awaitForever,+  transPipe, ConduitM, yield, ($=))+import Data.Default.Class (Default)+import Data.Map (Map, insert, lookup)+import Data.Maybe (fromMaybe)+import Data.Set (member, minView, (\\))+import Data.Text (pack)+import Data.Time.Clock (getCurrentTime)+import Data.UUID (UUID)+import Data.Word (Word64)+import GHC.Generics (Generic)+import Network.Legion.Application (Legionary, LegionConstraints,+  Persistence(getState, saveState, list), Legionary(Legionary,+  persistence, handleRequest), RequestMsg)+import Network.Legion.BSockAddr (BSockAddr)+import Network.Legion.ClusterState (claimParticipation, ClusterPropState,+  getPeers, getDistribution, ClusterPowerState)+import Network.Legion.Distribution (rebalanceAction, RebalanceAction(+  Invite), Peer, newPeer)+import Network.Legion.KeySet (union, KeySet)+import Network.Legion.LIO (LIO)+import Network.Legion.PartitionKey (PartitionKey)+import Network.Legion.PartitionState (PartitionPowerState, PartitionPropState)+import Network.Legion.PowerState (ApplyDelta)+import Network.Legion.UUID (getUUID)+import qualified Data.Conduit.List as CL+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Network.Legion.ClusterState as C+import qualified Network.Legion.KeySet as KS+import qualified Network.Legion.PartitionState as P+++{- | This conduit houses the main legionary state machine.  -}+stateMachine :: (LegionConstraints i o s)+  => Legionary i o s+  -> NodeState i o s+  -> Conduit (LInput i o s) LIO (LOutput i o s)+stateMachine l n = awaitForever (\msg -> do+    newState <- runStateMT n $ do+      handleMessage l msg+      heartbeat+      migrate l+      propagate+      rebalance l+      logState+    stateMachine l newState+  )+  where+    logState = lift . logNodeState =<< getS+++{- | Handle one incomming message.  -}+handleMessage :: (LegionConstraints i o s)+  => Legionary i o s+  -> LInput i o s+  -> StateM i o s ()++handleMessage l msg = do+  NodeState {cluster} <- getS+  let+    {- | Return `True` if the peer is a known peer, false otherwise.  -}+    known peer = peer `member` C.allParticipants cluster+  $(logDebug) . pack $ "Receiving: " ++ show msg+  case msg of+    P peerMsg@PeerMessage {source} ->+      if known source+        then handlePeerMessage l peerMsg+        else+          $(logWarn) . pack+            $ "Dropping message from unknown peer: " ++ show source+    R ((key, request), respond) ->+      case minView (C.findPartition key cluster) of+        Nothing ->+          $(logError) . pack+            $ "Keyspace does not contain key: " ++ show key ++ ". This "+            ++ "is a very bad thing and probably means there is a bug, "+            ++ "or else this node has not joined a cluster yet."+        Just (peer, _) ->+          forward peer key request respond+    J m -> handleJoinRequest m+    A m -> lift . handleAdminMessage l m =<< getS+++{- | Handles one incomming message from a peer. -}+handlePeerMessage :: (LegionConstraints i o s)+  => Legionary i o s+  -> PeerMessage i o s+  -> StateM i o s ()++handlePeerMessage -- PartitionMerge+    Legionary {+        persistence+      }+    msg@PeerMessage {+        source,+        payload = PartitionMerge key ps+      }+  = do+    nodeState@NodeState {self, propStates, cluster} <- getS+    propState <- lift $ maybe+      (getStateL persistence self cluster key)+      return+      (lookup key propStates)+    let+      owners = C.findPartition key cluster+    case P.mergeEither source ps propState of+      Left err ->+        $(logWarn) . pack+          $ "Can't apply incomming partition action message "+          ++ show msg ++ "because of: " ++ show err+      Right newPropState -> do+        $(logDebug) "Saving because of PartitionMerge"+        lift $ saveStateL persistence key (+            if P.participating newPropState+              then Just (P.getPowerState newPropState)+              else Nothing+          )+        putS nodeState {+            propStates = if newPropState == P.new key self owners+              then Map.delete key propStates+              else insert key newPropState propStates+          }++handlePeerMessage -- ForwardRequest+    Legionary {handleRequest, persistence}+    msg@PeerMessage {+        payload = ForwardRequest key request,+        source,+        messageId+      }+  = do+    ns@NodeState {self, cluster, propStates} <- getS+    let owners = C.findPartition key cluster+    if self `member` owners+      then do+        let+          respond = send source . ForwardResponse messageId++        -- TODO +        --   - figure out some slick concurrency here, by maintaining+        --       a map of keys that are currently being accessed or+        --       something+        -- +        either (respond . rethrow) respond =<< try (do +            prop <- lift $ getStateL persistence self cluster key+            let response = handleRequest key request (P.ask prop)+                newProp = P.delta request prop+            $(logDebug) "Saving because of ForwardRequest"+            lift $ saveStateL persistence key (Just (P.getPowerState newProp))+            $(logInfo) . pack+              $ "Handling user request: " ++ show request+            $(logDebug) . pack+              $ "Request details request: " ++ show prop ++ " ++ "+              ++ show request ++ " --> " ++ show (response, newProp)+            putS ns {propStates = insert key newProp propStates}+            return response+          )+      else+        {-+          we don't own the key after all, someone was wrong to forward+          us this request.+        -}+        case minView owners of+          Nothing -> $(logError) . pack+            $ "Can't find any owners for the key: " ++ show key+          Just (peer, _) ->+            emit (Send peer msg)+  where+    {- |+      rethrow is just a reification of `throw`.+    -}+    rethrow :: SomeException -> a+    rethrow = throw++handlePeerMessage -- ForwardResponse+    Legionary {}+    msg@PeerMessage {+        payload = ForwardResponse messageId response+      }+  = do+    nodeState@NodeState {forwarded} <- getS+    case lookup messageId (unF forwarded) of+      Nothing -> $(logWarn) . pack+        $  "This peer received a response for a forwarded request that it "+        ++ "didn't send. The only time you might expect to see this is if "+        ++ "this peer recently crashed and was immediately restarted. If "+        ++ "you are seeing this in other circumstances then probably "+        ++ "something is very wrong at the network level. The message was: "+        ++ show msg+      Just respond ->+        lift $ respond response+    putS nodeState {+        forwarded = F . Map.delete messageId . unF $ forwarded+      }++handlePeerMessage -- ClusterMerge+    Legionary {}+    msg@PeerMessage {+        source,+        payload = ClusterMerge ps+      }+  = do+    nodeState@NodeState {migration, cluster} <- getS+    case C.mergeEither source ps cluster of+      Left err ->+        $(logWarn) . pack+          $ "Can't apply incomming cluster action message "+          ++ show msg ++ "because of: " ++ show err+      Right (newCluster, newMigration) ->+        putS nodeState {+            migration = migration `union` newMigration,+            cluster = newCluster+          }+++{- | Handle a join request message -}+handleJoinRequest+  :: (JoinRequest, JoinResponse -> LIO ())+  -> StateM i o s ()++handleJoinRequest (JoinRequest peerAddr, respond) = do+  ns@NodeState {cluster} <- getS+  peer <- lift newPeer+  let newCluster = C.joinCluster peer peerAddr cluster+  emit .  NewPeers . getPeers $ newCluster+  lift $ respond (JoinOk peer (C.getPowerState newCluster))+  putS ns {cluster = newCluster}+++{- |+  Handle a message from the admin service.+-}+handleAdminMessage+  :: Legionary i o s+  -> AdminMessage i o s+  -> NodeState i o s+  -> LIO ()+handleAdminMessage _ (GetState respond) ns =+  respond ns+handleAdminMessage Legionary {persistence} (GetPart key respond) _ = do+  partitionVal <- lift (getState persistence key)+  respond partitionVal+++{- | Update all of the propagation states with the current time.  -}+heartbeat :: StateM i o s ()+heartbeat = do+  now <- liftIO getCurrentTime+  ns@NodeState {cluster, propStates} <- getS+  putS ns {+      cluster = C.heartbeat now cluster,+      propStates = Map.fromAscList [+          (k, P.heartbeat now p)+          | (k, p) <- Map.toAscList propStates+        ]+    }+++{- |+  Migrate partitions based on new cluster state information.++  TODO: this migration algorithm is super naive. It just goes ahead+  and migrates everything in one pass, which is going to be terrible+  for performance.++  Also, it is important to remember that "migrate" in this context does+  not mean "transfer data". Rather, "migrate" means to add a participating+  peer to a partition. This will cause the data to be transfered in the+  normal course of propagation.+-}+migrate :: (LegionConstraints i o s) => Legionary i o s -> StateM i o s ()+migrate Legionary{persistence} = do+    ns@NodeState {migration} <- getS+    unless (KS.null migration) $+      putS =<< lift (+          listL persistence+          $= CL.filter ((`KS.member` migration) . fst)+          $$ accum ns {migration = KS.empty}+        )+  where+    accum ns@NodeState {self, cluster, propStates} = await >>= \case+      Nothing -> return ns+      Just (key, ps) -> +        let+          origProp = fromMaybe (P.initProp self ps) (lookup key propStates)+          newPeers_ = C.findPartition key cluster \\ P.projParticipants origProp+          {- This 'P.participate' is where the magic happens. -}+          newProp = foldr P.participate origProp (Set.toList newPeers_)+        in do+          $(logDebug) . pack $ "Migrating: " ++ show key+          lift (saveStateL persistence key (Just (P.getPowerState newProp)))+          accum ns {+              propStates = Map.insert key newProp propStates+            }+++{- |+  Handle all cluster and partition state propagation actions, and return+  an updated node state.+-}+propagate :: (LegionConstraints i o s) => StateM i o s ()+propagate = do+    ns@NodeState {cluster, propStates, self} <- getS+    let (peers, ps, cluster2) = C.actions cluster+    $(logDebug) . pack $ "Cluster Actions: " ++ show (peers, ps)+    mapM_ (doClusterAction ps) (Set.toList peers)+    propStates2 <- mapM doPartitionActions (Map.toList propStates)+    putS ns {+        cluster = cluster2,+        propStates = Map.fromAscList [+            (k, p)+            | (k, p) <- propStates2+            , p /= P.initProp self (P.getPowerState p)+          ]+      }+  where+    doClusterAction ps peer =+      send peer (ClusterMerge ps)++    doPartitionActions (key, propState) = do+        let (peers, ps, propState2) = P.actions propState+        mapM_ (perform ps) (Set.toList peers)+        return (key, propState2)+      where+        perform ps peer =+          send peer (PartitionMerge key ps)+++{- |+  Figure out if any rebalancing actions must be taken by this node, and kick+  them off if so.+-}+rebalance :: (LegionConstraints i o s) => Legionary i o s -> StateM i o s ()+rebalance _ = do+  ns@NodeState {self, cluster} <- getS+  let+    allPeers = (Set.fromList . Map.keys . getPeers) cluster+    dist = getDistribution cluster+    action = rebalanceAction self allPeers dist+  $(logDebug) . pack $ "The rebalance action is: " ++ show action+  putS ns {+      cluster = case action of+        Nothing -> cluster+        Just (Invite ks) -> claimParticipation self ks cluster+    }+++{- | This is the type of input accepted by the legionary state machine. -}+data LInput i o s+  = P (PeerMessage i o s)+  | R (RequestMsg i o)+  | J (JoinRequest, JoinResponse -> LIO ())+  | A (AdminMessage i o s)++instance (Show i, Show o, Show s) => Show (LInput i o s) where+  show (P m) = "(P " ++ show m ++ ")"+  show (R ((p, i), _)) = "(R ((" ++ show p ++ ", " ++ show i ++ "), _))"+  show (J (jr, _)) = "(J (" ++ show jr ++ ", _))"+  show (A a) = "(A (" ++ show a ++ "))"+++{- | This is the type of output produced by the legionary state machine. -}+data LOutput i o s+  = Send Peer (PeerMessage i o s)+  | NewPeers (Map Peer BSockAddr)+++{- | A helper function to log the state of the node: -}+logNodeState :: (LegionConstraints i o s) => NodeState i o s -> LIO ()+logNodeState ns = $(logDebug) . pack+    $ "The current node state is: " ++ show ns+++{- | Like `getState`, but in LIO, and provides the correct bottom value.  -}+getStateL :: (ApplyDelta i s, Default s)+  => Persistence i s+  -> Peer+  -> ClusterPropState+  -> PartitionKey+  -> LIO (PartitionPropState i s)++getStateL p self cluster key =+  {- dp == default participants -}+  let dp = C.findPartition key cluster+  in maybe+      (P.new key self dp)+      (P.initProp self)+      <$> lift (getState p key)+++{- | Like `saveState`, but in LIO.  -}+saveStateL+  :: Persistence i s+  -> PartitionKey+  -> Maybe (PartitionPowerState i s)+  -> LIO ()+saveStateL p k = lift . saveState p k+++{- | Like `list`, but in LIO.  -}+listL :: Persistence i s -> Source LIO (PartitionKey, PartitionPowerState i s)+listL p = transPipe lift (list p)+++{- | This is the type of a join request message.  -}+data JoinRequest = JoinRequest BSockAddr+  deriving (Generic, Show)+instance Binary JoinRequest+++{- | The response to a JoinRequst message -}+data JoinResponse+  = JoinOk Peer ClusterPowerState+  | JoinRejected String+  deriving (Generic)+instance Binary JoinResponse+++{- |+  The type of messages sent by the admin service.+-}+data AdminMessage i o s+  = GetState (NodeState i o s -> LIO ())+  | GetPart PartitionKey (Maybe (PartitionPowerState i s) -> LIO ())++instance Show (AdminMessage i o s) where+  show (GetState _) = "(GetState _)"+  show (GetPart k _) = "(GetPart " ++ show k ++ " _)"+++{- | Defines the local state of a node in the cluster.  -}+data NodeState i o s = NodeState {+             self :: Peer,+          cluster :: ClusterPropState,+        forwarded :: Forwarded o,+       propStates :: Map PartitionKey (PartitionPropState i s),+        migration :: KeySet,+           nextId :: MessageId+  }+  deriving (Show)+++{- | A set of forwardmed messages.  -}+newtype Forwarded o = F {unF :: Map MessageId (o -> LIO ())}+instance Show (Forwarded o) where+  show = show . Map.keys . unF+++{- |+  The type of messages sent to us from other peers.+-}+data PeerMessage i o s = PeerMessage {+       source :: Peer,+    messageId :: MessageId,+      payload :: PeerMessagePayload i o s+  }+  deriving (Generic, Show)+instance (Binary i, Binary o, Binary s) => Binary (PeerMessage i o s)+++{- |+  The data contained within a peer message.++  When we get around to implementing durability and data replication,+  the sustained inability to confirm that a node has received one of+  these messages should result in the ejection of that node from the+  cluster and the blacklisting of that node so that it can never re-join.+-}+data PeerMessagePayload i o s+  = PartitionMerge PartitionKey (PartitionPowerState i s)+  | ForwardRequest PartitionKey i+  | ForwardResponse MessageId o+  | ClusterMerge ClusterPowerState+  deriving (Generic, Show)+instance (Binary i, Binary o, Binary s) => Binary (PeerMessagePayload i o s)+++data MessageId = M UUID Word64 deriving (Generic, Show, Eq, Ord)+instance Binary MessageId+++{- |+  Generate the next message id in the sequence. We would normally use+  `succ` for this kind of thing, but making `MessageId` an instance of+  `Enum` really isn't appropriate.+-}+next :: MessageId -> MessageId+next (M sequenceId ord) = M sequenceId (ord + 1)+++{- |+  Initialize a new sequence of messageIds+-}+newSequence ::  LIO MessageId+newSequence = lift $ do+  sid <- getUUID+  return (M sid 0)+++{- |+  Make a new node state.+-}+newNodeState :: Peer -> ClusterPropState -> LIO (NodeState i o s)+newNodeState self cluster = do+  nextId <- newSequence+  return NodeState {+      self,+      nextId,+      cluster,+      forwarded = F Map.empty,+      propStates = Map.empty,+      migration = KS.empty+    }+++send :: Peer -> PeerMessagePayload i o s -> StateM i o s ()+send peer payload = do+  ns@NodeState {self, nextId} <- getS+  emit (Send peer PeerMessage {+      source = self,+      messageId = nextId,+      payload+    })+  putS ns {nextId = next nextId}+++{- |+  Forward a user request to a peer for handling, being sure to do all+  the node state accounting.+-}+forward+  :: Peer+  -> PartitionKey+  -> i+  -> (o -> IO ())+  -> StateM i o s ()+forward peer key request respond = do+  ns@NodeState {nextId, self, forwarded} <- getS+  emit (Send peer PeerMessage {+      source = self,+      messageId = nextId,+      payload = ForwardRequest key request+    })+  putS ns {+      nextId = next nextId,+      forwarded = F . insert nextId (lift . respond) . unF $ forwarded+    }+++{- |+  The monad in which the internals of the state machine run. This is really+  just a conduit, but we wrap it because we only want to allow `yield`, which+  we have re-named `emit`.+-}+newtype StateMT i o s m r = StateMT {+    unStateMT ::+      StateT+        (NodeState i o s)+        (ConduitM (LInput i o s) (LOutput i o s) m)+        r+  } deriving (+    Functor, Applicative, Monad, MonadLogger, MonadCatch,+    MonadThrow, MonadIO+  )+instance MonadTrans (StateMT i o s) where+  lift = StateMT . lift . lift+++{- |+  The state machine monad, in LIO.+-}+type StateM i o s r = StateMT i o s LIO r+++{- |+  Run the state machine monad, starting with the initial node state.+-}+runStateMT+  :: NodeState i o s+  -> StateMT i o s m ()+  -> ConduitM (LInput i o s) (LOutput i o s) m (NodeState i o s)+runStateMT ns = fmap snd . (`runStateT` ns) . unStateMT+++{- |+  Emit some output from the state machine.+-}+emit :: LOutput i o s -> StateM i o s ()+emit = StateMT . lift . yield+++{- |+  Get the node State.+-}+getS :: StateMT i o s m (NodeState i o s)+getS = StateMT get+++{- |+  Put the node state.+-}+putS :: NodeState i o s -> StateMT i o s m ()+putS = StateMT . put++
+ src/Network/Legion/UUID.hs view
@@ -0,0 +1,22 @@+{- |+  Contains common UUID functionality.+-}+module Network.Legion.UUID (+  getUUID,+) where++import Control.Concurrent (threadDelay)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.UUID (UUID)+import Data.UUID.V1 (nextUUID)+++{- | A utility function that makes a UUID, no matter what.  -}+getUUID :: (MonadIO io) => io UUID++getUUID = liftIO nextUUID >>= maybe (wait >> getUUID) return+  where+    wait = liftIO (threadDelay oneMillisecond)+    oneMillisecond = 1000++