Skip to content

refactor: complete async/await migration#70

Merged
blackms merged 3 commits into
mainfrom
refactor/issue-21-async-migration
Apr 12, 2026
Merged

refactor: complete async/await migration#70
blackms merged 3 commits into
mainfrom
refactor/issue-21-async-migration

Conversation

@blackms

@blackms blackms commented Apr 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Migrated web controllers (Buy, Sell, Swap, BuyDefault) from sync to async, now calling BuyAsync/SellAsync/SwapAsync instead of blocking sync wrappers
  • Migrated LegacyTradingServiceAdapter from sync Buy/Sell/Swap calls to proper async with CancellationToken propagation
  • Migrated web services (SignalRBroadcasterService, TradingHub) from GetCurrentPrice to GetCurrentPriceAsync
  • Added ConfigureAwait(false) to all await calls in library projects (20 files across Core, Exchange, Trading, Infrastructure layers) to prevent potential deadlocks
  • Updated web controller tests to verify async method calls

Scope decisions

  • Sync wrapper methods (GetTickers, GetCurrentPrice, PlaceOrder, etc. in TradingService) are preserved because their callers in TradingAccountBase/ExchangeAccount operate inside lock blocks -- converting those is a larger refactor
  • HighResolutionTimedTask.Run() pattern is not changed (scope of issue Migrate to IHostedService #22)
  • BinanceExchangeService.Start()/ConnectTickersWebsocket() retain GetAwaiter().GetResult() since they run during startup with no SynchronizationContext

Test plan

  • All 1,588 tests pass (0 failures, 27 skipped -- pre-existing skips)
  • Build succeeds with no new errors (2 pre-existing CS0122 in Performance tests)
  • Web controller tests updated to verify async method calls with CancellationToken
  • CI green

Closes #21

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Refactor

    • Improved asynchronous operation handling throughout trading and background services for better thread-pool efficiency
    • Converted trading endpoints to properly async/await patterns for improved responsiveness
    • Updated internal service methods to properly handle asynchronous operations with cancellation token support
  • Tests

    • Updated trading controller tests to match async method implementations

@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@blackms has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 17 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 5 minutes and 17 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8995fe7-1bab-4c2d-b682-c78ef9fd8aa3

📥 Commits

Reviewing files that changed from the base of the PR and between 41b1abb and 02884ad.

📒 Files selected for processing (26)
  • IntelliTrader.Core/Services/CoreService.cs
  • IntelliTrader.Exchange.Base/Services/SecretRotationService.cs
  • IntelliTrader.Exchange.Binance/Services/BinanceExchangeService.cs
  • IntelliTrader.Exchange.Binance/Services/BinanceWebSocketService.cs
  • IntelliTrader.Infrastructure/Adapters/Exchange/BinanceExchangeAdapter.cs
  • IntelliTrader.Infrastructure/Adapters/Legacy/LegacyTradingServiceAdapter.cs
  • IntelliTrader.Infrastructure/Adapters/Persistence/Json/JsonPositionRepository.cs
  • IntelliTrader.Infrastructure/BackgroundServices/OrderExecutionService.cs
  • IntelliTrader.Infrastructure/BackgroundServices/SignalRuleProcessorService.cs
  • IntelliTrader.Infrastructure/BackgroundServices/TimedBackgroundService.cs
  • IntelliTrader.Infrastructure/BackgroundServices/TradingRuleProcessorService.cs
  • IntelliTrader.Infrastructure/Dispatching/InMemoryCommandDispatcher.cs
  • IntelliTrader.Infrastructure/Dispatching/InMemoryQueryDispatcher.cs
  • IntelliTrader.Infrastructure/Events/InMemoryDomainEventDispatcher.cs
  • IntelliTrader.Infrastructure/Migration/LegacyAccountMigrator.cs
  • IntelliTrader.Trading/IntelliTrader.Trading.csproj
  • IntelliTrader.Trading/Services/TradingService.cs
  • IntelliTrader.Web/BackgroundServices/SignalRBroadcasterService.cs
  • IntelliTrader.Web/Controllers/HomeController.cs
  • IntelliTrader.Web/Hubs/TradingHub.cs
  • tests/IntelliTrader.E2E.Tests/ApiAuthenticationTests.cs
  • tests/IntelliTrader.E2E.Tests/HealthEndpointTests.cs
  • tests/IntelliTrader.E2E.Tests/RateLimitingTests.cs
  • tests/IntelliTrader.Performance.Tests/Benchmarks/TradingBenchmarks.cs
  • tests/IntelliTrader.Performance.Tests/TradingPerformanceTests.cs
  • tests/IntelliTrader.Web.Tests/HomeControllerTests.cs
📝 Walkthrough

Walkthrough

This pull request completes the async/await migration across the codebase by adding .ConfigureAwait(false) to library code to prevent synchronization context capture and converting remaining synchronous method calls to their asynchronous equivalents in adapters, controllers, and web services.

Changes

Cohort / File(s) Summary
ConfigureAwait(false) - Core & Exchange Services
IntelliTrader.Core/Services/CoreService.cs, IntelliTrader.Exchange.Base/Services/SecretRotationService.cs, IntelliTrader.Exchange.Binance/Services/BinanceExchangeService.cs, IntelliTrader.Exchange.Binance/Services/BinanceWebSocketService.cs
Added ConfigureAwait(false) to awaited async operations throughout core startup, secret rotation, and Binance exchange/WebSocket services to avoid capturing synchronization context in library code.
ConfigureAwait(false) - Infrastructure Adapters & Repositories
IntelliTrader.Infrastructure/Adapters/Exchange/BinanceExchangeAdapter.cs, IntelliTrader.Infrastructure/Adapters/Persistence/Json/JsonPositionRepository.cs, IntelliTrader.Infrastructure/Migration/LegacyAccountMigrator.cs
Added ConfigureAwait(false) to awaited calls in adapter wrappers, repository file I/O, and migration operations.
ConfigureAwait(false) - Background Services
IntelliTrader.Infrastructure/BackgroundServices/OrderExecutionService.cs, IntelliTrader.Infrastructure/BackgroundServices/SignalRuleProcessorService.cs, IntelliTrader.Infrastructure/BackgroundServices/TimedBackgroundService.cs, IntelliTrader.Infrastructure/BackgroundServices/TradingRuleProcessorService.cs
Added ConfigureAwait(false) to awaited repository reads/writes, exchange operations, and inter-task delays in background job processing.
ConfigureAwait(false) - Dispatchers & Event Handlers
IntelliTrader.Infrastructure/Dispatching/InMemoryCommandDispatcher.cs, IntelliTrader.Infrastructure/Dispatching/InMemoryQueryDispatcher.cs, IntelliTrader.Infrastructure/Events/InMemoryDomainEventDispatcher.cs
Added ConfigureAwait(false) to awaited handler invocations in in-memory dispatcher and event distribution paths.
Async Method Conversion - Adapter
IntelliTrader.Infrastructure/Adapters/Legacy/LegacyTradingServiceAdapter.cs
Converted PlaceBuyOrderAsync, PlaceSellOrderAsync, and PlaceSwapOrderAsync from thin sync-wrapping tasks to truly async methods that await legacy async operations (BuyAsync, SellAsync, SwapAsync). Added ConfigureAwait(false) to GetTradingPairsAsync.
Sync-to-Async Web Service Calls
IntelliTrader.Web/BackgroundServices/SignalRBroadcasterService.cs, IntelliTrader.Web/Hubs/TradingHub.cs
Updated BroadcastTradingPairsAsync() and SendPairPriceAsync() to fetch current prices via GetCurrentPriceAsync(...) instead of synchronous calls; both await with ConfigureAwait(false).
Async Controller Methods
IntelliTrader.Web/Controllers/HomeController.cs
Converted Sell, Buy, BuyDefault, and Swap trading endpoints from synchronous to asynchronous Task<IActionResult> methods; each now awaits the corresponding Async service method and returns Ok() after completion.
Core Service Event Dispatch
IntelliTrader.Trading/Services/TradingService.cs
Added ConfigureAwait(false) to domain event dispatch in RaiseEventSafe.
Test Async Updates
tests/IntelliTrader.Web.Tests/HomeControllerTests.cs
Converted seven trading action test methods (Buy, Buy_WithInvalidModel, Sell, Sell_WithInvalidModel, Swap, Swap_WithInvalidModel, BuyDefault) from synchronous to async; updated Moq verifications to expect async trading service calls with CancellationToken parameter.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A whisper of context, now gently released—
ConfigureAwait flows from west to the east,
Async all the way, no sync left to block,
Controllers now wait, and adapters now rock! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.86% 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 PR title 'refactor: complete async/await migration' directly matches the primary change objective of fully migrating the codebase to async/await patterns.
Linked Issues check ✅ Passed The PR successfully addresses all acceptance criteria: sync methods removed from ITradingService usage, all callers updated to async versions, ConfigureAwait(false) applied across 20+ library files, and deadlock-free testing verified (1,588 tests pass).
Out of Scope Changes check ✅ Passed All changes are scoped to the async/await migration objective. Out-of-scope items (sync wrappers in lock blocks, HighResolutionTimedTask pattern, GetAwaiter().GetResult()) were explicitly preserved as noted in PR description.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/issue-21-async-migration

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
IntelliTrader.Exchange.Binance/Services/BinanceWebSocketService.cs (1)

162-167: ⚠️ Potential issue | 🟠 Major

Handle cancellation separately in connect/reconnect failure paths.

Lines 162-167 and 322-326 catch all exceptions, so cancellation can incorrectly trigger REST fallback / reconnect-failure logic during intentional shutdown.

💡 Proposed fix
 private async Task ConnectInternalAsync(CancellationToken cancellationToken)
 {
     ...
     try
     {
         ...
     }
+    catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+    {
+        ConnectionState = WebSocketConnectionState.Disconnected;
+        _loggingService.Debug("WebSocket connect canceled");
+        throw;
+    }
     catch (Exception ex)
     {
         _loggingService.Error("Failed to connect to Binance WebSocket", ex);
         ConnectionState = WebSocketConnectionState.Disconnected;
         await StartRestFallbackAsync(cancellationToken).ConfigureAwait(false);
         throw;
     }
 }

 public async Task ReconnectAsync(CancellationToken cancellationToken = default)
 {
     ...
     try
     {
         ...
     }
+    catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+    {
+        _loggingService.Debug("WebSocket reconnect canceled");
+    }
     catch (Exception ex)
     {
         _loggingService.Error("Failed to reconnect to Binance WebSocket", ex);
         await HandleReconnectFailureAsync(cancellationToken).ConfigureAwait(false);
     }
     ...
 }

Also applies to: 322-326

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

In `@IntelliTrader.Exchange.Binance/Services/BinanceWebSocketService.cs` around
lines 162 - 167, The catch-all exception handlers around connection logic (the
catch(Exception ex) that logs via _loggingService.Error, sets ConnectionState,
calls StartRestFallbackAsync, and the similar catch block near Reconnect logic)
must not treat operation cancellation as an error; update those handlers to
rethrow immediately when cancellation occurred by checking for
OperationCanceledException or cancellationToken.IsCancellationRequested (e.g.,
if (ex is OperationCanceledException ||
cancellationToken.IsCancellationRequested) throw;), and only perform the
logging, set ConnectionState = WebSocketConnectionState.Disconnected, and call
StartRestFallbackAsync or other reconnect/failure logic for non-cancellation
exceptions in the methods containing this catch, so intentional shutdown does
not trigger fallback/reconnect behavior.
IntelliTrader.Infrastructure/Adapters/Exchange/BinanceExchangeAdapter.cs (1)

33-37: ⚠️ Potential issue | 🟠 Major

CancellationToken is accepted but not honored in adapter methods.

At Line 36/77/116/149/180/205/235/273/303/381, the token is part of the signature but not checked or propagated before legacy calls. This weakens cancellation during trading flows.

Suggested patch pattern
 public async Task<Result<ExchangeOrderResult>> PlaceMarketBuyAsync(
     TradingPair pair,
     Money cost,
     CancellationToken cancellationToken = default)
 {
+    cancellationToken.ThrowIfCancellationRequested();
     ArgumentNullException.ThrowIfNull(pair);
     ArgumentNullException.ThrowIfNull(cost);

     try
     {
+        cancellationToken.ThrowIfCancellationRequested();
         var currentPrice = await _legacyExchange.GetLastPrice(pair.Symbol).ConfigureAwait(false);

Apply the same pre-check pattern in the other adapter methods that accept CancellationToken.

Also applies to: 74-78, 112-117, 145-150, 178-181, 203-206, 233-236, 272-274, 300-304, 381-381

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

In `@IntelliTrader.Infrastructure/Adapters/Exchange/BinanceExchangeAdapter.cs`
around lines 33 - 37, The adapter methods (e.g., PlaceMarketBuyAsync) accept a
CancellationToken but never honor it; update each method that takes
cancellationToken to (1) call cancellationToken.ThrowIfCancellationRequested()
at the start and before any long-running or legacy blocking call, and (2)
propagate the token into any awaited or legacy API calls that accept a
CancellationToken (or wrap calls with Task.Run/WithCancellation where supported)
so cancellation is respected; apply the same pattern to the other adapter
methods in this file that accept CancellationToken.
🧹 Nitpick comments (1)
tests/IntelliTrader.Web.Tests/HomeControllerTests.cs (1)

346-350: Assert the exact cancellation token instead of It.IsAny<CancellationToken>().

Current verifications confirm an async call happened, but not that the controller propagated request cancellation.

💡 Example tightening (apply similarly to Sell/Swap/BuyDefault)
+var cts = new CancellationTokenSource();
+_sut.ControllerContext.HttpContext.RequestAborted = cts.Token;

 var result = await _sut.Buy(model);

 _tradingServiceMock.Verify(x => x.BuyAsync(It.Is<BuyOptions>(o =>
     o.Pair == "BTCUSDT" &&
     o.Amount == 0.01m &&
     o.ManualOrder == true &&
-    o.IgnoreExisting == true), It.IsAny<CancellationToken>()), Times.Once);
+    o.IgnoreExisting == true), It.Is<CancellationToken>(ct => ct == cts.Token)), Times.Once);

