Skip to content

Commit 10478c1

Browse files
author
Thomas Gorissen
committed
Add X-Eval-Alloc-Bytes response header to evaluation endpoints
Track GHC allocation bytes consumed per evaluation and return them via response header for billing and resource monitoring.
1 parent 8f1fc86 commit 10478c1

File tree

1 file changed

+33
-24
lines changed

1 file changed

+33
-24
lines changed

jl4-service/src/DataPlane.hs

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import Data.Scientific (Scientific)
3737
import qualified Data.Set as Set
3838
import Data.Text (Text)
3939
import qualified Data.Text as Text
40-
import GHC.Conc (setAllocationCounter, enableAllocationLimit)
40+
import GHC.Conc (setAllocationCounter, getAllocationCounter, enableAllocationLimit)
4141
import GHC.IO.Exception (AllocationLimitExceeded (..))
4242
import Servant
4343
import System.FilePath ((<.>))
@@ -72,10 +72,14 @@ type DeploymentRoutes =
7272
:<|> "functions" :> Capture "name" Text :> FunctionRoutes
7373
:<|> "openapi.json" :> Get '[JSON] DeploymentMetadata -- Simplified; returns metadata for now
7474

75+
-- | Evaluation responses include an X-Eval-Alloc-Bytes header reporting
76+
-- the GHC allocation bytes consumed by the evaluation.
77+
type EvalResponse a = Headers '[Header "X-Eval-Alloc-Bytes" Int64] a
78+
7579
type FunctionRoutes =
7680
Get '[JSON] Function
77-
:<|> "evaluation" :> Header "X-L4-Trace" Text :> QueryParam "trace" TraceLevel :> QueryParam "graphviz" Bool :> ReqBody '[JSON] FnArguments :> Post '[JSON] SimpleResponse
78-
:<|> "evaluation" :> "batch" :> Header "X-L4-Trace" Text :> QueryParam "trace" TraceLevel :> QueryParam "graphviz" Bool :> ReqBody '[JSON] BatchRequest :> Post '[JSON] BatchResponse
81+
:<|> "evaluation" :> Header "X-L4-Trace" Text :> QueryParam "trace" TraceLevel :> QueryParam "graphviz" Bool :> ReqBody '[JSON] FnArguments :> Post '[JSON] (EvalResponse SimpleResponse)
82+
:<|> "evaluation" :> "batch" :> Header "X-L4-Trace" Text :> QueryParam "trace" TraceLevel :> QueryParam "graphviz" Bool :> ReqBody '[JSON] BatchRequest :> Post '[JSON] (EvalResponse BatchResponse)
7983
:<|> "query-plan" :> ReqBody '[JSON] FnArguments :> Post '[JSON] QueryPlanResponse
8084
:<|> "state-graphs" :> Get '[JSON] StateGraphListResponse
8185
:<|> "state-graphs" :> Capture "graphName" Text :> Get '[PlainText] Text
@@ -163,7 +167,7 @@ evalFunctionHandler
163167
-> Maybe TraceLevel -- ?trace= query param
164168
-> Maybe Bool -- ?graphviz= query param
165169
-> FnArguments
166-
-> AppM SimpleResponse
170+
-> AppM (EvalResponse SimpleResponse)
167171
evalFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz fnArgs = do
168172
(fns, _meta) <- requireDeploymentReady deployId
169173
vf <- requireFunction fns fnName
@@ -173,7 +177,7 @@ evalFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz fnArgs =
173177
isDeontic = Map.member "startTime" paramMap
174178
&& Map.member "events" paramMap
175179

176-
case (isDeontic, fnArgs.startTime, fnArgs.events) of
180+
(result, allocBytes) <- case (isDeontic, fnArgs.startTime, fnArgs.events) of
177181
-- Non-deontic function: reject deontic params
178182
(False, Just _, _) ->
179183
throwError err400 { errBody = jsonError "startTime and events are only valid for functions returning DEONTIC" }
@@ -192,6 +196,7 @@ evalFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz fnArgs =
192196
runDeonticEvaluatorFor vf fnArgs.fnEvalBackend (Map.toList fnArgs.fnArguments) st evts
193197
vf.fnImpl.deonticPartyType vf.fnImpl.deonticActionType
194198
mTraceHeader mTraceParam mGraphViz
199+
pure $ addHeader allocBytes result
195200

