[Blazor][Fixes #11964] Limit the amount of pending renders * Adds a default limit of 10 queued pending renders per application. * Stops producing new render batches after that limit is hit. * Resumes producing render batches as soon as the client acknowledges a batch.
This commit is contained in:
parent
43350b57b9
commit
31cfa2e305
|
|
@ -535,13 +535,13 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
public Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
|
||||
public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; }
|
||||
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
|
||||
protected internal virtual void AddToRenderQueue(int componentId, Microsoft.AspNetCore.Components.RenderFragment renderFragment) { }
|
||||
protected internal int AssignRootComponentId(Microsoft.AspNetCore.Components.IComponent component) { throw null; }
|
||||
public virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; }
|
||||
public void Dispose() { }
|
||||
protected virtual void Dispose(bool disposing) { }
|
||||
protected abstract void HandleException(System.Exception exception);
|
||||
protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; }
|
||||
protected virtual void ProcessPendingRender() { }
|
||||
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId) { throw null; }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; }
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
}
|
||||
|
||||
internal static void DisposingComponent(ILogger<Renderer> logger, ComponentState componentState)
|
||||
public static void DisposingComponent(ILogger<Renderer> logger, ComponentState componentState)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Debug)) // This is almost always false, so skip the evaluations
|
||||
{
|
||||
|
|
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
}
|
||||
|
||||
internal static void HandlingEvent(ILogger<Renderer> logger, ulong eventHandlerId, EventArgs eventArgs)
|
||||
public static void HandlingEvent(ILogger<Renderer> logger, ulong eventHandlerId, EventArgs eventArgs)
|
||||
{
|
||||
_handlingEvent(logger, eventHandlerId, eventArgs?.GetType().Name ?? "null", null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
// Since the task has yielded - process any queued rendering work before we return control
|
||||
// to the caller.
|
||||
ProcessRenderQueue();
|
||||
ProcessPendingRender();
|
||||
}
|
||||
|
||||
// Task completed synchronously or is still running. We already processed all of the rendering
|
||||
|
|
@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// </summary>
|
||||
/// <param name="componentId">The ID of the component to render.</param>
|
||||
/// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
|
||||
protected internal virtual void AddToRenderQueue(int componentId, RenderFragment renderFragment)
|
||||
internal void AddToRenderQueue(int componentId, RenderFragment renderFragment)
|
||||
{
|
||||
EnsureSynchronizationContext();
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
if (!_isBatchInProgress)
|
||||
{
|
||||
ProcessRenderQueue();
|
||||
ProcessPendingRender();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -398,13 +398,27 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
? componentState
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Processses pending renders requests from components if there are any.
|
||||
/// </summary>
|
||||
protected virtual void ProcessPendingRender()
|
||||
{
|
||||
ProcessRenderQueue();
|
||||
}
|
||||
|
||||
private void ProcessRenderQueue()
|
||||
{
|
||||
EnsureSynchronizationContext();
|
||||
_isBatchInProgress = true;
|
||||
var updateDisplayTask = Task.CompletedTask;
|
||||
|
||||
try
|
||||
{
|
||||
if (_batchBuilder.ComponentRenderQueue.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Process render queue until empty
|
||||
while (_batchBuilder.ComponentRenderQueue.Count > 0)
|
||||
{
|
||||
|
|
@ -423,6 +437,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
{
|
||||
// Ensure we catch errors while running the render functions of the components.
|
||||
HandleException(e);
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public int DisconnectedCircuitMaxRetained { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public System.TimeSpan DisconnectedCircuitRetentionPeriod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public System.TimeSpan JSInteropDefaultCallTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public int MaxBufferedUnacknowledgedRenderBatches { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
}
|
||||
}
|
||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||
|
|
|
|||
|
|
@ -64,5 +64,17 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
/// Defaults to <c>1 minute</c>.
|
||||
/// </value>
|
||||
public TimeSpan JSInteropDefaultCallTimeout { get; set; } = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of render batches that a circuit will buffer until an acknowledgement for the batch is
|
||||
/// received.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the limit of buffered render batches is reached components will stop rendering and will wait until either the
|
||||
/// circuit is disconnected and disposed or at least one batch gets acknowledged.
|
||||
/// </remarks>
|
||||
/// <value>
|
||||
/// Defaults to <c>10</c>.</value>
|
||||
public int MaxBufferedUnacknowledgedRenderBatches { get; set; } = 10;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
catch (Exception ex)
|
||||
{
|
||||
// We don't expect any of this code to actually throw, because DotNetDispatcher.BeginInvoke doesn't throw
|
||||
// however, we still want this to get logged if we do.
|
||||
// however, we still want this to get logged if we do.
|
||||
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.Web.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
|
|
@ -13,8 +14,8 @@ using Microsoft.AspNetCore.Http;
|
|||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||
{
|
||||
|
|
@ -24,16 +25,19 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger _logger;
|
||||
private readonly CircuitIdFactory _circuitIdFactory;
|
||||
private readonly CircuitOptions _options;
|
||||
|
||||
public DefaultCircuitFactory(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILoggerFactory loggerFactory,
|
||||
CircuitIdFactory circuitIdFactory)
|
||||
CircuitIdFactory circuitIdFactory,
|
||||
IOptions<CircuitOptions> options)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = _loggerFactory.CreateLogger<CircuitFactory>();
|
||||
_circuitIdFactory = circuitIdFactory ?? throw new ArgumentNullException(nameof(circuitIdFactory));
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public override CircuitHost CreateCircuitHost(
|
||||
|
|
@ -80,6 +84,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
scope.ServiceProvider,
|
||||
_loggerFactory,
|
||||
rendererRegistry,
|
||||
_options,
|
||||
jsRuntime,
|
||||
client,
|
||||
encoder,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
|
@ -23,8 +23,10 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly CircuitClientProxy _client;
|
||||
private readonly CircuitOptions _options;
|
||||
private readonly RendererRegistry _rendererRegistry;
|
||||
private readonly ILogger _logger;
|
||||
internal readonly ConcurrentQueue<UnacknowledgedRenderBatch> _unacknowledgedRenderBatches = new ConcurrentQueue<UnacknowledgedRenderBatch>();
|
||||
private long _nextRenderId = 1;
|
||||
private bool _disposing = false;
|
||||
|
||||
|
|
@ -40,6 +42,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
IServiceProvider serviceProvider,
|
||||
ILoggerFactory loggerFactory,
|
||||
RendererRegistry rendererRegistry,
|
||||
CircuitOptions options,
|
||||
IJSRuntime jsRuntime,
|
||||
CircuitClientProxy client,
|
||||
HtmlEncoder encoder,
|
||||
|
|
@ -49,13 +52,12 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
_rendererRegistry = rendererRegistry;
|
||||
_jsRuntime = jsRuntime;
|
||||
_client = client;
|
||||
_options = options;
|
||||
|
||||
Id = _rendererRegistry.Add(this);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
internal ConcurrentQueue<UnacknowledgedRenderBatch> UnacknowledgedRenderBatches = new ConcurrentQueue<UnacknowledgedRenderBatch>();
|
||||
|
||||
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
|
||||
|
||||
public int Id { get; }
|
||||
|
|
@ -81,6 +83,34 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
return RenderRootComponentAsync(componentId);
|
||||
}
|
||||
|
||||
protected override void ProcessPendingRender()
|
||||
{
|
||||
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)
|
||||
{
|
||||
// If we got here it means we are at max capacity, so we don't want to actually process the queue,
|
||||
// as we have a client that is not acknowledging render batches fast enough (something we consider needs
|
||||
// to be fast).
|
||||
// The result is something as follows:
|
||||
// Lets imagine an extreme case where the server produces a new batch every milisecond.
|
||||
// Lets say the client is able to ACK a batch every 100 miliseconds.
|
||||
// When the app starts the client might see the sequence 0->(MaxUnacknowledgedRenderBatches-1) and then
|
||||
// after 100 miliseconds it sees it jump to 1xx, then to 2xx where xx is something between {0..99} the
|
||||
// reason for this is that the server slows down rendering new batches to as fast as the client can consume
|
||||
// them.
|
||||
// Similarly, if a client were to send events at a faster pace than the server can consume them, the server
|
||||
// would still proces the events, but would not produce new renders until it gets an ack that frees up space
|
||||
// for a new render.
|
||||
// We should never see UnacknowledgedRenderBatches.Count > _options.MaxBufferedUnacknowledgedRenderBatches
|
||||
|
||||
// But if we do, it's safer to simply disable the rendering in that case too instead of allowing batches to
|
||||
Log.FullUnacknowledgedRenderBatchesQueue(_logger);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
base.ProcessPendingRender();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void HandleException(Exception exception)
|
||||
{
|
||||
|
|
@ -104,7 +134,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
{
|
||||
_disposing = true;
|
||||
_rendererRegistry.TryRemove(Id);
|
||||
while (UnacknowledgedRenderBatches.TryDequeue(out var entry))
|
||||
while (_unacknowledgedRenderBatches.TryDequeue(out var entry))
|
||||
{
|
||||
entry.CompletionSource.TrySetCanceled();
|
||||
entry.Data.Dispose();
|
||||
|
|
@ -146,7 +176,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
// Buffer the rendered batches no matter what. We'll send it down immediately when the client
|
||||
// is connected or right after the client reconnects.
|
||||
|
||||
UnacknowledgedRenderBatches.Enqueue(pendingRender);
|
||||
_unacknowledgedRenderBatches.Enqueue(pendingRender);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -167,7 +197,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
// All the batches are sent in order based on the fact that SignalR
|
||||
// provides ordering for the underlying messages and that the batches
|
||||
// are always in order.
|
||||
return Task.WhenAll(UnacknowledgedRenderBatches.Select(b => WriteBatchBytesAsync(b)));
|
||||
return Task.WhenAll(_unacknowledgedRenderBatches.Select(b => WriteBatchBytesAsync(b)));
|
||||
}
|
||||
|
||||
private async Task WriteBatchBytesAsync(UnacknowledgedRenderBatch pending)
|
||||
|
|
@ -203,12 +233,12 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
// disposed.
|
||||
}
|
||||
|
||||
public void OnRenderCompleted(long incomingBatchId, string errorMessageOrNull)
|
||||
public Task OnRenderCompleted(long incomingBatchId, string errorMessageOrNull)
|
||||
{
|
||||
if (_disposing)
|
||||
{
|
||||
// Disposing so don't do work.
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// When clients send acks we know for sure they received and applied the batch.
|
||||
|
|
@ -234,18 +264,21 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
// synchronizes calls to hub methods. That is, it won't issue more than one call to this method from the same hub
|
||||
// at the same time on different threads.
|
||||
|
||||
if (!UnacknowledgedRenderBatches.TryPeek(out var nextUnacknowledgedBatch) || incomingBatchId < nextUnacknowledgedBatch.BatchId)
|
||||
if (!_unacknowledgedRenderBatches.TryPeek(out var nextUnacknowledgedBatch) || incomingBatchId < nextUnacknowledgedBatch.BatchId)
|
||||
{
|
||||
Log.ReceivedDuplicateBatchAck(_logger, incomingBatchId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else
|
||||
{
|
||||
var lastBatchId = nextUnacknowledgedBatch.BatchId;
|
||||
// Order is important here so that we don't prematurely dequeue the last nextUnacknowledgedBatch
|
||||
while (UnacknowledgedRenderBatches.TryPeek(out nextUnacknowledgedBatch) && nextUnacknowledgedBatch.BatchId <= incomingBatchId)
|
||||
while (_unacknowledgedRenderBatches.TryPeek(out nextUnacknowledgedBatch) && nextUnacknowledgedBatch.BatchId <= incomingBatchId)
|
||||
{
|
||||
lastBatchId = nextUnacknowledgedBatch.BatchId;
|
||||
UnacknowledgedRenderBatches.TryDequeue(out _);
|
||||
// At this point the queue is definitely not full, we have at least emptied one slot, so we allow a further
|
||||
// full queue log entry the next time it fills up.
|
||||
_unacknowledgedRenderBatches.TryDequeue(out _);
|
||||
ProcessPendingBatch(errorMessageOrNull, nextUnacknowledgedBatch);
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +286,16 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
{
|
||||
HandleException(
|
||||
new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'."));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Normally we will not have pending renders, but it might happen that we reached the limit of
|
||||
// available buffered renders and new renders got queued.
|
||||
// Invoke ProcessBufferedRenderRequests so that we might produce any additional batch that is
|
||||
// missing.
|
||||
|
||||
// We return the task in here, but the caller doesn't await it.
|
||||
return Dispatcher.InvokeAsync(() => ProcessPendingRender());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -321,6 +363,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
private static readonly Action<ILogger, long, string, double, Exception> _completingBatchWithError;
|
||||
private static readonly Action<ILogger, long, double, Exception> _completingBatchWithoutError;
|
||||
private static readonly Action<ILogger, long, Exception> _receivedDuplicateBatchAcknowledgement;
|
||||
private static readonly Action<ILogger, Exception> _fullUnacknowledgedRenderBatchesQueue;
|
||||
|
||||
private static class EventIds
|
||||
{
|
||||
|
|
@ -331,6 +374,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
public static readonly EventId CompletingBatchWithError = new EventId(104, "CompletingBatchWithError");
|
||||
public static readonly EventId CompletingBatchWithoutError = new EventId(105, "CompletingBatchWithoutError");
|
||||
public static readonly EventId ReceivedDuplicateBatchAcknowledgement = new EventId(106, "ReceivedDuplicateBatchAcknowledgement");
|
||||
public static readonly EventId FullUnacknowledgedRenderBatchesQueue = new EventId(107, "FullUnacknowledgedRenderBatchesQueue");
|
||||
}
|
||||
|
||||
static Log()
|
||||
|
|
@ -369,6 +413,11 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
LogLevel.Debug,
|
||||
EventIds.ReceivedDuplicateBatchAcknowledgement,
|
||||
"Received a duplicate ACK for batch id '{IncomingBatchId}'.");
|
||||
|
||||
_fullUnacknowledgedRenderBatchesQueue = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
EventIds.FullUnacknowledgedRenderBatchesQueue,
|
||||
"The queue of unacknowledged render batches is full.");
|
||||
}
|
||||
|
||||
public static void SendBatchDataFailed(ILogger logger, Exception exception)
|
||||
|
|
@ -421,10 +470,27 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
null);
|
||||
}
|
||||
|
||||
internal static void ReceivedDuplicateBatchAck(ILogger logger, long incomingBatchId)
|
||||
public static void ReceivedDuplicateBatchAck(ILogger logger, long incomingBatchId)
|
||||
{
|
||||
_receivedDuplicateBatchAcknowledgement(logger, incomingBatchId, null);
|
||||
}
|
||||
|
||||
public static void FullUnacknowledgedRenderBatchesQueue(ILogger logger)
|
||||
{
|
||||
_fullUnacknowledgedRenderBatchesQueue(logger, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct PendingRender
|
||||
{
|
||||
public PendingRender(int componentId, RenderFragment renderFragment)
|
||||
{
|
||||
ComponentId = componentId;
|
||||
RenderFragment = renderFragment;
|
||||
}
|
||||
|
||||
public int ComponentId { get; }
|
||||
public RenderFragment RenderFragment { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
}
|
||||
|
||||
Log.ReceivedConfirmationForBatch(_logger, renderId);
|
||||
CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
|
||||
_ = CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
|
||||
}
|
||||
|
||||
public void OnLocationChanged(string uri, bool intercepted)
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
private class TestRemoteRenderer : RemoteRenderer
|
||||
{
|
||||
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client)
|
||||
: base(serviceProvider, NullLoggerFactory.Instance, rendererRegistry, jsRuntime, new CircuitClientProxy(client, "connection"), HtmlEncoder.Default, NullLogger.Instance)
|
||||
: base(serviceProvider, NullLoggerFactory.Instance, rendererRegistry, new CircuitOptions(), jsRuntime, new CircuitClientProxy(client, "connection"), HtmlEncoder.Default, NullLogger.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ using System.Text.Encodings.Web;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.JSInterop;
|
||||
using Moq;
|
||||
|
|
@ -48,7 +50,82 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
component.TriggerRender();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, renderer.UnacknowledgedRenderBatches.Count);
|
||||
Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotAcknowledgingRenders_ProducesBatches_UpToTheLimit()
|
||||
{
|
||||
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
||||
var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider);
|
||||
var component = new TestComponent(builder =>
|
||||
{
|
||||
builder.OpenElement(0, "my element");
|
||||
builder.AddContent(1, "some text");
|
||||
builder.CloseElement();
|
||||
});
|
||||
|
||||
// Act
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
component.TriggerRender();
|
||||
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoNewBatchesAreCreated_WhenThereAreNoPendingRenderRequestsFromComponents()
|
||||
{
|
||||
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
||||
var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider);
|
||||
var component = new TestComponent(builder =>
|
||||
{
|
||||
builder.OpenElement(0, "my element");
|
||||
builder.AddContent(1, "some text");
|
||||
builder.CloseElement();
|
||||
});
|
||||
|
||||
// Act
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
component.TriggerRender();
|
||||
}
|
||||
|
||||
await renderer.OnRenderCompleted(2, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(9, renderer._unacknowledgedRenderBatches.Count);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ProducesNewBatch_WhenABatchGetsAcknowledged()
|
||||
{
|
||||
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
||||
var renderer = (RemoteRenderer)GetHtmlRenderer(serviceProvider);
|
||||
var i = 0;
|
||||
var component = new TestComponent(builder =>
|
||||
{
|
||||
builder.AddContent(0, $"Value {i}");
|
||||
});
|
||||
|
||||
// Act
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
for (i = 0; i < 20; i++)
|
||||
{
|
||||
component.TriggerRender();
|
||||
}
|
||||
Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count);
|
||||
|
||||
await renderer.OnRenderCompleted(2, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(10, renderer._unacknowledgedRenderBatches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -83,7 +160,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
component.TriggerRender();
|
||||
renderer.OnRenderCompleted(2, null);
|
||||
_ = renderer.OnRenderCompleted(2, null);
|
||||
|
||||
@event.Reset();
|
||||
firstBatchTCS.SetResult(null);
|
||||
|
|
@ -101,7 +178,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
|
||||
foreach (var id in renderIds.ToArray())
|
||||
{
|
||||
renderer.OnRenderCompleted(id, null);
|
||||
_ = renderer.OnRenderCompleted(id, null);
|
||||
}
|
||||
|
||||
secondBatchTCS.SetResult(null);
|
||||
|
|
@ -152,7 +229,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
};
|
||||
// This produces an additional batch (id = 3)
|
||||
trigger.TriggerRender();
|
||||
var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count;
|
||||
var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
|
||||
|
||||
// Act
|
||||
offlineClient.Transfer(onlineClient.Object, "new-connection");
|
||||
|
|
@ -164,14 +241,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
};
|
||||
|
||||
// Receive the ack for the intial batch
|
||||
renderer.OnRenderCompleted(2, null);
|
||||
_ = renderer.OnRenderCompleted(2, null);
|
||||
// Receive the ack for the second batch
|
||||
renderer.OnRenderCompleted(3, null);
|
||||
_ = renderer.OnRenderCompleted(3, null);
|
||||
|
||||
firstBatchTCS.SetResult(null);
|
||||
secondBatchTCS.SetResult(null);
|
||||
// Repeat the ack for the third batch
|
||||
renderer.OnRenderCompleted(3, null);
|
||||
_ = renderer.OnRenderCompleted(3, null);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(exceptions);
|
||||
|
|
@ -215,7 +292,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
};
|
||||
// This produces an additional batch (id = 3)
|
||||
trigger.TriggerRender();
|
||||
var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count;
|
||||
var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
|
||||
|
||||
// Act
|
||||
offlineClient.Transfer(onlineClient.Object, "new-connection");
|
||||
|
|
@ -227,14 +304,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
};
|
||||
|
||||
// Receive the ack for the intial batch
|
||||
renderer.OnRenderCompleted(2, null);
|
||||
_ = renderer.OnRenderCompleted(2, null);
|
||||
// Receive the ack for the second batch
|
||||
renderer.OnRenderCompleted(2, null);
|
||||
_ = renderer.OnRenderCompleted(2, null);
|
||||
|
||||
firstBatchTCS.SetResult(null);
|
||||
secondBatchTCS.SetResult(null);
|
||||
// Repeat the ack for the third batch
|
||||
renderer.OnRenderCompleted(3, null);
|
||||
_ = renderer.OnRenderCompleted(3, null);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(exceptions);
|
||||
|
|
@ -278,7 +355,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
};
|
||||
// This produces an additional batch (id = 3)
|
||||
trigger.TriggerRender();
|
||||
var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count;
|
||||
var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
|
||||
|
||||
// Act
|
||||
var exceptions = new List<Exception>();
|
||||
|
|
@ -288,13 +365,13 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
};
|
||||
|
||||
// Pretend that we missed the ack for the initial batch
|
||||
renderer.OnRenderCompleted(3, null);
|
||||
_ = renderer.OnRenderCompleted(3, null);
|
||||
firstBatchTCS.SetResult(null);
|
||||
secondBatchTCS.SetResult(null);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(exceptions);
|
||||
Assert.Empty(renderer.UnacknowledgedRenderBatches);
|
||||
Assert.Empty(renderer._unacknowledgedRenderBatches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -335,7 +412,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
};
|
||||
// This produces an additional batch (id = 3)
|
||||
trigger.TriggerRender();
|
||||
var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count;
|
||||
var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
|
||||
|
||||
// Act
|
||||
var exceptions = new List<Exception>();
|
||||
|
|
@ -344,7 +421,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
exceptions.Add(e);
|
||||
};
|
||||
|
||||
renderer.OnRenderCompleted(4, null);
|
||||
_ = renderer.OnRenderCompleted(4, null);
|
||||
firstBatchTCS.SetResult(null);
|
||||
secondBatchTCS.SetResult(null);
|
||||
|
||||
|
|
@ -372,7 +449,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
// Assert
|
||||
Assert.Equal(0, first.ComponentId);
|
||||
Assert.Equal(1, second.ComponentId);
|
||||
Assert.Equal(2, renderer.UnacknowledgedRenderBatches.Count);
|
||||
Assert.Equal(2, renderer._unacknowledgedRenderBatches.Count);
|
||||
}
|
||||
|
||||
private RemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClientProxy)
|
||||
|
|
@ -389,6 +466,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
|||
serviceProvider,
|
||||
NullLoggerFactory.Instance,
|
||||
new RendererRegistry(),
|
||||
new CircuitOptions(),
|
||||
jsRuntime.Object,
|
||||
circuitClientProxy,
|
||||
HtmlEncoder.Default,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
serviceScope.ServiceProvider ?? Mock.Of<IServiceProvider>(),
|
||||
NullLoggerFactory.Instance,
|
||||
new RendererRegistry(),
|
||||
new CircuitOptions(),
|
||||
jsRuntime,
|
||||
clientProxy,
|
||||
HtmlEncoder.Default,
|
||||
|
|
|
|||
|
|
@ -507,15 +507,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("event-handler-throw-sync");
|
||||
await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false);
|
||||
|
||||
Assert.Contains(
|
||||
logEvents,
|
||||
e => LogLevel.Warning == e.logLevel &&
|
||||
"UnhandledExceptionInCircuit" == e.eventIdName &&
|
||||
"Handler threw an exception" == e.exception.Message);
|
||||
|
||||
await ValidateClientKeepsWorking(Client, batches);
|
||||
}
|
||||
|
||||
private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, int, byte[])> batches) =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Ignitor;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
public class RemoteRendererBufferLimitTest : IClassFixture<AspNetSiteServerFixture>, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.FromSeconds(60) : TimeSpan.FromMilliseconds(500);
|
||||
|
||||
private AspNetSiteServerFixture _serverFixture;
|
||||
|
||||
public RemoteRendererBufferLimitTest(AspNetSiteServerFixture serverFixture)
|
||||
{
|
||||
serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
|
||||
_serverFixture = serverFixture;
|
||||
|
||||
// Needed here for side-effects
|
||||
_ = _serverFixture.RootUri;
|
||||
|
||||
Client = new BlazorClient() { DefaultLatencyTimeout = DefaultLatencyTimeout };
|
||||
Client.RenderBatchReceived += (rendererId, id, data) => Batches.Add(new Batch(rendererId, id, data));
|
||||
|
||||
Sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
|
||||
Sink.MessageLogged += LogMessages;
|
||||
}
|
||||
|
||||
public BlazorClient Client { get; set; }
|
||||
|
||||
private IList<Batch> Batches { get; set; } = new List<Batch>();
|
||||
|
||||
// We use a stack so that we can search the logs in reverse order
|
||||
private ConcurrentStack<LogMessage> Logs { get; set; } = new ConcurrentStack<LogMessage>();
|
||||
|
||||
public TestSink Sink { get; private set; }
|
||||
|
||||
[Fact]
|
||||
public async Task DispatchedEventsWillKeepBeingProcessed_ButUpdatedWillBeDelayedUntilARenderIsAcknowledged()
|
||||
{
|
||||
// Arrange
|
||||
var baseUri = new Uri(_serverFixture.RootUri, "/subdir");
|
||||
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
|
||||
Assert.Single(Batches);
|
||||
|
||||
await Client.SelectAsync("test-selector-select", "BasicTestApp.LimitCounterComponent");
|
||||
Client.ConfirmRenderBatch = false;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
await Client.ClickAsync("increment");
|
||||
}
|
||||
await Client.ClickAsync("increment", expectRenderBatch: false);
|
||||
|
||||
Assert.Single(Logs, l => (LogLevel.Debug, "The queue of unacknowledged render batches is full.") == (l.LogLevel, l.Message));
|
||||
Assert.Equal("10", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent);
|
||||
var fullCount = Batches.Count;
|
||||
|
||||
// Act
|
||||
await Client.ClickAsync("increment", expectRenderBatch: false);
|
||||
|
||||
Assert.Contains(Logs, l => (LogLevel.Debug, "The queue of unacknowledged render batches is full.") == (l.LogLevel, l.Message));
|
||||
Assert.Equal(fullCount, Batches.Count);
|
||||
Client.ConfirmRenderBatch = true;
|
||||
|
||||
// This will resume the render batches.
|
||||
await Client.ExpectRenderBatch(() => Client.ConfirmBatch(Batches[^1].Id));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("12", ((TextNode)Client.FindElementById("the-count").Children.Single()).TextContent);
|
||||
Assert.Equal(fullCount + 1, Batches.Count);
|
||||
}
|
||||
|
||||
private void LogMessages(WriteContext context) => Logs.Push(new LogMessage(context.LogLevel, context.Message, context.Exception));
|
||||
|
||||
[DebuggerDisplay("{Message,nq}")]
|
||||
private class LogMessage
|
||||
{
|
||||
public LogMessage(LogLevel logLevel, string message, Exception exception)
|
||||
{
|
||||
LogLevel = logLevel;
|
||||
Message = message;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
public LogLevel LogLevel { get; set; }
|
||||
public string Message { get; set; }
|
||||
public Exception Exception { get; set; }
|
||||
}
|
||||
|
||||
private class Batch
|
||||
{
|
||||
public Batch(int rendererId, int id, byte[] data)
|
||||
{
|
||||
Id = id;
|
||||
RendererId = rendererId;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public int Id { get; }
|
||||
public int RendererId { get; }
|
||||
public byte[] Data { get; }
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Sink != null)
|
||||
{
|
||||
Sink.MessageLogged -= LogMessages;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
<option value="BasicTestApp.KeyCasesComponent">Key cases</option>
|
||||
<option value="BasicTestApp.KeyPressEventComponent">Key press event</option>
|
||||
<option value="BasicTestApp.LaggyTypingComponent">Laggy typing</option>
|
||||
<option value="BasicTestApp.LimitCounterComponent">Limit counter component</option>
|
||||
<option value="BasicTestApp.LocalizedText">Localized Text</option>
|
||||
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
|
||||
<option value="BasicTestApp.LongRunningInterop">Long running interop</option>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<h1>Counter</h1>
|
||||
|
||||
<p>This counter component is used in the server-side limit tests to showcase that we keep dispatching and processing
|
||||
events when the unacknowledged render batches queue is full and that we produce the update UI once we receive an
|
||||
acknowledgement.</p>
|
||||
|
||||
<p>Current count: <span id="the-count">@currentCount</span></p>
|
||||
<p><button id="increment" @onclick="@(handleClicks ? (Action)IncrementCount : null)">Click me</button></p>
|
||||
|
||||
|
||||
@code {
|
||||
int currentCount = 0;
|
||||
bool handleClicks = true;
|
||||
|
||||
void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
currentCount = 0;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
|
@ -112,14 +112,20 @@ namespace Ignitor
|
|||
return NextErrorReceived.Completion.Task;
|
||||
}
|
||||
|
||||
public async Task ClickAsync(string elementId)
|
||||
public Task ClickAsync(string elementId, bool expectRenderBatch = true)
|
||||
{
|
||||
if (!Hive.TryFindElementById(elementId, out var elementNode))
|
||||
{
|
||||
throw new InvalidOperationException($"Could not find element with id {elementId}.");
|
||||
}
|
||||
|
||||
await ExpectRenderBatch(() => elementNode.ClickAsync(HubConnection));
|
||||
if (expectRenderBatch)
|
||||
{
|
||||
return ExpectRenderBatch(() => elementNode.ClickAsync(HubConnection));
|
||||
}
|
||||
else
|
||||
{
|
||||
return elementNode.ClickAsync(HubConnection);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SelectAsync(string elementId, string value)
|
||||
|
|
@ -292,7 +298,7 @@ namespace Ignitor
|
|||
|
||||
if (ConfirmRenderBatch)
|
||||
{
|
||||
HubConnection.InvokeAsync("OnRenderCompleted", batchId, /* error */ null);
|
||||
_ = ConfirmBatch(batchId);
|
||||
}
|
||||
|
||||
NextBatchReceived?.Completion?.TrySetResult(null);
|
||||
|
|
@ -303,6 +309,11 @@ namespace Ignitor
|
|||
}
|
||||
}
|
||||
|
||||
public Task ConfirmBatch(int batchId, string error = null)
|
||||
{
|
||||
return HubConnection.InvokeAsync("OnRenderCompleted", batchId, error);
|
||||
}
|
||||
|
||||
private void OnError(string error)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
Loading…
Reference in New Issue