Feature/rule engine parallel execution - #1417
Conversation
Each triggered rule now runs in its own DI scope, so the scoped conversation, state and routing services start clean per run and no longer bleed between rules or into the caller's scope. The message hub observers are subscribed per run so rule-triggered conversations emit the same events as user-initiated ones. The nested agent/rule loops are flattened into a single list and dispatched via Parallel.ForEachAsync, throttled by MaxConcurrency (options -> RuleSettings -> built-in default of 5). Results are written into an indexed array so conversation ids keep rule order without a concurrent-add race. Triggered also takes a CancellationToken: it stops dispatching new rules, interrupts the inter-rule delay, and propagates to the caller. Per-rule failures are logged and isolated so one bad rule does not take down the others. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A cancelled run cannot both return its conversation ids and surface the cancellation, so the ids now ride along on the exception. Triggered throws RuleTriggerCanceledException, which derives from OperationCanceledException (existing handlers keep working) and exposes the conversations that were already started, so callers can still act on work that cannot be undone. Also drops the CancellationToken from the rule trigger endpoint, so a client disconnect no longer stops rules that are mid-dispatch. The token stays optional on IRuleEngine.Triggered for other callers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR Summary by QodoRun triggered rules in isolated, cancellable parallel scopes
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
1. Cancellation loses conversation IDs
|
| var delay = options?.SendMessageDelayMs ?? RuleTriggerOptions.DefaultSendMessageDelayMs; | ||
| if (delay > 0) | ||
| { | ||
| await Task.Delay(delay, cancellationToken); |
There was a problem hiding this comment.
1. Cancellation loses conversation ids 🐞 Bug ≡ Correctness
RunRule waits on a cancellation-aware delay after creating the conversation, but convIds is assigned only after that delay completes. Cancellation during the delay therefore omits an already-started conversation from RuleTriggerCanceledException.ConversationIds, defeating the exception's recovery contract.
Agent Prompt
## Issue description
A conversation ID is returned from `SendMessageToAgent`, but it is not stored in `convIds` until `RunRule` finishes its subsequent cancellation-aware delay. If cancellation occurs during that delay, the already-created conversation is missing from `RuleTriggerCanceledException.ConversationIds`.
## Issue Context
Preserve the post-send throttling behavior while ensuring the ID is recorded immediately after message processing succeeds and before any operation that can throw due to cancellation. One option is to move the delay to the parallel callback after assigning the returned ID.
## Fix Focus Areas
- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs[68-87]
- src/Infrastructure/BotSharp.Core.Rules/Engines/RuleEngine.cs[141-156]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| /// <param name="cancellationToken">Stops dispatching further rules and cancels the pause between them.</param> | ||
| /// <returns></returns> | ||
| /// <exception cref="NotImplementedException"></exception> | ||
| Task<IEnumerable<string>> Triggered(IRuleTrigger trigger, string text, IEnumerable<MessageState>? states = null, RuleTriggerOptions? options = null) | ||
| Task<IEnumerable<string>> Triggered(IRuleTrigger trigger, string text, IEnumerable<MessageState>? states = null, RuleTriggerOptions? options = null, CancellationToken cancellationToken = default) |
There was a problem hiding this comment.
2. Endpoint ignores request cancellation 🐞 Bug ☼ Reliability
The repository's only IRuleEngine.Triggered caller omits the newly added token, so HTTP request cancellation cannot stop dispatching rules or cancel their delays. Disconnected requests can therefore continue launching conversations and consuming downstream LLM capacity until the full batch completes.
Agent Prompt
## Issue description
The new cancellation-aware `Triggered` overload is called by the rule HTTP endpoint without a cancellation token. Consequently, request abortion never reaches the engine and all rule work continues with the default non-cancelable token.
## Issue Context
Pass `HttpContext.RequestAborted`, or accept an action `CancellationToken` and forward it as the final `Triggered` argument. Preserve the existing API response behavior for non-cancelled requests.
## Fix Focus Areas
- src/Infrastructure/BotSharp.Core.Rules/Controllers/RuleController.cs[25-40]
- src/Infrastructure/BotSharp.Abstraction/Rules/IRuleEngine.cs[9-18]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Drops Parallel.ForEachAsync in favour of a plain loop over the rules. Conversation ids are appended as each rule finishes, so the indexed array and its collect helper are no longer needed to keep them in order. MaxConcurrency only existed to throttle the parallel run, so it goes too, along with the RuleSettings class and its "Rule" config binding that were added to configure it. The inter-rule delay falls back to the per-call option and then the built-in default. Per-rule service scopes, cancellation and per-rule error isolation are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No description provided.