Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concurrency issues with EF Core #582

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ public partial class QuickGrid<TGridItem> : IAsyncDisposable
/// </summary>
[Parameter] public PaginationState? Pagination { get; set; }

/// <summary>
/// An optional delegate that gets called when the grid needs to refresh.
/// A typical use case may be to update items provider with updated sort orders.
/// </summary>
[Parameter] public Action? OnRefresh { get; set; }

[Inject] private IServiceProvider Services { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;

Expand Down Expand Up @@ -134,6 +140,7 @@ public partial class QuickGrid<TGridItem> : IAsyncDisposable
private int? _lastRefreshedPaginationStateHash;
private object? _lastAssignedItemsOrProvider;
private CancellationTokenSource? _pendingDataLoadCancellationTokenSource;
private SemaphoreSlim _pendingDataLoadSemaphore = new SemaphoreSlim(1);

// If the PaginationState mutates, it raises this event. We use it to trigger a re-render.
private readonly EventCallbackSubscriber<PaginationState> _currentPageItemsChanged;
Expand Down Expand Up @@ -276,6 +283,8 @@ public async Task RefreshDataAsync()
// because in that case there's going to be a re-render anyway.
private async Task RefreshDataCoreAsync()
{
OnRefresh?.Invoke();

// Move into a "loading" state, cancelling any earlier-but-still-pending load
_pendingDataLoadCancellationTokenSource?.Cancel();
var thisLoadCts = _pendingDataLoadCancellationTokenSource = new CancellationTokenSource();
Expand Down Expand Up @@ -306,15 +315,28 @@ private async Task RefreshDataCoreAsync()
}
}

[Parameter] public int MaxDebounceMs { get; set; } = 100;
private DateTime lastBounced; // Default value is BOC
private async Task SmartConfigurableDebounce()
{
var now = DateTime.UtcNow;
var delay = lastBounced.AddMilliseconds(MaxDebounceMs) - now;
lastBounced = now;
if (delay > TimeSpan.Zero)
{
// Task.Delay() does not support negative delays!
await Task.Delay(delay);
}
}

// Gets called both by RefreshDataCoreAsync and directly by the Virtualize child component during scrolling
private async ValueTask<ItemsProviderResult<(int, TGridItem)>> ProvideVirtualizedItems(ItemsProviderRequest request)
{
_lastRefreshedPaginationStateHash = Pagination?.GetHashCode();

// Debounce the requests. This eliminates a lot of redundant queries at the cost of slight lag after interactions.
// TODO: Consider making this configurable, or smarter (e.g., doesn't delay on first call in a batch, then the amount
// of delay increases if you rapidly issue repeated requests, such as when scrolling a long way)
await Task.Delay(100);
await SmartConfigurableDebounce();

if (request.CancellationToken.IsCancellationRequested)
{
return default;
Expand Down Expand Up @@ -364,14 +386,25 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
}
else if (Items is not null)
{
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items);
var result = request.ApplySorting(Items).Skip(request.StartIndex);
if (request.Count.HasValue)
// EF Core does not support Multiple Concurrent Data Readers.
// This may happen with virtualized grids of larger datasets.
// Let's wait if that happens.
await _pendingDataLoadSemaphore.WaitAsync().ConfigureAwait(false);
try
{
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items);
var result = request.ApplySorting(Items).Skip(request.StartIndex);
if (request.Count.HasValue)
{
result = result.Take(request.Count.Value);
}
var resultArray = _asyncQueryExecutor is null ? result.ToArray() : await _asyncQueryExecutor.ToArrayAsync(result);
return GridItemsProviderResult.From(resultArray, totalItemCount);
}
finally
{
result = result.Take(request.Count.Value);
_pendingDataLoadSemaphore.Release();
}
var resultArray = _asyncQueryExecutor is null ? result.ToArray() : await _asyncQueryExecutor.ToArrayAsync(result);
return GridItemsProviderResult.From(resultArray, totalItemCount);
}
else
{
Expand Down