Also applies to: 378-381, 409-412, 443-446

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

In `@tests/IntelliTrader.Web.Tests/HomeControllerTests.cs` around lines 346 - 350,
The Verify call for _tradingServiceMock currently uses
It.IsAny<CancellationToken>() so the test doesn't assert the controller
propagated the incoming CancellationToken; update the Verify for BuyAsync (and
similarly for SellAsync, SwapAsync, BuyDefaultAsync at the other locations) to
match the exact token passed by the controller test (use the same
CancellationToken instance/variable used when invoking the controller) instead
of It.IsAny<CancellationToken>(), keeping the same predicate on BuyOptions
(o.Pair, o.Amount, o.ManualOrder, o.IgnoreExisting) and Times.Once to ensure the
controller forwards the exact cancellation token.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@IntelliTrader.Core/Services/CoreService.cs`:
- Around line 91-95: The delayed startup is fire-and-forget and can start tasks
after shutdown; wrap the Task.Run delay with the service lifecycle
CancellationToken (the same token used by Stop()/Restart()) by passing it into
Task.Run and into Task.Delay(Constants.Timeouts.StartupDelayMs, lifecycleToken),
then check lifecycleToken.IsCancellationRequested (or call
lifecycleToken.ThrowIfCancellationRequested()) before calling StartAllTasks();
reference the StartAllTasks method and ensure the cancellation token used by
Stop()/Restart() is the one supplied to this delayed task so the startup is
skipped if the service is stopping.

In `@IntelliTrader.Infrastructure/Adapters/Legacy/LegacyTradingServiceAdapter.cs`:
- Around line 93-98: The current catch-all Exception handlers in
PlaceBuyOrderAsync, PlaceSellOrderAsync, and PlaceSwapOrderAsync swallow
OperationCanceledException and turn cancellations into exchange errors; change
the catch ordering so you first catch OperationCanceledException and rethrow (or
detect and rethrow) before the generic catch, then keep the existing
_logger.LogError(...) and Result<...>.Failure(...) behavior in the generic catch
block to preserve cancellation semantics while still logging other exceptions.

In `@IntelliTrader.Web/Controllers/HomeController.cs`:
- Around line 552-564: The Sell controller action is not propagating the HTTP
cancellation token to the service call; update the call to
_tradingService.SellAsync(...) to pass HttpContext.RequestAborted (i.e., call
SellAsync(new SellOptions(model.Pair){...}, HttpContext.RequestAborted)) so the
operation cancels if the client disconnects, and make the same change for the
other controller endpoints that call SellAsync/related async service methods
(the other occurrences noted in the review) ensuring the service methods accept
a CancellationToken parameter if they don't already.

---

Outside diff comments:
In `@IntelliTrader.Exchange.Binance/Services/BinanceWebSocketService.cs`:
- Around line 162-167: The catch-all exception handlers around connection logic
(the catch(Exception ex) that logs via _loggingService.Error, sets
ConnectionState, calls StartRestFallbackAsync, and the similar catch block near
Reconnect logic) must not treat operation cancellation as an error; update those
handlers to rethrow immediately when cancellation occurred by checking for
OperationCanceledException or cancellationToken.IsCancellationRequested (e.g.,
if (ex is OperationCanceledException ||
cancellationToken.IsCancellationRequested) throw;), and only perform the
logging, set ConnectionState = WebSocketConnectionState.Disconnected, and call
StartRestFallbackAsync or other reconnect/failure logic for non-cancellation
exceptions in the methods containing this catch, so intentional shutdown does
not trigger fallback/reconnect behavior.

In `@IntelliTrader.Infrastructure/Adapters/Exchange/BinanceExchangeAdapter.cs`:
- Around line 33-37: The adapter methods (e.g., PlaceMarketBuyAsync) accept a
CancellationToken but never honor it; update each method that takes
cancellationToken to (1) call cancellationToken.ThrowIfCancellationRequested()
at the start and before any long-running or legacy blocking call, and (2)
propagate the token into any awaited or legacy API calls that accept a
CancellationToken (or wrap calls with Task.Run/WithCancellation where supported)
so cancellation is respected; apply the same pattern to the other adapter
methods in this file that accept CancellationToken.

---

Nitpick comments:
In `@tests/IntelliTrader.Web.Tests/HomeControllerTests.cs`:
- Around line 346-350: The Verify call for _tradingServiceMock currently uses
It.IsAny<CancellationToken>() so the test doesn't assert the controller
propagated the incoming CancellationToken; update the Verify for BuyAsync (and
similarly for SellAsync, SwapAsync, BuyDefaultAsync at the other locations) to
match the exact token passed by the controller test (use the same
CancellationToken instance/variable used when invoking the controller) instead
of It.IsAny<CancellationToken>(), keeping the same predicate on BuyOptions
(o.Pair, o.Amount, o.ManualOrder, o.IgnoreExisting) and Times.Once to ensure the
controller forwards the exact cancellation token.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 00124e82-2afb-4892-834e-6b978d2b87f1

📥 Commits

Reviewing files that changed from the base of the PR and between 2cb1fcc and 41b1abb.

📒 Files selected for processing (20)
  • IntelliTrader.Core/Services/CoreService.cs
  • IntelliTrader.Exchange.Base/Services/SecretRotationService.cs
  • IntelliTrader.Exchange.Binance/Services/BinanceExchangeService.cs
  • IntelliTrader.Exchange.Binance/Services/BinanceWebSocketService.cs
  • IntelliTrader.Infrastructure/Adapters/Exchange/BinanceExchangeAdapter.cs
  • IntelliTrader.Infrastructure/Adapters/Legacy/LegacyTradingServiceAdapter.cs
  • IntelliTrader.Infrastructure/Adapters/Persistence/Json/JsonPositionRepository.cs
  • IntelliTrader.Infrastructure/BackgroundServices/OrderExecutionService.cs
  • IntelliTrader.Infrastructure/BackgroundServices/SignalRuleProcessorService.cs
  • IntelliTrader.Infrastructure/BackgroundServices/TimedBackgroundService.cs
  • IntelliTrader.Infrastructure/BackgroundServices/TradingRuleProcessorService.cs
  • IntelliTrader.Infrastructure/Dispatching/InMemoryCommandDispatcher.cs
  • IntelliTrader.Infrastructure/Dispatching/InMemoryQueryDispatcher.cs
  • IntelliTrader.Infrastructure/Events/InMemoryDomainEventDispatcher.cs
  • IntelliTrader.Infrastructure/Migration/LegacyAccountMigrator.cs
  • IntelliTrader.Trading/Services/TradingService.cs
  • IntelliTrader.Web/BackgroundServices/SignalRBroadcasterService.cs
  • IntelliTrader.Web/Controllers/HomeController.cs
  • IntelliTrader.Web/Hubs/TradingHub.cs
  • tests/IntelliTrader.Web.Tests/HomeControllerTests.cs

Comment on lines 91 to 95
_ = Task.Run(async () =>
{
await Task.Delay(Constants.Timeouts.StartupDelayMs);
await Task.Delay(Constants.Timeouts.StartupDelayMs).ConfigureAwait(false);
StartAllTasks();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard delayed startup with a lifecycle cancellation token.

Line 93 schedules delayed StartAllTasks() as fire-and-forget; if Stop()/Restart() runs before the delay completes, tasks can be started after shutdown.

💡 Proposed fix
+private CancellationTokenSource? _startupDelayCts;

 public void Start()
 {
+    _startupDelayCts?.Cancel();
+    _startupDelayCts?.Dispose();
+    _startupDelayCts = new CancellationTokenSource();
+    var startupToken = _startupDelayCts.Token;
+
     _ = Task.Run(async () =>
     {
-        await Task.Delay(Constants.Timeouts.StartupDelayMs).ConfigureAwait(false);
-        StartAllTasks();
-    });
+        try
+        {
+            await Task.Delay(Constants.Timeouts.StartupDelayMs, startupToken).ConfigureAwait(false);
+            if (!startupToken.IsCancellationRequested && Running)
+            {
+                StartAllTasks();
+            }
+        }
+        catch (OperationCanceledException) when (startupToken.IsCancellationRequested) { }
+    }, startupToken);

     Running = true;
 }

 public void Stop()
 {
+    _startupDelayCts?.Cancel();
     Running = false;
     ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@IntelliTrader.Core/Services/CoreService.cs` around lines 91 - 95, The delayed
