Skip to content

Fix Twilio IncomingMessage timeouts#338

Merged
ucswift merged 1 commit intomasterfrom
develop
Apr 21, 2026
Merged

Fix Twilio IncomingMessage timeouts#338
ucswift merged 1 commit intomasterfrom
develop

Conversation

@ucswift
Copy link
Copy Markdown
Member

@ucswift ucswift commented Apr 21, 2026

  • SubscriptionsService: reduce billing API MaxTimeout from 200s to 5s and wrap call in try-catch so network/timeout errors fall back to freePlan instead of hanging the request past Twilio's 15s webhook deadline
  • TwilioController.IncomingMessage: run the four independent department lookups (GetDepartmentById, GetTextToCallSourceNumbers, CanDepartmentProvisionNumber, GetAllActiveCustomStates) concurrently with Task.WhenAll instead of sequentially
  • TwilioController.IncomingMessage: reuse the UserProfile already fetched in the department-resolution block instead of issuing a second DB call inside the !isDispatchSource branch

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling for billing plan lookups to ensure graceful fallback when API issues occur.
  • Performance

    • Reduced response time for billing plan checks through optimized API timeout settings.
    • Accelerated incoming message processing through concurrent data operations and reduced redundant queries.

- SubscriptionsService: reduce billing API MaxTimeout from 200s to 5s and
  wrap call in try-catch so network/timeout errors fall back to freePlan
  instead of hanging the request past Twilio's 15s webhook deadline
- TwilioController.IncomingMessage: run the four independent department
  lookups (GetDepartmentById, GetTextToCallSourceNumbers,
  CanDepartmentProvisionNumber, GetAllActiveCustomStates) concurrently
  with Task.WhenAll instead of sequentially
- TwilioController.IncomingMessage: reuse the UserProfile already fetched
  in the department-resolution block instead of issuing a second DB call
  inside the !isDispatchSource branch

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

Two service files modified: SubscriptionsService adds error handling and reduces timeout for Billing API calls, while TwilioController optimizes async operations by executing four concurrent department-scoped lookups instead of sequential calls and reuses cached profile data.

Changes

Cohort / File(s) Summary
Billing API Resilience
Core/Resgrid.Services/SubscriptionsService.cs
Wrapped GetCurrentPlanForDepartmentAsync Billing API call in try/catch block; reduced REST client timeout from 200000ms to 5000ms; logs exceptions via Framework.Logging and returns pre-fetched freePlan fallback on failure.
Concurrent Lookups & Profile Reuse
Web/Resgrid.Web.Services/Controllers/TwilioController.cs
Changed IncomingMessage logic to execute four async department-scoped lookups (GetDepartmentByIdAsync, GetTextToCallSourceNumbersForDepartmentAsync, CanDepartmentProvisionNumberAsync, GetAllActiveCustomStatesForDepartmentAsync) concurrently; optimized profile retrieval to reuse previously-fetched userProfile instead of re-querying when available.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main objective of the PR—fixing Twilio IncomingMessage timeouts through reduced API timeouts and concurrent lookups.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
Core/Resgrid.Services/SubscriptionsService.cs (1)

59-86: LGTM — sensible fail-fast + fallback for webhook path.

The 5s cap plus try/catch fallback to freePlan keeps the Twilio webhook within its 15s deadline, and existing NotFound / null-data fallbacks are preserved. Minor, optional considerations (non-blocking):

  • catch (Exception ex) will also swallow OperationCanceledException from a caller-supplied token. If you ever thread a CancellationToken through this method, consider rethrowing cancellations:
    catch (Exception ex) when (ex is not OperationCanceledException)
  • The RestClient is not disposed here (pre-existing pattern throughout the file). Not introduced by this PR, but worth tracking — RestClient in RestSharp v107+ owns an HttpClient and is intended to be long-lived/reused; per-call construction can exhaust sockets under load. Consider caching a single RestClient per base URL as a follow-up across this file.
  • Only GetCurrentPlanForDepartmentAsync gets the 5s timeout; the other billing calls in this file still use MaxTimeout = 200000. That's fine if they're not on the Twilio hot path, but if any of them are reachable from the webhook transitively, they'd reintroduce the same problem.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/Resgrid.Services/SubscriptionsService.cs` around lines 59 - 86, The
catch-all in the GetCurrentPlanForDepartmentAsync call swallows caller
cancellations; update the exception handling around the
RestClient/ExecuteAsync<GetCurrentPlanForDepartmentResult> block so
OperationCanceledException is not caught—i.e., only log and return freePlan for
non-cancellation exceptions and rethrow or let OperationCanceledException
propagate; keep references to RestClient,
ExecuteAsync<GetCurrentPlanForDepartmentResult>, freePlan, and
Framework.Logging.LogException when locating the code to change.
Web/Resgrid.Web.Services/Controllers/TwilioController.cs (1)

146-157: Minor cleanup for the parallelized lookups.