196201
-- | POST /deployments/{id}/functions/{fn}/evaluation/batch
197202
batchFunctionHandler
@@ -201,7 +206,7 @@ batchFunctionHandler
201206
-> Maybe TraceLevel -- ?trace= query param
202207
-> Maybe Bool -- ?graphviz= query param
203208
-> BatchRequest
204-
-> AppM BatchResponse
209+
-> AppM (EvalResponse BatchResponse)
205210
batchFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz batchArgs = do
206211
let traceLevel = determineTraceLevel mTraceHeader mTraceParam
207212
includeGraphViz = traceLevel == TraceFull && Maybe.fromMaybe False mGraphViz
@@ -211,7 +216,7 @@ batchFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz batchArg
211216
-- Capture the environment before going concurrent
212217
env <- ask
213218

214-
-- Evaluate all cases in parallel
219+
-- Evaluate all cases in parallel, collecting alloc bytes per case
215220
evalResults <- liftIO $ forConcurrently batchArgs.cases $ \inputCase -> do
216221
let args = Map.assocs $ fmap Just inputCase.attributes
217222
r <- runAppM env (runEvaluatorForDirect vf Nothing args outputFilter traceLevel includeGraphViz)
@@ -223,12 +228,13 @@ batchFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz batchArg
223228
[] -> pure ()
224229

225230
let
226-
responses = [(rid, simpleResp) | (rid, Right simpleResp) <- evalResults]
231+
responses = [(rid, simpleResp, alloc) | (rid, Right (simpleResp, alloc)) <- evalResults]
227232
nCases = length responses
233+
totalAllocBytes = sum [alloc | (_, _, alloc) <- responses]
228234