startup is fire-and-forget and can start tasks after shutdown; wrap the Task.Run
delay with the service lifecycle CancellationToken (the same token used by
Stop()/Restart()) by passing it into Task.Run and into
Task.Delay(Constants.Timeouts.StartupDelayMs, lifecycleToken), then check
lifecycleToken.IsCancellationRequested (or call
lifecycleToken.ThrowIfCancellationRequested()) before calling StartAllTasks();
reference the StartAllTasks method and ensure the cancellation token used by
Stop()/Restart() is the one supplied to this delayed task so the startup is
skipped if the service is stopping.

Comment on lines 93 to 98
catch (Exception ex)
{
_logger.LogError(ex, "Error placing buy order for {Pair}", pair);
return Task.FromResult(Result<PlaceBuyOrderResult>.Failure(
Error.ExchangeError($"Failed to place buy order: {ex.Message}")));
return Result<PlaceBuyOrderResult>.Failure(
Error.ExchangeError($"Failed to place buy order: {ex.Message}"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve cancellation semantics in async order placement methods.

catch (Exception) on Lines 93, 156, and 214 also catches OperationCanceledException, turning cancellations into exchange errors.

💡 Proposed fix
     try
     {
         await _tradingService.BuyAsync(options, cancellationToken).ConfigureAwait(false);
         ...
     }
+    catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+    {
+        throw;
+    }
     catch (Exception ex)
     {
         _logger.LogError(ex, "Error placing buy order for {Pair}", pair);
         return Result<PlaceBuyOrderResult>.Failure(
             Error.ExchangeError($"Failed to place buy order: {ex.Message}"));
     }

Apply the same catch ordering to PlaceSellOrderAsync and PlaceSwapOrderAsync.

Also applies to: 156-161, 214-218

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

In `@IntelliTrader.Infrastructure/Adapters/Legacy/LegacyTradingServiceAdapter.cs`
around lines 93 - 98, The current catch-all Exception handlers in
PlaceBuyOrderAsync, PlaceSellOrderAsync, and PlaceSwapOrderAsync swallow
OperationCanceledException and turn cancellations into exchange errors; change
the catch ordering so you first catch OperationCanceledException and rethrow (or
detect and rethrow) before the generic catch, then keep the existing
_logger.LogError(...) and Result<...>.Failure(...) behavior in the generic catch
block to preserve cancellation semantics while still logging other exceptions.

Comment on lines +552 to 564
public async Task<IActionResult> Sell([FromForm] SellInputModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}

_tradingService.Sell(new SellOptions(model.Pair)
await _tradingService.SellAsync(new SellOptions(model.Pair)
{
Amount = model.Amount,
ManualOrder = true
});
}).ConfigureAwait(false);
return Ok();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Propagate request cancellation token from controller endpoints.

At Line 559, Line 578, Line 598, and Line 622, async service calls omit HttpContext.RequestAborted. If the client disconnects, these operations may continue unnecessarily.

Suggested patch
-            await _tradingService.SellAsync(new SellOptions(model.Pair)
+            await _tradingService.SellAsync(new SellOptions(model.Pair)
             {
                 Amount = model.Amount,
                 ManualOrder = true
-            }).ConfigureAwait(false);
+            }, HttpContext.RequestAborted).ConfigureAwait(false);

-            await _tradingService.BuyAsync(new BuyOptions(model.Pair)
+            await _tradingService.BuyAsync(new BuyOptions(model.Pair)
             {
                 Amount = model.Amount,
                 IgnoreExisting = true,
                 ManualOrder = true
-            }).ConfigureAwait(false);
+            }, HttpContext.RequestAborted).ConfigureAwait(false);

