[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 Renderer(System.IServiceProvider serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
|
||||||
public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; }
|
public abstract Microsoft.AspNetCore.Components.Dispatcher Dispatcher { get; }
|
||||||
public event System.UnhandledExceptionEventHandler UnhandledSynchronizationException { add { } remove { } }
|
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; }
|
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 virtual System.Threading.Tasks.Task DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.Rendering.EventFieldInfo fieldInfo, System.EventArgs eventArgs) { throw null; }
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
protected virtual void Dispose(bool disposing) { }
|
protected virtual void Dispose(bool disposing) { }
|
||||||
protected abstract void HandleException(System.Exception exception);
|
protected abstract void HandleException(System.Exception exception);
|
||||||
protected Microsoft.AspNetCore.Components.IComponent InstantiateComponent(System.Type componentType) { throw null; }
|
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; }
|
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId) { throw null; }
|
||||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||||
protected System.Threading.Tasks.Task RenderRootComponentAsync(int componentId, Microsoft.AspNetCore.Components.ParameterView initialParameters) { throw null; }
|
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
|
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);
|
_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
|
// Since the task has yielded - process any queued rendering work before we return control
|
||||||
// to the caller.
|
// to the caller.
|
||||||
ProcessRenderQueue();
|
ProcessPendingRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task completed synchronously or is still running. We already processed all of the rendering
|
// Task completed synchronously or is still running. We already processed all of the rendering
|
||||||
|
|
@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="componentId">The ID of the component to render.</param>
|
/// <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>
|
/// <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();
|
EnsureSynchronizationContext();
|
||||||
|
|
||||||
|
|
@ -351,7 +351,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
|
|
||||||
if (!_isBatchInProgress)
|
if (!_isBatchInProgress)
|
||||||
{
|
{
|
||||||
ProcessRenderQueue();
|
ProcessPendingRender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,13 +398,27 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
? componentState
|
? componentState
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processses pending renders requests from components if there are any.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void ProcessPendingRender()
|
||||||
|
{
|
||||||
|
ProcessRenderQueue();
|
||||||
|
}
|
||||||
|
|
||||||
private void ProcessRenderQueue()
|
private void ProcessRenderQueue()
|
||||||
{
|
{
|
||||||
|
EnsureSynchronizationContext();
|
||||||
_isBatchInProgress = true;
|
_isBatchInProgress = true;
|
||||||
var updateDisplayTask = Task.CompletedTask;
|
var updateDisplayTask = Task.CompletedTask;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (_batchBuilder.ComponentRenderQueue.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Process render queue until empty
|
// Process render queue until empty
|
||||||
while (_batchBuilder.ComponentRenderQueue.Count > 0)
|
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.
|
// Ensure we catch errors while running the render functions of the components.
|
||||||
HandleException(e);
|
HandleException(e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
finally
|
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 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 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 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
|
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
|
|
|
||||||
|
|
@ -64,5 +64,17 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
/// Defaults to <c>1 minute</c>.
|
/// Defaults to <c>1 minute</c>.
|
||||||
/// </value>
|
/// </value>
|
||||||
public TimeSpan JSInteropDefaultCallTimeout { get; set; } = TimeSpan.FromMinutes(1);
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// We don't expect any of this code to actually throw, because DotNetDispatcher.BeginInvoke doesn't throw
|
// 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));
|
UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.Components.Web.Rendering;
|
using Microsoft.AspNetCore.Components.Web.Rendering;
|
||||||
using Microsoft.AspNetCore.Components.Routing;
|
using Microsoft.AspNetCore.Components.Routing;
|
||||||
|
|
@ -13,8 +14,8 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
{
|
{
|
||||||
|
|
@ -24,16 +25,19 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly CircuitIdFactory _circuitIdFactory;
|
private readonly CircuitIdFactory _circuitIdFactory;
|
||||||
|
private readonly CircuitOptions _options;
|
||||||
|
|
||||||
public DefaultCircuitFactory(
|
public DefaultCircuitFactory(
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
CircuitIdFactory circuitIdFactory)
|
CircuitIdFactory circuitIdFactory,
|
||||||
|
IOptions<CircuitOptions> options)
|
||||||
{
|
{
|
||||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_logger = _loggerFactory.CreateLogger<CircuitFactory>();
|
_logger = _loggerFactory.CreateLogger<CircuitFactory>();
|
||||||
_circuitIdFactory = circuitIdFactory ?? throw new ArgumentNullException(nameof(circuitIdFactory));
|
_circuitIdFactory = circuitIdFactory ?? throw new ArgumentNullException(nameof(circuitIdFactory));
|
||||||
|
_options = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override CircuitHost CreateCircuitHost(
|
public override CircuitHost CreateCircuitHost(
|
||||||
|
|
@ -80,6 +84,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
scope.ServiceProvider,
|
scope.ServiceProvider,
|
||||||
_loggerFactory,
|
_loggerFactory,
|
||||||
rendererRegistry,
|
rendererRegistry,
|
||||||
|
_options,
|
||||||
jsRuntime,
|
jsRuntime,
|
||||||
client,
|
client,
|
||||||
encoder,
|
encoder,
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Components.Server;
|
||||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Internal;
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
@ -23,8 +23,10 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
|
|
||||||
private readonly IJSRuntime _jsRuntime;
|
private readonly IJSRuntime _jsRuntime;
|
||||||
private readonly CircuitClientProxy _client;
|
private readonly CircuitClientProxy _client;
|
||||||
|
private readonly CircuitOptions _options;
|
||||||
private readonly RendererRegistry _rendererRegistry;
|
private readonly RendererRegistry _rendererRegistry;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
internal readonly ConcurrentQueue<UnacknowledgedRenderBatch> _unacknowledgedRenderBatches = new ConcurrentQueue<UnacknowledgedRenderBatch>();
|
||||||
private long _nextRenderId = 1;
|
private long _nextRenderId = 1;
|
||||||
private bool _disposing = false;
|
private bool _disposing = false;
|
||||||
|
|
||||||
|
|
@ -40,6 +42,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
RendererRegistry rendererRegistry,
|
RendererRegistry rendererRegistry,
|
||||||
|
CircuitOptions options,
|
||||||
IJSRuntime jsRuntime,
|
IJSRuntime jsRuntime,
|
||||||
CircuitClientProxy client,
|
CircuitClientProxy client,
|
||||||
HtmlEncoder encoder,
|
HtmlEncoder encoder,
|
||||||
|
|
@ -49,13 +52,12 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
_rendererRegistry = rendererRegistry;
|
_rendererRegistry = rendererRegistry;
|
||||||
_jsRuntime = jsRuntime;
|
_jsRuntime = jsRuntime;
|
||||||
_client = client;
|
_client = client;
|
||||||
|
_options = options;
|
||||||
|
|
||||||
Id = _rendererRegistry.Add(this);
|
Id = _rendererRegistry.Add(this);
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ConcurrentQueue<UnacknowledgedRenderBatch> UnacknowledgedRenderBatches = new ConcurrentQueue<UnacknowledgedRenderBatch>();
|
|
||||||
|
|
||||||
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
|
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
|
||||||
|
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
|
|
@ -81,6 +83,34 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
return RenderRootComponentAsync(componentId);
|
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 />
|
/// <inheritdoc />
|
||||||
protected override void HandleException(Exception exception)
|
protected override void HandleException(Exception exception)
|
||||||
{
|
{
|
||||||
|
|
@ -104,7 +134,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
{
|
{
|
||||||
_disposing = true;
|
_disposing = true;
|
||||||
_rendererRegistry.TryRemove(Id);
|
_rendererRegistry.TryRemove(Id);
|
||||||
while (UnacknowledgedRenderBatches.TryDequeue(out var entry))
|
while (_unacknowledgedRenderBatches.TryDequeue(out var entry))
|
||||||
{
|
{
|
||||||
entry.CompletionSource.TrySetCanceled();
|
entry.CompletionSource.TrySetCanceled();
|
||||||
entry.Data.Dispose();
|
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
|
// Buffer the rendered batches no matter what. We'll send it down immediately when the client
|
||||||
// is connected or right after the client reconnects.
|
// is connected or right after the client reconnects.
|
||||||
|
|
||||||
UnacknowledgedRenderBatches.Enqueue(pendingRender);
|
_unacknowledgedRenderBatches.Enqueue(pendingRender);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -167,7 +197,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
// All the batches are sent in order based on the fact that SignalR
|
// All the batches are sent in order based on the fact that SignalR
|
||||||
// provides ordering for the underlying messages and that the batches
|
// provides ordering for the underlying messages and that the batches
|
||||||
// are always in order.
|
// 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)
|
private async Task WriteBatchBytesAsync(UnacknowledgedRenderBatch pending)
|
||||||
|
|
@ -203,12 +233,12 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
// disposed.
|
// disposed.
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnRenderCompleted(long incomingBatchId, string errorMessageOrNull)
|
public Task OnRenderCompleted(long incomingBatchId, string errorMessageOrNull)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
{
|
{
|
||||||
// Disposing so don't do work.
|
// Disposing so don't do work.
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When clients send acks we know for sure they received and applied the batch.
|
// 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
|
// 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.
|
// 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);
|
Log.ReceivedDuplicateBatchAck(_logger, incomingBatchId);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var lastBatchId = nextUnacknowledgedBatch.BatchId;
|
var lastBatchId = nextUnacknowledgedBatch.BatchId;
|
||||||
// Order is important here so that we don't prematurely dequeue the last nextUnacknowledgedBatch
|
// 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;
|
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);
|
ProcessPendingBatch(errorMessageOrNull, nextUnacknowledgedBatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +286,16 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
{
|
{
|
||||||
HandleException(
|
HandleException(
|
||||||
new InvalidOperationException($"Received an acknowledgement for batch with id '{incomingBatchId}' when the last batch produced was '{lastBatchId}'."));
|
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, string, double, Exception> _completingBatchWithError;
|
||||||
private static readonly Action<ILogger, long, double, Exception> _completingBatchWithoutError;
|
private static readonly Action<ILogger, long, double, Exception> _completingBatchWithoutError;
|
||||||
private static readonly Action<ILogger, long, Exception> _receivedDuplicateBatchAcknowledgement;
|
private static readonly Action<ILogger, long, Exception> _receivedDuplicateBatchAcknowledgement;
|
||||||
|
private static readonly Action<ILogger, Exception> _fullUnacknowledgedRenderBatchesQueue;
|
||||||
|
|
||||||
private static class EventIds
|
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 CompletingBatchWithError = new EventId(104, "CompletingBatchWithError");
|
||||||
public static readonly EventId CompletingBatchWithoutError = new EventId(105, "CompletingBatchWithoutError");
|
public static readonly EventId CompletingBatchWithoutError = new EventId(105, "CompletingBatchWithoutError");
|
||||||
public static readonly EventId ReceivedDuplicateBatchAcknowledgement = new EventId(106, "ReceivedDuplicateBatchAcknowledgement");
|
public static readonly EventId ReceivedDuplicateBatchAcknowledgement = new EventId(106, "ReceivedDuplicateBatchAcknowledgement");
|
||||||
|
public static readonly EventId FullUnacknowledgedRenderBatchesQueue = new EventId(107, "FullUnacknowledgedRenderBatchesQueue");
|
||||||
}
|
}
|
||||||
|
|
||||||
static Log()
|
static Log()
|
||||||
|
|
@ -369,6 +413,11 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
EventIds.ReceivedDuplicateBatchAcknowledgement,
|
EventIds.ReceivedDuplicateBatchAcknowledgement,
|
||||||
"Received a duplicate ACK for batch id '{IncomingBatchId}'.");
|
"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)
|
public static void SendBatchDataFailed(ILogger logger, Exception exception)
|
||||||
|
|
@ -421,10 +470,27 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void ReceivedDuplicateBatchAck(ILogger logger, long incomingBatchId)
|
public static void ReceivedDuplicateBatchAck(ILogger logger, long incomingBatchId)
|
||||||
{
|
{
|
||||||
_receivedDuplicateBatchAcknowledgement(logger, incomingBatchId, null);
|
_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);
|
Log.ReceivedConfirmationForBatch(_logger, renderId);
|
||||||
CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
|
_ = CircuitHost.Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnLocationChanged(string uri, bool intercepted)
|
public void OnLocationChanged(string uri, bool intercepted)
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
private class TestRemoteRenderer : RemoteRenderer
|
private class TestRemoteRenderer : RemoteRenderer
|
||||||
{
|
{
|
||||||
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client)
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Components.Server;
|
||||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
@ -48,7 +50,82 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
component.TriggerRender();
|
component.TriggerRender();
|
||||||
|
|
||||||
// Assert
|
// 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]
|
[Fact]
|
||||||
|
|
@ -83,7 +160,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
|
|
||||||
var componentId = renderer.AssignRootComponentId(component);
|
var componentId = renderer.AssignRootComponentId(component);
|
||||||
component.TriggerRender();
|
component.TriggerRender();
|
||||||
renderer.OnRenderCompleted(2, null);
|
_ = renderer.OnRenderCompleted(2, null);
|
||||||
|
|
||||||
@event.Reset();
|
@event.Reset();
|
||||||
firstBatchTCS.SetResult(null);
|
firstBatchTCS.SetResult(null);
|
||||||
|
|
@ -101,7 +178,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
|
|
||||||
foreach (var id in renderIds.ToArray())
|
foreach (var id in renderIds.ToArray())
|
||||||
{
|
{
|
||||||
renderer.OnRenderCompleted(id, null);
|
_ = renderer.OnRenderCompleted(id, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
secondBatchTCS.SetResult(null);
|
secondBatchTCS.SetResult(null);
|
||||||
|
|
@ -152,7 +229,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
};
|
};
|
||||||
// This produces an additional batch (id = 3)
|
// This produces an additional batch (id = 3)
|
||||||
trigger.TriggerRender();
|
trigger.TriggerRender();
|
||||||
var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count;
|
var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
offlineClient.Transfer(onlineClient.Object, "new-connection");
|
offlineClient.Transfer(onlineClient.Object, "new-connection");
|
||||||
|
|
@ -164,14 +241,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
};
|
};
|
||||||
|
|
||||||
// Receive the ack for the intial batch
|
// Receive the ack for the intial batch
|
||||||
renderer.OnRenderCompleted(2, null);
|
_ = renderer.OnRenderCompleted(2, null);
|
||||||
// Receive the ack for the second batch
|
// Receive the ack for the second batch
|
||||||
renderer.OnRenderCompleted(3, null);
|
_ = renderer.OnRenderCompleted(3, null);
|
||||||
|
|
||||||
firstBatchTCS.SetResult(null);
|
firstBatchTCS.SetResult(null);
|
||||||
secondBatchTCS.SetResult(null);
|
secondBatchTCS.SetResult(null);
|
||||||
// Repeat the ack for the third batch
|
// Repeat the ack for the third batch
|
||||||
renderer.OnRenderCompleted(3, null);
|
_ = renderer.OnRenderCompleted(3, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(exceptions);
|
Assert.Empty(exceptions);
|
||||||
|
|
@ -215,7 +292,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
};
|
};
|
||||||
// This produces an additional batch (id = 3)
|
// This produces an additional batch (id = 3)
|
||||||
trigger.TriggerRender();
|
trigger.TriggerRender();
|
||||||
var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count;
|
var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
offlineClient.Transfer(onlineClient.Object, "new-connection");
|
offlineClient.Transfer(onlineClient.Object, "new-connection");
|
||||||
|
|
@ -227,14 +304,14 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
};
|
};
|
||||||
|
|
||||||
// Receive the ack for the intial batch
|
// Receive the ack for the intial batch
|
||||||
renderer.OnRenderCompleted(2, null);
|
_ = renderer.OnRenderCompleted(2, null);
|
||||||
// Receive the ack for the second batch
|
// Receive the ack for the second batch
|
||||||
renderer.OnRenderCompleted(2, null);
|
_ = renderer.OnRenderCompleted(2, null);
|
||||||
|
|
||||||
firstBatchTCS.SetResult(null);
|
firstBatchTCS.SetResult(null);
|
||||||
secondBatchTCS.SetResult(null);
|
secondBatchTCS.SetResult(null);
|
||||||
// Repeat the ack for the third batch
|
// Repeat the ack for the third batch
|
||||||
renderer.OnRenderCompleted(3, null);
|
_ = renderer.OnRenderCompleted(3, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(exceptions);
|
Assert.Empty(exceptions);
|
||||||
|
|
@ -278,7 +355,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
};
|
};
|
||||||
// This produces an additional batch (id = 3)
|
// This produces an additional batch (id = 3)
|
||||||
trigger.TriggerRender();
|
trigger.TriggerRender();
|
||||||
var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count;
|
var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exceptions = new List<Exception>();
|
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
|
// Pretend that we missed the ack for the initial batch
|
||||||
renderer.OnRenderCompleted(3, null);
|
_ = renderer.OnRenderCompleted(3, null);
|
||||||
firstBatchTCS.SetResult(null);
|
firstBatchTCS.SetResult(null);
|
||||||
secondBatchTCS.SetResult(null);
|
secondBatchTCS.SetResult(null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(exceptions);
|
Assert.Empty(exceptions);
|
||||||
Assert.Empty(renderer.UnacknowledgedRenderBatches);
|
Assert.Empty(renderer._unacknowledgedRenderBatches);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -335,7 +412,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
};
|
};
|
||||||
// This produces an additional batch (id = 3)
|
// This produces an additional batch (id = 3)
|
||||||
trigger.TriggerRender();
|
trigger.TriggerRender();
|
||||||
var originallyQueuedBatches = renderer.UnacknowledgedRenderBatches.Count;
|
var originallyQueuedBatches = renderer._unacknowledgedRenderBatches.Count;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exceptions = new List<Exception>();
|
var exceptions = new List<Exception>();
|
||||||
|
|
@ -344,7 +421,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
exceptions.Add(e);
|
exceptions.Add(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderer.OnRenderCompleted(4, null);
|
_ = renderer.OnRenderCompleted(4, null);
|
||||||
firstBatchTCS.SetResult(null);
|
firstBatchTCS.SetResult(null);
|
||||||
secondBatchTCS.SetResult(null);
|
secondBatchTCS.SetResult(null);
|
||||||
|
|
||||||
|
|
@ -372,7 +449,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(0, first.ComponentId);
|
Assert.Equal(0, first.ComponentId);
|
||||||
Assert.Equal(1, second.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)
|
private RemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClientProxy)
|
||||||
|
|
@ -389,6 +466,7 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
|
||||||
serviceProvider,
|
serviceProvider,
|
||||||
NullLoggerFactory.Instance,
|
NullLoggerFactory.Instance,
|
||||||
new RendererRegistry(),
|
new RendererRegistry(),
|
||||||
|
new CircuitOptions(),
|
||||||
jsRuntime.Object,
|
jsRuntime.Object,
|
||||||
circuitClientProxy,
|
circuitClientProxy,
|
||||||
HtmlEncoder.Default,
|
HtmlEncoder.Default,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
serviceScope.ServiceProvider ?? Mock.Of<IServiceProvider>(),
|
serviceScope.ServiceProvider ?? Mock.Of<IServiceProvider>(),
|
||||||
NullLoggerFactory.Instance,
|
NullLoggerFactory.Instance,
|
||||||
new RendererRegistry(),
|
new RendererRegistry(),
|
||||||
|
new CircuitOptions(),
|
||||||
jsRuntime,
|
jsRuntime,
|
||||||
clientProxy,
|
clientProxy,
|
||||||
HtmlEncoder.Default,
|
HtmlEncoder.Default,
|
||||||
|
|
|
||||||
|
|
@ -507,15 +507,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||||
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
|
sink.MessageLogged += (wc) => logEvents.Add((wc.LogLevel, wc.EventId.Name, wc.Exception));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await Client.ClickAsync("event-handler-throw-sync");
|
await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false);
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
logEvents,
|
logEvents,
|
||||||
e => LogLevel.Warning == e.logLevel &&
|
e => LogLevel.Warning == e.logLevel &&
|
||||||
"UnhandledExceptionInCircuit" == e.eventIdName &&
|
"UnhandledExceptionInCircuit" == e.eventIdName &&
|
||||||
"Handler threw an exception" == e.exception.Message);
|
"Handler threw an exception" == e.exception.Message);
|
||||||
|
|
||||||
await ValidateClientKeepsWorking(Client, batches);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ValidateClientKeepsWorking(BlazorClient Client, List<(int, int, byte[])> 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.KeyCasesComponent">Key cases</option>
|
||||||
<option value="BasicTestApp.KeyPressEventComponent">Key press event</option>
|
<option value="BasicTestApp.KeyPressEventComponent">Key press event</option>
|
||||||
<option value="BasicTestApp.LaggyTypingComponent">Laggy typing</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.LocalizedText">Localized Text</option>
|
||||||
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
|
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
|
||||||
<option value="BasicTestApp.LongRunningInterop">Long running interop</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;
|
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))
|
if (!Hive.TryFindElementById(elementId, out var elementNode))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Could not find element with id {elementId}.");
|
throw new InvalidOperationException($"Could not find element with id {elementId}.");
|
||||||
}
|
}
|
||||||
|
if (expectRenderBatch)
|
||||||
await ExpectRenderBatch(() => elementNode.ClickAsync(HubConnection));
|
{
|
||||||
|
return ExpectRenderBatch(() => elementNode.ClickAsync(HubConnection));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return elementNode.ClickAsync(HubConnection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SelectAsync(string elementId, string value)
|
public async Task SelectAsync(string elementId, string value)
|
||||||
|
|
@ -292,7 +298,7 @@ namespace Ignitor
|
||||||
|
|
||||||
if (ConfirmRenderBatch)
|
if (ConfirmRenderBatch)
|
||||||
{
|
{
|
||||||
HubConnection.InvokeAsync("OnRenderCompleted", batchId, /* error */ null);
|
_ = ConfirmBatch(batchId);
|
||||||
}
|
}
|
||||||
|
|
||||||
NextBatchReceived?.Completion?.TrySetResult(null);
|
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)
|
private void OnError(string error)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue