{-# LANGUAGE TupleSections #-} {-| Ad-hoc rate limiting for the JQScheduler based on reason trails. -} {- Copyright (C) 2014 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. 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 AND CONTRIBUTORS "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 HOLDER OR CONTRIBUTORS 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. -} module Ganeti.JQScheduler.ReasonRateLimiting ( reasonRateLimit -- * For testing only , parseReasonRateLimit , countMapFromJob , slotMapFromJobs ) where import Data.List import Data.Maybe import qualified Data.Map as Map import Ganeti.Lens hiding (chosen) import Ganeti.JQScheduler.Types import Ganeti.JQueue (QueuedJob(..)) import Ganeti.JQueue.Lens import Ganeti.OpCodes.Lens import Ganeti.SlotMap import Ganeti.Utils -- | Ad-hoc rate limiting buckets are identified by the /combination/ -- `REASONSTRING:n`, so "mybucket:3" and "mybucket:4" are /different/ buckets. type AdHocReasonKey = String -- | Parses an ad-hoc rate limit from a reason trail, as defined under -- "Ad-Hoc Rate Limiting" in `doc/design-optables.rst`. -- -- The parse succeeds only on reasons of form `rate-limit:n:REASONSTRING` -- where `n` is a positive integer and `REASONSTRING` is an arbitrary -- string (may include spaces). parseReasonRateLimit :: (Monad m) => String -> m (String, Int) parseReasonRateLimit reason = case sepSplit ':' reason of "rate-limit":nStr:rest | Just n <- readMaybe nStr , n > 0 -> return (intercalate ":" (nStr:rest), n) _ -> fail $ "'" ++ reason ++ "' is not a valid ad-hoc rate limit reason" -- | Computes the bucket slots required by a job, also extracting how many -- slots are available from the reason rate limits in the job reason trails. -- -- A job can have multiple `OpCode`s, and the `ReasonTrail`s -- can be different for each `OpCode`. The `OpCode`s of a job are -- run sequentially, so a job can only take 1 slot. -- Thus a job takes part in a set of buckets, requiring 1 slot in -- each of them. labelCountMapFromJob :: QueuedJob -> CountMap (String, Int) labelCountMapFromJob job = let reasonsStrings = job ^.. qjOpsL . traverse . qoInputL . validOpCodeL . metaParamsL . opReasonL . traverse . _2 buckets = ordNub . mapMaybe parseReasonRateLimit $ reasonsStrings -- Buckets are already unique from `ordNub`. in Map.fromList $ map (, 1) buckets -- | Computes the bucket slots required by a job. countMapFromJob :: QueuedJob -> CountMap AdHocReasonKey countMapFromJob = Map.mapKeys (\(str, n) -> str ++ ":" ++ show n) . labelCountMapFromJob -- | Map of how many slots are in use for a given bucket, for a list of jobs. -- The slot limits are taken from the ad-hoc reason rate limiting strings. slotMapFromJobs :: [QueuedJob] -> SlotMap AdHocReasonKey slotMapFromJobs jobs = Map.mapKeys (\(str, n) -> str ++ ":" ++ show n) . Map.mapWithKey (\(_str, limit) occup -> Slot occup limit) . Map.unionsWith (+) . map labelCountMapFromJob $ jobs -- | Like `slotMapFromJobs`, but setting all occupation counts to 0. -- Useful to find what the bucket limits of a set of jobs are. unoccupiedSlotMapFromJobs :: [QueuedJob] -> SlotMap AdHocReasonKey unoccupiedSlotMapFromJobs = Map.map (\s -> s{ slotOccupied = 0 }) . slotMapFromJobs -- | Implements ad-hoc rate limiting using the reason trail as specified -- in `doc/design-optables.rst`. -- -- Reasons of form `rate-limit:n:REASONSTRING` define buckets that limit -- how many jobs with that reason can be running at the same time to -- a positive integer n of available slots. -- -- The used buckets map is currently not cached across `selectJobsToRun` -- invocations because the number of running jobs is typically small -- (< 100). reasonRateLimit :: Queue -> [JobWithStat] -> [JobWithStat] reasonRateLimit queue jobs = let -- For the purpose of rate limiting, manipulated jobs count as running. running = map jJob $ qRunning queue ++ qManipulated queue candidates = map jJob jobs -- Reason rate limiting slot map of the jobs running in the queue. -- All jobs determine the reason buckets, but only running jobs count -- to the initial limits. initSlotMap = unoccupiedSlotMapFromJobs (running ++ candidates) `occupySlots` toCountMap (slotMapFromJobs running) -- A job can be run (fits) if all buckets it takes part in have -- a free slot. If yes, accept the job and update the slotMap. -- Note: If the slotMap is overfull in some slots, but the job -- doesn't take part in any of those, it is to be accepted. accumFittingJobs slotMap job = let jobBuckets = countMapFromJob (jJob job) in if slotMap `hasSlotsFor` jobBuckets then (slotMap `occupySlots` jobBuckets, Just job) -- job fits else (slotMap, Nothing) -- job doesn't fit in catMaybes . snd . mapAccumL accumFittingJobs initSlotMap $ jobs