fix(webapp,run-engine,core): drop the hidden debounce ceiling, fail fast on an unusable maxDelay (#4521)
Debouncing with a `delay` longer than an hour did nothing at all. The engine applied a server-side ceiling on how long a debounced run could be pushed back, measured from the run's `createdAt` and defaulting to one hour. A run is only pushed back while its new execution time stays inside that ceiling, so a `delay` at or above it could never push anything: the waiting run was released, the trigger started its own run, and the next trigger repeated it. A `delay: "12h"` produced one run per trigger, each correctly delayed by 12h, with no error raised and nothing on the run to show the debounce key had been ignored. The ceiling is now unset by default. A debounce key with no `maxDelay` keeps collapsing triggers for as long as they keep arriving, which is what the docs have always described. Self-hosters who want a bound can still set `RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS`. That has a consequence worth stating plainly, so the docs now carry a warning for it: with no `maxDelay`, a continuously triggered key never executes. Set `maxDelay` when the work has to happen eventually. **Failing fast on an unusable `maxDelay`.** A caller who sets `maxDelay` no longer than their `delay` hits exactly the dead end described above, so that pair is now rejected at trigger time instead of silently behaving as if no debounce were set: ``` debounce.maxDelay (1h) must be longer than debounce.delay (12h). A debounced run is only pushed back while it stays inside maxDelay, so with these values every trigger would create its own run. ``` An unparseable `maxDelay` is rejected too, rather than quietly falling back to no bound at all, and so is a `delay` given as a date rather than a duration, which could never work because the value is re-applied on every push. The same check runs against a configured server ceiling, so a self-hosted deployment that sets `RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS` gets the error rather than the silent failure this PR is about. With no `maxDelay` and no configured ceiling, which is the default, there is nothing to conflict with and nothing is rejected. The docs, the `TriggerOptions` JSDoc and the engine option all now state that the room available to push is the gap between `delay` and `maxDelay`. The run engine suite gains the case that motivated this: four triggers on one key with a 12h delay now collapse to a single run.
This commit is contained in:
+16
-4
@@ -873,7 +873,7 @@ The `debounce` option accepts:
|
||||
- `key` - A unique string to identify the debounce group (scoped to the task)
|
||||
- `delay` - Duration string specifying how long to delay. Supported units: `s` (seconds), `m` (minutes), `h`/`hr` (hours), `d` (days), `w` (weeks). Minimum is 1 second. Examples: `"5s"`, `"1m"`, `"2h30m"`
|
||||
- `mode` - Optional. Controls which trigger's data is used: `"leading"` (default) or `"trailing"`
|
||||
- `maxDelay` - Optional. Maximum total time from the first trigger before the run must execute. Uses the same duration format as `delay`
|
||||
- `maxDelay` - Optional. Maximum total time from the first trigger before the run must execute. Uses the same duration format as `delay`. Not set by default
|
||||
|
||||
**How it works:**
|
||||
|
||||
@@ -882,9 +882,18 @@ The `debounce` option accepts:
|
||||
3. Once no new triggers occur within the delay duration, the run executes
|
||||
4. After the run starts executing, a new trigger with the same key will create a new run
|
||||
|
||||
<Warning>
|
||||
There is no time limit on step 2. While triggers keep arriving on the same key, the run keeps
|
||||
being pushed back and never executes. A key triggered every 10 seconds with a `delay` of `"30s"`
|
||||
runs 30 seconds after the triggers stop, however long that takes. Set `maxDelay` whenever the
|
||||
work needs to happen eventually. This matters most with `triggerAndWait`: every parent waiting
|
||||
on a debounced run stays blocked, and holds its concurrency, for as long as the run keeps being
|
||||
pushed back.
|
||||
</Warning>
|
||||
|
||||
**Limiting total delay with `maxDelay`:**
|
||||
|
||||
By default, continuous triggers can delay execution indefinitely. The `maxDelay` option sets an upper bound on the total delay from the first trigger, ensuring the run eventually executes even with constant activity.
|
||||
The `maxDelay` option sets an upper bound on the total delay from the first trigger, ensuring the run eventually executes even with constant activity.
|
||||
|
||||
```ts
|
||||
await summarizeChat.trigger(
|
||||
@@ -921,11 +930,14 @@ Consider `delay: "5s"` and `maxDelay: "30s"` with triggers arriving every 2 seco
|
||||
|
||||
Without `maxDelay`, continuous triggers would prevent the run from ever executing. With `maxDelay: "30s"`, execution is guaranteed within 30 seconds of the first trigger.
|
||||
|
||||
Keep `delay` well below `maxDelay`. A run is only pushed back while its new execution time stays inside `maxDelay`, so the room you have to push is `maxDelay` minus `delay`. Setting them equal, or setting `delay` higher, leaves no room at all, and the trigger is rejected rather than accepted as a debounce that could never collapse anything.
|
||||
|
||||
<Note>
|
||||
The `maxDelay` value is evaluated from each trigger call, not stored with the original run. This
|
||||
means if you pass different `maxDelay` values for the same debounce key, each trigger uses its own
|
||||
`maxDelay` to check against the original run's creation time. For consistent behavior, use the
|
||||
same `maxDelay` value for all triggers with the same debounce key.
|
||||
`maxDelay` to check against the original run's creation time. A trigger that omits `maxDelay`
|
||||
has no bound at all, so a single call without it can push the run past the limit the other calls
|
||||
set. Use the same `maxDelay` value for every trigger with the same debounce key.
|
||||
</Note>
|
||||
|
||||
**Leading vs Trailing mode:**
|
||||
|
||||
Reference in New Issue
Block a user