packages feed

hashring (empty) → 0.0.0

raw patch · 6 files changed

+389/−0 lines, 6 filesdep +QuickCheckdep +basedep +containerssetup-changed

Dependencies added: QuickCheck, base, containers, hashable, hashring, test-framework, test-framework-quickcheck2

Files

+ Data/HashRing.hs view
@@ -0,0 +1,189 @@+-- | module: Data.HashRing+-- copyright: (c) Michael S. Craig 2012+-- license: BSD3+-- maintainer: mkscrg@gmail.com+-- stability: experimental+-- portability: portable+--+-- An efficient implementation of consistent hashing, as described in+--+--    * David Karger et al.,+--    \"/Consistent hashing and random trees: distributed caching protocols for relieving hot spots on the World Wide Web/\",+--    29th Annual ACM Symposium on Theory,+--    <http://dl.acm.org/citation.cfm?id=258660>+--+-- In distributed computing applications, it's usually necessary route messages+-- to some group of N nodes in the network. Message locality, wherein messages+-- of the same kind are routed to the same node, is often desirable. \"Normal\"+-- hashing, where a message's node is determined by some hash function modulo+-- N, has the undesirable property that adding or removing a node from the+-- network causes the key sets of all other nodes to change drastically.+-- In contrast, consistent hashing has the property that small changes to the+-- size of the node set cause only small changes to key sets of the nodes.+--+-- This implementation is built on top of 'IntMap' and 'Set'. It provides+-- /O(1)/ lookup functions as well as /O(min(log n, R))/ insertion and deletion+-- functions, where /R/ is the number of replica nodes used in the ring (see+-- 'empty').+--+-- The key space of the ring is the full range of 'Int' values. To insert a+-- node, we generate (/R > 0/) keys by hashing the node with /R/ successive+-- salts, and the node is referenced by those keys in the ring. To get a node+-- for a message, we hash the message to an 'Int' value /k/ and find the+-- smallest key /k'/ in the ring such that /k <= k'/. The node is the value+-- referenced by /k'/. Higher values of /R/ give a more even distribution of+-- keys to nodes but slow down insertions and deletions of nodes. /R/ is+-- specified when constructing a 'HashRing' with 'empty', 'singleton', or+-- 'fromList' and retrievable with 'replicas'.+--+-- The ability of 'HashRing' to fairly distribute messages among nodes relies+-- on the implementations of 'Data.Hashable.hashWithSalt' for the message and+-- node types. For example, the default implementation for+-- 'Data.ByteString.ByteString' is non-uniform on short inputs, and so it's+-- unsuitable for use with 'HashRing'. Reimplementing+-- 'Data.Hashable.hashWithSalt' for your message and node types with a+-- cryptographic hash function (like MD5 or SHA1 from the @cryptohash@ package)+-- will give better results.++module Data.HashRing (+  -- * Map type+  HashRing+  -- * Construction+, empty+, singleton+  -- * Operators+, (!)+  -- * Query+, null+, size+, replicas+, member+, lookup+, find+  -- * Insertion+, insert+  -- * Deletion+, delete+  -- * Conversion+, fromList+, toList+) where++import Prelude hiding (lookup, null)+import Data.Hashable (Hashable, hash, hashWithSalt)+import Data.IntMap (IntMap)+import qualified Data.IntMap as IM+import Data.Set (Set)+import qualified Data.Set as S+++-- | The constructor for this data type is not exported. See 'empty',+-- 'singleton', or 'fromList'.+--+-- Note that 'HashRing' is parameterized by the node type and not by the+-- message type. As made clear by the type signatures for '!', 'find', and+-- 'lookup', any 'Hashable' type can be used as a message.+data HashRing a = HashRing+  { nodeMap :: IntMap a+  , nodeSet :: Set a+  , replicas :: Int+  -- ^ Number of replica nodes (/R/) in the ring for each real node.+  } deriving (Eq)+++instance Show a => Show (HashRing a) where+  showsPrec d ring = showParen (d > 10)+                   $ showString "fromList "+                   . shows (replicas ring)+                   . showString " "+                   . shows (toList ring)++instance (Read a, Ord a, Hashable a) => Read (HashRing a) where+  readsPrec d = readParen (d > 10) $ \s -> do+    ("fromList", s') <- lex s+    (nreps, s'') <- reads s'+    (nodes, s''') <- reads s''+    return (fromList nreps nodes, s''')+++-- | Construct an empty ring with a specific /R/ value.+empty :: Int -> HashRing a+empty nreps = HashRing IM.empty S.empty nreps++-- | Construct a single-node ring with a specific /R/ value.+singleton :: (Ord a, Hashable a) => Int -> a -> HashRing a+singleton nreps node = insert node $ empty nreps+++-- | @True@ if the ring is empty, @False@ otherwise.+null :: HashRing a -> Bool+null ring = S.null $ nodeSet ring++-- | Number of nodes in the ring.+size :: HashRing a -> Int+size ring = S.size $ nodeSet ring++-- | @True@ if the node is in the ring, @False@ otherwise.+member :: Ord a => a -> HashRing a -> Bool+member node ring = S.member node $ nodeSet ring+++-- | Get the node in the ring corresponding to a message, or @Nothing@ if the+-- ring is empty.+lookup :: Hashable b => b -> HashRing a -> Maybe a+lookup msg ring = let nmap = nodeMap ring in if IM.null nmap+  then Nothing+  else Just $ case IM.splitLookup (hash msg) nmap of+    (_, Just node, _) -> node+    (_, _, submap) -> if IM.null submap+      then snd $ IM.findMin nmap+      else snd $ IM.findMin submap++-- | Get the node in the ring corresponding to a message, or error if the ring+-- is empty.+find :: Hashable b => b -> HashRing a -> a+find msg ring = case lookup msg ring of+  Just x -> x+  Nothing -> error "HashRing.find: empty hash ring"++-- | Get the node in the ring corresponding to a message, or error if the ring+-- is empty.+(!) :: Hashable b => HashRing a -> b -> a+ring ! msg = find msg ring+++-- | Add a node to the ring.+insert :: (Ord a, Hashable a) => a -> HashRing a -> HashRing a+insert node ring+  | S.member node $ nodeSet ring = ring+  | otherwise = ring+    { nodeMap = foldr (\key nmap -> IM.insert key node nmap) (nodeMap ring)+              $ take (replicas ring)+              $ filter (\key -> not $ IM.member key (nodeMap ring))+              $ nodeKeys node+    , nodeSet = S.insert node (nodeSet ring) }++-- | Remove a node from the ring.+delete :: (Ord a, Hashable a) => a -> HashRing a -> HashRing a+delete node ring+  | not $ S.member node $ nodeSet ring = ring+  | otherwise = ring+    { nodeMap = foldr (\key nmap -> IM.delete key nmap) (nodeMap ring)+              $ take (replicas ring)+              $ filter (\key -> IM.lookup key (nodeMap ring) == Just node)+              $ nodeKeys node+    , nodeSet = S.delete node (nodeSet ring) }+++-- | Construct a ring from an /R/ value and a list of nodes.+fromList :: (Ord a, Hashable a) => Int -> [a] -> HashRing a+fromList nreps nodes = foldr insert (empty nreps) nodes++-- | Construct a list containing the nodes in the ring.+toList :: HashRing a -> [a]+toList ring = S.toList $ nodeSet ring+++-- List of possible keys for a new node+nodeKeys :: Hashable a => a -> [Int]+nodeKeys node = map (\i -> hashWithSalt i node) [0..]
+ LICENSE.md view
@@ -0,0 +1,25 @@+The following license covers this documentation, and the source code, except+where otherwise indicated.++Copyright (c) 2012, Michael S. Craig. All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,6 @@+# hashring++Efficient [consistent hashing](http://en.wikipedia.org/wiki/Consistent_hashing)+in Haskell.++See the haddocks on [Hackage](http://hackage.haskell.org/package/hashring).
+ Setup.hs view
@@ -0,0 +1,4 @@+import Distribution.Simple++main :: IO ()+main = defaultMain
+ hashring.cabal view
@@ -0,0 +1,46 @@+name: hashring+version: 0.0.0+license: BSD3+license-file: LICENSE.md+author: Michael S. Craig <mkscrg@gmail.com>+maintainer: Michael S. Craig <mkscrg@gmail.com>+stability: experimental+synopsis: Efficient consistent hashing.+description:+  An efficient implementation of consistent hashing. See the documentation for+  @Data.HashRing@ for more info.+homepage: https://github.com/mkscrg/hashring+bug-reports: https://github.com/mkscrg/hashring/issues+category: Data+build-type: Simple+cabal-version: >= 1.9.2+extra-source-files: README.md++library+  exposed-modules: Data.HashRing+  ghc-options: -Wall -O2+  build-depends:+    base >= 4 && < 5,+    containers >= 0.4 && < 0.5,+    hashable >= 1.1.1 && < 1.2++test-suite tests+  type: exitcode-stdio-1.0+  main-is: Properties.hs+  hs-source-dirs: tests+  ghc-options: -Wall -O2 -fno-warn-orphans+  build-depends:+    base >= 4 && < 5,+    hashring,+    test-framework,+    test-framework-quickcheck2,+    QuickCheck++source-repository head+  type: git+  location: git://github.com/mkscrg/hashring++source-repository this+  type: git+  location: git://github.com/mkscrg/hashring+  tag: 0.0.0
+ tests/Properties.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}++module Main (main) where++import Data.HashRing (HashRing, (!))+import qualified Data.HashRing as R+import Data.List ((\\), nub)+import Data.Word (Word8)+import Test.QuickCheck (Arbitrary (..), Gen, Property, (==>), listOf, resize)+import Test.Framework (defaultMain, testGroup )+import Test.Framework.Providers.QuickCheck2 (testProperty)+++main :: IO ()+main = defaultMain+  [ testGroup "basic properties"+    [ testProperty "empty ring is null" pEmptyIsNull+    , testProperty "nonempty rings are not null" pNonemptyNonNull+    , testProperty "size is # of nodes" pSize+    , testProperty "replicas is # of replicas" pReplicas+    , testProperty "equality" pInequality ]+  , testGroup "query properties"+    [ testProperty "lookup in empty ring" pLookupEmpty+    , testProperty "member identity" pMember+    , testProperty "lookup identity" pLookupId+    , testProperty "find identity" pFindId+    , testProperty "index (bang) identity" pIndexId+    , testProperty "lookup not Nothing" pLookupJust+    , testProperty "lookup wraps" pLookupWrap ]+  , testGroup "insert/delete properties"+    [ testProperty "insert into empty" pSingleton+    , testProperty "insert idempotency" pInsertIdem+    , testProperty "insert then delete" pInsertDelete+    , testProperty "delete nonmember" pDeleteNonMember ]+  , testGroup "conversion properties"+    [ testProperty "show/read identity" pShowRead+    , testProperty "list conversion" pListConvert ] ]+++type IRing = HashRing Int+newtype NReps = NReps Int deriving (Show)++instance Arbitrary NReps where+  arbitrary = fmap (NReps . (+1) . fromIntegral) (arbitrary :: Gen Word8)++instance Arbitrary IRing where+  arbitrary = do+    NReps nreps <- arbitrary+    nodes <- resize 100 $ listOf arbitrary+    return $ R.fromList nreps nodes+++pEmptyIsNull :: NReps -> Bool+pEmptyIsNull (NReps nreps) = R.null $ R.empty nreps++pNonemptyNonNull :: IRing -> Property+pNonemptyNonNull ring = R.size ring > 0 ==> not (R.null ring)++pSize :: NReps -> [Int] -> Bool+pSize (NReps nreps) nodes = go nodes $ R.empty nreps+  where+    go [] ring = R.size ring == length (nub nodes)+    go (n:ns) ring = go ns $ R.insert n ring++pReplicas :: NReps -> Bool+pReplicas (NReps nreps) = R.replicas (R.empty nreps) == nreps++pInequality :: NReps -> Int -> Bool+pInequality (NReps nreps) node = R.singleton nreps node /= R.empty nreps+++pLookupEmpty :: Int -> NReps -> Bool+pLookupEmpty node (NReps nreps) =+  R.lookup node (R.empty nreps :: IRing) == Nothing++pMember :: Int -> IRing -> Bool+pMember node ring = R.member node (R.insert node ring) == True++pLookupId :: Int -> IRing -> Bool+pLookupId msgnode ring = R.lookup msgnode (R.insert msgnode ring) == Just msgnode++pFindId :: Int -> IRing -> Bool+pFindId msgnode ring = R.find msgnode (R.insert msgnode ring) == msgnode++pIndexId :: Int -> IRing -> Bool+pIndexId msgnode ring = R.insert msgnode ring ! msgnode == msgnode++pLookupJust :: Int -> IRing -> Property+pLookupJust msg ring = not (R.null ring) ==> R.lookup msg ring /= Nothing++pLookupWrap :: Int -> NReps -> Property+pLookupWrap msgnode (NReps nreps) = msgnode /= maxBound ==>+  R.lookup (msgnode + 1) (R.insert msgnode $ R.empty nreps) == Just msgnode+++pSingleton :: NReps -> Int -> Bool+pSingleton (NReps nreps) node =+  R.insert node (R.empty nreps) == R.singleton nreps node++pInsertIdem :: Int -> IRing -> Bool+pInsertIdem node ring =+  R.insert node (R.insert node ring) == R.insert node ring++pInsertDelete :: Int -> IRing -> Bool+pInsertDelete node ring = ring == R.delete node (R.insert node ring)++pDeleteNonMember :: Int -> IRing -> Property+pDeleteNonMember node ring =+  not (R.member node ring) ==> R.delete node ring == ring+++pShowRead :: IRing -> Bool+pShowRead ring = ring == read (show ring)++pListConvert :: NReps -> [Int] -> Bool+pListConvert (NReps nreps) ints =+  let innodes = nub ints+      outnodes = R.toList $ R.fromList nreps innodes+  in innodes \\ outnodes == [] && outnodes \\ innodes == []