-            await _tradingService.BuyAsync(new BuyOptions(model.Pair)
+            await _tradingService.BuyAsync(new BuyOptions(model.Pair)
             {
                 MaxCost = _tradingService.GetPairConfig(model.Pair).BuyMaxCost,
                 IgnoreExisting = true,
                 ManualOrder = true,
                 Metadata = new OrderMetadata
                 {
                     BoughtGlobalRating = _signalsService.GetGlobalRating()
                 }
-            }).ConfigureAwait(false);
+            }, HttpContext.RequestAborted).ConfigureAwait(false);

-            await _tradingService.SwapAsync(new SwapOptions(model.Pair, model.Swap, new OrderMetadata())
+            await _tradingService.SwapAsync(new SwapOptions(model.Pair, model.Swap, new OrderMetadata())
             {
                 ManualOrder = true
-            }).ConfigureAwait(false);
+            }, HttpContext.RequestAborted).ConfigureAwait(false);

Also applies to: 571-584, 591-608, 615-626

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

In `@IntelliTrader.Web/Controllers/HomeController.cs` around lines 552 - 564, The
Sell controller action is not propagating the HTTP cancellation token to the
service call; update the call to _tradingService.SellAsync(...) to pass
HttpContext.RequestAborted (i.e., call SellAsync(new
SellOptions(model.Pair){...}, HttpContext.RequestAborted)) so the operation
cancels if the client disconnects, and make the same change for the other
controller endpoints that call SellAsync/related async service methods (the
other occurrences noted in the review) ensuring the service methods accept a
CancellationToken parameter if they don't already.