The parallelization is correct — per the provided context snippets, all four calls are independent and safe to fan out. A few small polish items on the new block:

  • System.Threading.Tasks is already imported (line 7); the fully-qualified System.Threading.Tasks.Task.WhenAll can just be Task.WhenAll.
  • After await Task.WhenAll(...), prefer await task over task.Result to unwrap values — it's more idiomatic C# and avoids AggregateException-style unwrapping semantics if anything ever changes upstream.
  • The new local authroized carries forward a misspelling (also present further down in this file around line 747). Worth renaming to authorized while you're touching this code.

Also note that if any of these four tasks throws, await Task.WhenAll rethrows only the first exception; the remaining faults are still observed (tasks are awaited), so you're fine w.r.t. UnobservedTaskException, and the outer try/catch at line 448 will log it and the finally will still persist messageEvent. No functional concern — just flagging for awareness.

♻️ Proposed refactor
-					// Run all department-level lookups in parallel — they are independent of each other.
-					var departmentTask = _departmentsService.GetDepartmentByIdAsync(departmentId.Value);
-					var dispatchNumbersTask = _departmentSettingsService.GetTextToCallSourceNumbersForDepartmentAsync(departmentId.Value);
-					var authorizedTask = _limitsService.CanDepartmentProvisionNumberAsync(departmentId.Value);
-					var customStatesTask = _customStateService.GetAllActiveCustomStatesForDepartmentAsync(departmentId.Value);
-
-					await System.Threading.Tasks.Task.WhenAll(departmentTask, dispatchNumbersTask, authorizedTask, customStatesTask);
-
-					var department = departmentTask.Result;
-					var dispatchNumbers = dispatchNumbersTask.Result;
-					var authroized = authorizedTask.Result;
-					var customStates = customStatesTask.Result;
+					// Run all department-level lookups in parallel — they are independent of each other.
+					var departmentTask = _departmentsService.GetDepartmentByIdAsync(departmentId.Value);
+					var dispatchNumbersTask = _departmentSettingsService.GetTextToCallSourceNumbersForDepartmentAsync(departmentId.Value);
+					var authorizedTask = _limitsService.CanDepartmentProvisionNumberAsync(departmentId.Value);
+					var customStatesTask = _customStateService.GetAllActiveCustomStatesForDepartmentAsync(departmentId.Value);
+
+					await Task.WhenAll(departmentTask, dispatchNumbersTask, authorizedTask, customStatesTask);
+
+					var department = await departmentTask;
+					var dispatchNumbers = await dispatchNumbersTask;
+					var authorized = await authorizedTask;
+					var customStates = await customStatesTask;

You'll want to follow through and rename authroizedauthorized at line 161 as well.

As per coding guidelines: "Use modern C# features appropriately" and "Use meaningful, descriptive names for types, methods, and parameters; avoid unclear abbreviations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Web/Resgrid.Web.Services/Controllers/TwilioController.cs` around lines 146 -
157, Replace the fully-qualified System.Threading.Tasks.Task.WhenAll call with
Task.WhenAll and, after awaiting it, unwrap each result by awaiting the
individual tasks (await departmentTask, await dispatchNumbersTask, await
authorizedTask, await customStatesTask) instead of using .Result; also correct
the misspelled local variable authroized to authorized (and update any other
occurrences of that misspelling in this file) so the locals are department,
dispatchNumbers, authorized, and customStates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Core/Resgrid.Services/SubscriptionsService.cs`:
- Around line 59-86: The catch-all in the GetCurrentPlanForDepartmentAsync call
swallows caller cancellations; update the exception handling around the
RestClient/ExecuteAsync<GetCurrentPlanForDepartmentResult> block so
OperationCanceledException is not caught—i.e., only log and return freePlan for
non-cancellation exceptions and rethrow or let OperationCanceledException
propagate; keep references to RestClient,
ExecuteAsync<GetCurrentPlanForDepartmentResult>, freePlan, and
Framework.Logging.LogException when locating the code to change.

In `@Web/Resgrid.Web.Services/Controllers/TwilioController.cs`:
- Around line 146-157: Replace the fully-qualified
System.Threading.Tasks.Task.WhenAll call with Task.WhenAll and, after awaiting
it, unwrap each result by awaiting the individual tasks (await departmentTask,
await dispatchNumbersTask, await authorizedTask, await customStatesTask) instead
of using .Result; also correct the misspelled local variable authroized to
authorized (and update any other occurrences of that misspelling in this file)
so the locals are department, dispatchNumbers, authorized, and customStates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2d59eb31-b74b-4554-a9ff-8ba87a91af95

📥 Commits

Reviewing files that changed from the base of the PR and between b32d621 and 5a61660.

📒 Files selected for processing (2)
  • Core/Resgrid.Services/SubscriptionsService.cs
  • Web/Resgrid.Web.Services/Controllers/TwilioController.cs

@ucswift
Copy link
Copy Markdown
Member Author

ucswift commented Apr 21, 2026

Approve

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is approved.

@ucswift ucswift merged commit 8999854 into master Apr 21, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant