Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't exceed maximum resources #16

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# resource-pool-0.4.0.0 (????-??-??)
* Require `poolMaxResources` to be not smaller than the number of stripes.
* Add support for setting the number of stripes.
* The pool will not not exceed maximum resources. [#16](https://github.com/scrive/pool/pull/16)

# resource-pool-0.3.1.0 (2022-06-15)
* Add `tryWithResource` and `tryTakeResource`.
Expand Down
23 changes: 23 additions & 0 deletions resource-pool.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,26 @@ library
, LambdaCase
, RankNTypes
, TypeApplications

test-suite tests
type: exitcode-stdio-1.0
hs-source-dirs: test
main-is: Main.hs
build-depends:
base
, hedgehog
, hspec
, hspec-hedgehog
, resource-pool
ghc-options: -Wall -Wcompat
build-tool-depends: hspec-discover:hspec-discover

default-language: Haskell2010

default-extensions: DeriveGeneric
, LambdaCase
, RankNTypes
, TypeApplications

other-modules:
Data.Pool.InternalSpec
2 changes: 1 addition & 1 deletion src/Data/Pool.hs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ createPool create free numStripes idleTime maxResources = newPool PoolConfig
{ createResource = create
, freeResource = free
, poolCacheTTL = realToFrac idleTime
, poolMaxResources = numStripes * maxResources
, poolMaxResources = maxResources
parsonsmatt marked this conversation as resolved.
Show resolved Hide resolved
, poolNumStripes = Just numStripes
}

Expand Down
75 changes: 58 additions & 17 deletions src/Data/Pool/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
-- This module is intended for internal use only, and may change without warning
-- in subsequent releases.
{-# OPTIONS_HADDOCK not-home #-}

module Data.Pool.Internal where

import Control.Concurrent
Expand Down Expand Up @@ -64,15 +65,16 @@ data PoolConfig a = PoolConfig
-- ^ The maximum number of resources to keep open across all stripes. The
-- smallest acceptable value is @1@.
--
-- /Note:/ for each stripe the number of resources is divided by the number of
-- stripes and rounded up, hence the pool might end up creating up to @N - 1@
-- resources more in total than specified, where @N@ is the number of stripes.
-- /Note/: Each stripe will try to have an equal amount of resources per
-- stripe. If the number of resources does not evenly divide into the
-- stripes, then some stripes will have one fewer resource than others.
, poolNumStripes :: !(Maybe Int)
-- ^ The number of stripes in the pool.
-- ^ The number of stripes in the pool. If 'Nothing' is provided, then
-- this will use 'getNumCapabilities'.
--
-- /Note:/ if set to 'Nothing', it defaults to the number of capabilities,
-- which ensures that threads never compete over access to the same stripe and
-- results in a very good performance in a multi-threaded environment.
-- The resource count in 'poolMaxResources' will be split evenly among
-- the available stripes. If there are fewer stripes than resources, then
-- this will only create as many stripes as resources.
Comment on lines +77 to +79
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code currently on master will treat this case as a programmer error. I feel like this is a bad state, and fewer error calls would be preferable. This PR clamps the value such that maxResources is always less than or equal to numStripes, preferring to have more stripes with fewer resources each.

I feel like error is worse than value clamping, but neither are great.

Prior to this PR, the max resources parameter was not really establishing a "maximum count of resources." This PR makes it so that a Pool never has more resources than the maxResources parameter.

Alternatively, we could replace poolMaxResources with poolResourcesPerStripe. This has an unambiguous meaning, and could really simplify the code. The place where we currently calculate can be relocated to the deprecated createPool, which would determine (given a numStripes and maxResources) a way to have resourcesPerStripe * numStripes <= maxResources.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing as the number of capabilities is a runtime environment-specific detail that the programmer cannot know in advance, I think replacing poolMaxResources with poolResourcesPerStripe makes sense.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have time to look at this any further, but I did wonder if it wouldn't be a pain in the ass to make the resource limits more expressive, something like

-- | Limits can be expressed in two different ways.
--
-- To set the stage, it's better for performance to have individual pools for
-- each capability (i.e., physical CPU) available to the program. This is
-- accomplished with "stripes".
--
-- Some resources, like database connections, are relatively cheap to hold on
-- to. In that case it makes sense to size the pool by choosing the size of
-- the stripes. You can specify number of stripes, but the default is the number
-- of capabilities, which is sensible.
--
-- Other resources (example would be nice?) are expensive to hold on to, in which
-- case you may want to limit their absolute number __across__ all stripes. You
-- can still specify the number of stripes, but only as a maximum. The actual
-- number of stripes will be the min of maxResources and maxStripes.
--
-- To match behavior of resource-pool <4, use @LimitStripeSize@.
data ResourceLimits 
    = LimitStripeSize
        { maxStripeSize :: Int
        , maxStripes :: Maybe Int
        }
    | LimitTotalResources
        { maxResources :: Int
        , maxStripes :: Maybe Int
        }

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, this PR seems like an improvement over the status quo, and this whole discussion here could be inspiration for a followup. (I'm not asking for changes.)

Comment on lines +77 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears swapped.

Suggested change
-- The resource count in 'poolMaxResources' will be split evenly among
-- the available stripes. If there are fewer stripes than resources, then
-- this will only create as many stripes as resources.
-- The resource count in 'poolMaxResources' will be split evenly among
-- the available stripes. If there are fewer resources than stripes, then
-- this will only create as many stripes as resources.

}

-- | Create a new striped resource pool.
Expand All @@ -87,22 +89,34 @@ newPool pc = do
error "poolCacheTTL must be at least 0.5"
when (poolMaxResources pc < 1) $ do
error "poolMaxResources must be at least 1"
numStripes <- maybe getNumCapabilities pure (poolNumStripes pc)
when (numStripes < 1) $ do
numStripesRequested <- maybe getNumCapabilities pure (poolNumStripes pc)
when (numStripesRequested < 1) $ do
error "numStripes must be at least 1"
let stripeResourceAllocation =
howManyStripes Input
{ inputMaxResources = poolMaxResources pc
, inputStripes = numStripesRequested
}
stripeAllocations =
robin stripeResourceAllocation
indexedAllocations =
zip [1..] stripeAllocations
numStripes =
allowedStripes stripeResourceAllocation

when (poolMaxResources pc < numStripes) $ do
error "poolMaxResources must not be smaller than numStripes"
pools <- fmap (smallArrayFromListN numStripes) . forM [1..numStripes] $ \n -> do
pools <- fmap (smallArrayFromListN numStripes) . forM indexedAllocations $ \(index, allocation) -> do
ref <- newIORef ()
stripe <- newMVar Stripe
{ available = poolMaxResources pc `quotCeil` numStripes
{ available = allocation
, cache = []
, queue = Empty
, queueR = Empty
}
-- When the local pool goes out of scope, free its resources.
void . mkWeakIORef ref $ cleanStripe (const True) (freeResource pc) stripe
pure LocalPool { stripeId = n
pure LocalPool { stripeId = index
, stripeVar = stripe
, cleanerRef = ref
}
Expand All @@ -118,18 +132,45 @@ newPool pc = do
, reaperRef = ref
}
where
quotCeil :: Int -> Int -> Int
quotCeil x y =
-- Basically ceiling (x / y) without going through Double.
let (z, r) = x `quotRem` y in if r == 0 then z else z + 1

-- Collect stale resources from the pool once per second.
collector pools = forever $ do
threadDelay 1000000
now <- getMonotonicTime
let isStale e = now - lastUsed e > poolCacheTTL pc
mapM_ (cleanStripe isStale (freeResource pc) . stripeVar) pools

-- | A datatype representing the requested maximum resources and count of
-- stripes. We don't use these figurs directly, but instead calculate a
data Input = Input { inputMaxResources :: !Int, inputStripes :: !Int }
deriving Show

data StripeResourceAllocation = StripeResourceAllocation { poolInput :: !Input, allowedStripes :: !Int }
deriving Show

-- | Determine how many resources should be allocated to each stripe.
--
-- The output list contains a single `Int` per stripe, with the 'Int'
-- representing the amount of resources available to that stripe.
robin :: StripeResourceAllocation -> [Int]
robin stripeResourceAllocation =
let
numStripes =
allowedStripes stripeResourceAllocation
(baseCount, remainder) =
inputMaxResources (poolInput stripeResourceAllocation)
`divMod` numStripes
in
replicate remainder (baseCount + 1) ++ replicate (numStripes - remainder) baseCount

howManyStripes :: Input -> StripeResourceAllocation
howManyStripes inp = StripeResourceAllocation
{ allowedStripes =
if inputStripes inp > inputMaxResources inp
then inputMaxResources inp
else inputStripes inp
, poolInput = inp
}

-- | Destroy a resource.
--
-- Note that this will ignore any exceptions in the destroy function.
Expand Down
46 changes: 46 additions & 0 deletions test/Data/Pool/InternalSpec.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{-# language RecordWildCards #-}
{-# language OverloadedStrings #-}

module Data.Pool.InternalSpec where

import Test.Hspec
import Test.Hspec.Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import Data.Pool.Internal

spec :: Spec
spec = do
describe "howManyStripes" $ do
it "has one stripe per resource" $ hedgehog $ do
input@Input {..} <- forAll inputs
let allocation = howManyStripes input
-- actual stripes may be at most requested stripes
if inputStripes >= inputMaxResources
then do
label "More stripes than resources"
else do
label "Fewer stripes than resources"
inputStripes === allowedStripes allocation
diff (allowedStripes allocation) (<=) inputStripes

describe "robin" $ do
it "sum of resources is always equal to input" $ hedgehog $ do
input <- forAll inputs
let resourceAllocations = robin (howManyStripes input)
sum resourceAllocations === inputMaxResources input
it "the difference between smallest and largest stripe is at most 1" $ hedgehog $ do
input <- forAll inputs
let resourceAllocations = robin (howManyStripes input)
diff (minimum resourceAllocations) (\a b -> abs (a - b) <= 1) (maximum resourceAllocations)
it "there is a resource allocation for each stripe" $ hedgehog $ do
input <- forAll inputs
let stripeAndResource = howManyStripes input
let resourceAllocations = robin stripeAndResource
length resourceAllocations === allowedStripes stripeAndResource

inputs :: Gen Input
inputs = do
resources <- Gen.int (Range.exponentialFrom 8 1 1000)
stripes <- Gen.int (Range.exponentialFrom 20 1 100)
pure $ Input resources stripes
1 change: 1 addition & 0 deletions test/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}