@codecov

codecov Bot commented Apr 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 41.02564% with 92 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...change.Binance/Services/BinanceWebSocketService.cs 0.00% 33 Missing ⚠️
...ure/Adapters/Legacy/LegacyTradingServiceAdapter.cs 0.00% 16 Missing ⚠️
...ucture/BackgroundServices/OrderExecutionService.cs 34.78% 15 Missing ⚠️
...er.Exchange.Base/Services/SecretRotationService.cs 20.00% 8 Missing ⚠️
...xchange.Binance/Services/BinanceExchangeService.cs 0.00% 7 Missing ⚠️
...e/BackgroundServices/SignalRuleProcessorService.cs 40.00% 3 Missing ⚠️
...astructure/Events/InMemoryDomainEventDispatcher.cs 0.00% 3 Missing ⚠️
...structure/Dispatching/InMemoryCommandDispatcher.cs 0.00% 2 Missing ⚠️
IntelliTrader.Core/Services/CoreService.cs 0.00% 1 Missing ⚠️
...rastructure/Dispatching/InMemoryQueryDispatcher.cs 0.00% 1 Missing ⚠️
... and 3 more
Files with missing lines Coverage Δ
...ucture/Adapters/Exchange/BinanceExchangeAdapter.cs 85.77% <100.00%> (ø)
...dapters/Persistence/Json/JsonPositionRepository.cs 96.00% <100.00%> (ø)
...cture/BackgroundServices/TimedBackgroundService.cs 92.68% <100.00%> (ø)
.../BackgroundServices/TradingRuleProcessorService.cs 78.63% <100.00%> (ø)
....Infrastructure/Migration/LegacyAccountMigrator.cs 84.25% <100.00%> (ø)
IntelliTrader.Web/Controllers/HomeController.cs 49.39% <100.00%> (ø)
IntelliTrader.Core/Services/CoreService.cs 31.03% <0.00%> (ø)
...rastructure/Dispatching/InMemoryQueryDispatcher.cs 0.00% <0.00%> (ø)
IntelliTrader.Trading/Services/TradingService.cs 47.61% <0.00%> (ø)
...eb/BackgroundServices/SignalRBroadcasterService.cs 15.11% <0.00%> (ø)
... and 9 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

