fix(schedule-engine,webapp): log out-of-entitlements scheduled triggers as warnings (#4067)

## Summary

When a scheduled task fires for an organization that is out of
entitlements, the trigger can't proceed. That's an expected outcome, but
it was being logged at error level and surfaced as a failure.

## Fix

The trigger callback now classifies an out-of-entitlements result as its
own error type (`OUT_OF_ENTITLEMENTS`), and the schedule engine logs
both that and the existing queue-limit result as warnings rather than
errors. The run still doesn't fire and the `schedule_execution_failure`
metric still records the outcome (now tagged `out_of_entitlements`), so
nothing about observability or behavior changes beyond the log level.
This commit is contained in:
Eric Allam
2026-06-29 19:10:10 +01:00
committed by GitHub
parent 4ef8baa971
commit aae1c6a512
4 changed files with 30 additions and 6 deletions
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---
Treat a scheduled task trigger that fails because the organization is out of entitlements as an expected outcome: the schedule engine now logs it as a warning instead of an error, mirroring how environment queue-limit results are already handled.
+6 -1
View File
@@ -6,7 +6,7 @@ import { env } from "~/env.server";
import { devPresence } from "~/presenters/v3/DevPresence.server";
import { logger } from "~/services/logger.server";
import { singleton } from "~/utils/singleton";
import { TriggerTaskService } from "./services/triggerTask.server";
import { OutOfEntitlementError, TriggerTaskService } from "./services/triggerTask.server";
import { meter, tracer } from "./tracer.server";
import { workerQueue } from "~/services/worker.server";
import { ServiceValidationError } from "./services/common.server";
@@ -136,6 +136,11 @@ function createScheduleEngine() {
errorMessage.includes("queue size limit for this environment has been reached")
) {
errorType = "QUEUE_LIMIT";
} else if (error instanceof OutOfEntitlementError) {
// The org is out of entitlements. This is an expected outcome, not a
// system error, so the engine logs it as a warning rather than
// reporting it as an error.
errorType = "OUT_OF_ENTITLEMENTS";
}
return {
@@ -522,13 +522,19 @@ export class ScheduleEngine {
span.setAttribute("trigger_success", true);
} else {
const isQueueLimit = result.errorType === "QUEUE_LIMIT";
// QUEUE_LIMIT and OUT_OF_ENTITLEMENTS are expected,
// non-actionable outcomes (the environment is at its queue limit,
// or the org is out of entitlements). Log them as warnings so they
// aren't reported as errors, while still recording the metric.
const isExpectedFailure =
result.errorType === "QUEUE_LIMIT" || result.errorType === "OUT_OF_ENTITLEMENTS";
if (isQueueLimit) {
this.logger.warn("Scheduled task trigger skipped due to queue limit", {
if (isExpectedFailure) {
this.logger.warn("Scheduled task trigger skipped", {
instanceId: params.instanceId,
taskIdentifier: instance.taskSchedule.taskIdentifier,
durationMs: triggerDuration,
errorType: result.errorType,
error: result.error,
});
} else {
@@ -540,10 +546,17 @@ export class ScheduleEngine {
});
}
const failureErrorType =
result.errorType === "QUEUE_LIMIT"
? "queue_limit"
: result.errorType === "OUT_OF_ENTITLEMENTS"
? "out_of_entitlements"
: "task_failure";
this.scheduleExecutionFailureCounter.add(1, {
environment_type: environmentType,
schedule_type: scheduleType,
error_type: isQueueLimit ? "queue_limit" : "task_failure",
error_type: failureErrorType,
});
span.setAttribute("trigger_success", false);
@@ -24,7 +24,7 @@ export type TriggerScheduledTaskParams = {
exactScheduleTime?: Date;
};
export type TriggerScheduledTaskErrorType = "QUEUE_LIMIT" | "SYSTEM_ERROR";
export type TriggerScheduledTaskErrorType = "QUEUE_LIMIT" | "OUT_OF_ENTITLEMENTS" | "SYSTEM_ERROR";
export interface TriggerScheduledTaskCallback {
(params: TriggerScheduledTaskParams): Promise<{