229235
successfulRuns =
230236
Maybe.mapMaybe
231-
( \(rid, simpleRes) -> case simpleRes of
237+
( \(rid, simpleRes, _) -> case simpleRes of
232238
SimpleResponse r -> Just (rid, r)
233239
SimpleError _ -> Nothing
234240
)
@@ -237,7 +243,7 @@ batchFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz batchArg
237243
nSuccessful = length successfulRuns
238244
nIgnored = nCases - nSuccessful
239245

240-
pure $ BatchResponse
246+
pure $ addHeader totalAllocBytes $ BatchResponse
241247
{ cases =
242248
[ OutputCase
243249
{ id = rid
@@ -341,7 +347,7 @@ openApiHandler deployId = do
341347
-- Evaluation helpers
342348
-- ----------------------------------------------------------------------------
343349

344-
-- | Run a function evaluator, returning SimpleResponse.
350+
-- | Run a function evaluator, returning SimpleResponse and alloc bytes.
345351
runEvaluatorFor
346352
:: ValidatedFunction
347353
-> Maybe EvalBackend
@@ -350,27 +356,27 @@ runEvaluatorFor
350356
-> Maybe Text -- X-L4-Trace header
351357
-> Maybe TraceLevel -- ?trace= query param
352358
-> Maybe Bool -- ?graphviz= query param
353-
-> AppM SimpleResponse
359+
-> AppM (SimpleResponse, Int64)
354360
runEvaluatorFor vf engine args outputFilter mTraceHeader mTraceParam mGraphViz = do
355361
let traceLevel = determineTraceLevel mTraceHeader mTraceParam
356362
includeGraphViz = traceLevel == TraceFull && Maybe.fromMaybe False mGraphViz
357363
runEvaluatorForDirect vf engine args outputFilter traceLevel includeGraphViz
358364

359-
-- | Core evaluator logic.
365+
-- | Core evaluator logic. Returns the response and GHC allocation bytes consumed.
360366
runEvaluatorForDirect
361367
:: ValidatedFunction
362368
-> Maybe EvalBackend
363369
-> [(Text, Maybe FnLiteral)]
364370
-> Maybe (Set.Set Text)
365371
-> TraceLevel
366372
-> Bool
367-
-> AppM SimpleResponse
373+
-> AppM (SimpleResponse, Int64)
368374
runEvaluatorForDirect vf engine args outputFilter traceLevel includeGraphViz = do
369375
let evalBackend = Maybe.fromMaybe JL4 engine
370376
case Map.lookup evalBackend vf.fnEvaluator of
371377
Nothing -> throwError err500 { errBody = jsonError "No evaluator available for backend" }
372378
Just runFn -> do
373-
evaluationResult <-
379+
(evaluationResult, allocBytes) <-
374380
timeoutAction $
375381
runExceptT
376382
( runFn.runFunction
@@ -380,8 +386,8 @@ runEvaluatorForDirect vf engine args outputFilter traceLevel includeGraphViz = d
380386
includeGraphViz
381387
)
382388
case evaluationResult of
383-
Left err -> pure $ SimpleError err
384-
Right r -> pure $ SimpleResponse r
389+
Left err -> pure (SimpleError err, allocBytes)
390+
Right r -> pure (SimpleResponse r, allocBytes)
385391

386392
-- | Run deontic evaluation with EVALTRACE.
387393
runDeonticEvaluatorFor
@@ -395,7 +401,7 @@ runDeonticEvaluatorFor
395401
-> Maybe Text -- X-L4-Trace header
396402
-> Maybe TraceLevel -- ?trace= query param
397403
-> Maybe Bool -- ?graphviz= query param
398-
-> AppM SimpleResponse
404+
-> AppM (SimpleResponse, Int64)
399405
runDeonticEvaluatorFor vf _engine args startTime events mPartyType mActionType mTraceHeader mTraceParam mGraphViz = do
400406
let traceLevel = determineTraceLevel mTraceHeader mTraceParam
401407
includeGraphViz = traceLevel == TraceFull && Maybe.fromMaybe False mGraphViz
@@ -407,7 +413,7 @@ runDeonticEvaluatorFor vf _engine args startTime events mPartyType mActionType m
407413
let fnDecl = toDecl vf.fnImpl
408414
filepath = Text.unpack vf.fnImpl.name <.> "l4"
409415

410-
evaluationResult <-
416+
(evaluationResult, allocBytes) <-
411417
timeoutAction $
412418
runExceptT
413419
( evaluateWithCompiledDeontic
@@ -424,16 +430,17 @@ runDeonticEvaluatorFor vf _engine args startTime events mPartyType mActionType m
424430
)
425431

426432
case evaluationResult of
427-
Left err -> pure $ SimpleError err
428-
Right r -> pure $ SimpleResponse r
433+
Left err -> pure (SimpleError err, allocBytes)
434+
Right r -> pure (SimpleResponse r, allocBytes)
429435

430436
-- | Run an AppM action in IO with a given environment.
431437
runAppM :: AppEnv -> AppM a -> IO (Either ServerError a)
432438
runAppM env action = runHandler $ runReaderT action env
433439

434440
-- | Timeout and memory-limited evaluation action.
435441
-- Uses configurable eval timeout and per-evaluation allocation limits.
436-
timeoutAction :: IO b -> AppM b
442+
-- Returns the result and the number of GHC allocation bytes consumed.
443+
timeoutAction :: IO b -> AppM (b, Int64)
437444
timeoutAction act = do
438445
cfg <- asks (.options)
439446
let timeoutMicros = cfg.evalTimeout * 1_000_000
@@ -442,12 +449,14 @@ timeoutAction act = do
442449
(timeout timeoutMicros $ do
443450
setAllocationCounter memLimitBytes
444451
enableAllocationLimit
445-
act
452+
r <- act
453+
remaining <- getAllocationCounter
454+
pure (r, memLimitBytes - remaining)
446455
) `catch` \AllocationLimitExceeded ->
447456
pure Nothing
448457
case result of
449458
Nothing -> throwError err500 { errBody = jsonError "Evaluation resource limit exceeded" }
450-
Just r -> pure r
459+
Just x -> pure x
451460

452461
-- ----------------------------------------------------------------------------
453462
-- Helpers

0 commit comments

Comments
 (0)