blackms and others added 2 commits April 12, 2026 11:49
- Web controllers (Buy, Sell, Swap, BuyDefault): migrated from sync to
  async, now call BuyAsync/SellAsync/SwapAsync instead of sync wrappers
- Web services (SignalRBroadcasterService, TradingHub): migrated
  GetCurrentPrice to GetCurrentPriceAsync
- LegacyTradingServiceAdapter: migrated PlaceBuyOrderAsync,
  PlaceSellOrderAsync, PlaceSwapOrderAsync from sync wrappers to proper
  async calls with CancellationToken propagation
- Added ConfigureAwait(false) to all await calls in library projects:
  CoreService, NotificationService, BinanceExchangeService,
  BinanceWebSocketService, SecretRotationService, TradingService,
  OrderExecutionService, SignalRuleProcessorService,
  TradingRuleProcessorService, TimedBackgroundService,
  BinanceExchangeAdapter, JsonPositionRepository,
  InMemoryCommandDispatcher, InMemoryQueryDispatcher,
  InMemoryDomainEventDispatcher, LegacyAccountMigrator
- Updated web controller tests to verify async method calls

Sync wrapper methods (GetTickers, GetCurrentPrice, PlaceOrder, etc.)
are preserved for callers in lock blocks (TradingAccountBase,
ExchangeAccount) and HighResolutionTimedTask - those are scope for
issue #22.

Closes #21

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add InternalsVisibleTo for IntelliTrader.Performance.Tests to fix
  CS0122 build error (TradingService inaccessible)
- Inject default TradingConfig via reflection in performance tests to
  fix ArgumentNullException for 'configuration' parameter

These issues pre-date this PR but were masked by the build error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@blackms blackms force-pushed the refactor/issue-21-async-migration branch from 76290e6 to f575d56 Compare April 12, 2026 09:50
…ntainer

Add [Collection("E2E")] to all E2E test classes to prevent xUnit from
running them in parallel. The tests share a static Startup.Container
reference which causes race conditions when multiple TestWebHostFactory
instances are created concurrently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@blackms blackms merged commit cfc61a2 into main Apr 12, 2026
3 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.

Complete Async/Await Migration

1 participant