{"record":{"id":"0d572ee7cbf6d75f","repo":"microsoft/aspire","slug":"graceful-budget-cannot-be-negative","errorCode":null,"errorMessage":"Graceful budget cannot be negative.","messagePattern":"Graceful budget cannot be negative\\.","errorType":"validation","errorClass":"ArgumentOutOfRangeException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Cli/ConsoleCancellationManager.cs","lineNumber":188,"sourceCode":"    /// Whether graceful shutdown is enabled for the running command — i.e. a positive budget was\n    /// configured via <see cref=\"ConfigureForCommand\"/>. When <see langword=\"false\"/>, shutdown ladders\n    /// escalate straight to forceful termination.\n    /// </summary>\n    public bool IsEnabled => _gracefulBudget > TimeSpan.Zero;\n\n    public bool IsCancellationRequested => _cts.IsCancellationRequested;\n\n    /// <summary>\n    /// Sets the graceful-shutdown budget for the currently-executing command. Default is zero, meaning\n    /// ladders that consume <see cref=\"GracefulShutdownToken\"/> fall through to escalation immediately\n    /// (preserving today's behavior for every command that doesn't opt in). The <c>aspire run</c> handler\n    /// calls this so the AppHost gets a real cooperative-shutdown window before escalation.\n    /// </summary>\n    public void ConfigureForCommand(TimeSpan gracefulBudget)\n    {\n        if (gracefulBudget < TimeSpan.Zero)\n        {\n            throw new ArgumentOutOfRangeException(nameof(gracefulBudget), \"Graceful budget cannot be negative.\");\n        }\n\n        _gracefulBudget = gracefulBudget;\n    }\n\n    /// <summary>\n    /// Starts the graceful-shutdown clock. Idempotent — the first caller arms a <c>CancelAfter(budget)</c>\n    /// so <see cref=\"GracefulShutdownToken\"/> is guaranteed to fire within the budget; subsequent calls are\n    /// no-ops. Called by whoever initiates teardown (a user signal via <see cref=\"Cancel\"/>, or a child\n    /// owner's disposal-driven ladder) so the token is always bounded.\n    /// </summary>\n    public void BeginGracefulWindow()\n    {\n        // When a debugger is attached, never arm the clock — the developer needs unlimited time to step\n        // through cancellation/cleanup logic. The token therefore never auto-fires; ladders that observe it\n        // sit indefinitely (the right behavior for stepping). A manual second Ctrl+C still escalates because\n        // it calls Expire() directly, bypassing this method.\n        if (Debugger.IsAttached)","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Cli/ConsoleCancellationManager.cs#L170-L206","documentation":"ConsoleCancellationManager.ConfigureForCommand validates the cooperative-shutdown graceful budget and throws ArgumentOutOfRangeException when a negative TimeSpan is supplied. The budget controls how long Ctrl+C lets the AppHost shut down gracefully before escalation, so a negative value is meaningless.","triggerScenarios":"Calling ConfigureForCommand with a negative TimeSpan (e.g. TimeSpan.FromSeconds(-5)), often from a miscomputed value, a parsed setting that allowed negatives, or a default computed by subtraction.","commonSituations":"Config-driven timeouts where a negative env/config value was passed through unvalidated; arithmetic like (a - b) producing negative durations; typos in timeout constants.","solutions":["Pass a non-negative TimeSpan to ConfigureForCommand.","Clamp or validate the value at the call site before configuring, e.g. TimeSpan.FromMinutes(1) as a default.","If the value is user/config-sourced, validate it is >= TimeSpan.Zero before applying."],"exampleFix":"// before\nmanager.ConfigureForCommand(TimeSpan.FromSeconds(-5));\n// after\nvar budget = TimeSpan.FromSeconds(Math.Max(0, configuredSeconds));\nmanager.ConfigureForCommand(budget);","handlingStrategy":"validation","validationCode":"if (gracefulBudget < TimeSpan.Zero)\n    throw new ArgumentException(\"Graceful budget must be non-negative.\", nameof(gracefulBudget));","typeGuard":null,"tryCatchPattern":"try\n{\n    manager.ConfigureForCommand(budget);\n}\ncatch (ArgumentOutOfRangeException ex) when (ex.ParamName == nameof(gracefulBudget))\n{\n    logger.LogWarning(\"Configured graceful budget was negative; falling back to default.\");\n    manager.ConfigureForCommand(TimeSpan.FromSeconds(30));\n}","preventionTips":["Clamp parsed timeout values with Math.Max(TimeSpan.Zero, value).","Validate config/env-sourced durations before applying them.","Avoid deriving budgets from unchecked subtractions."],"tags":["argument-validation","cancellation","timeout"],"backgroundTag":"argument-out-of-range","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T09:17:21.228Z"}