我也使用Installing a new middleware at runtime in ASP.Net Core的方法遇到了这个间歇性问题
堆栈跟踪
System.ArgumentException
Destination array was not long enough. Check the destination index, length, and the array's lower bounds. (Parameter 'destinationArray')
MyAssembly.Services.RuntimeMiddlewareService+<>c__DisplayClass2_0.<Use>b__1
System.ArgumentException:
at System.Array.Copy (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at System.Collections.Generic.List`1.CopyTo (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at System.Collections.Generic.EnumerableHelpers.ToArray (System.Linq, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a)
at System.Linq.Buffer`1..ctor (System.Linq, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a)
at System.Linq.Enumerable+ReverseIterator`1.MoveNext (System.Linq, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a)
at Microsoft.AspNetCore.Builder.ApplicationBuilder.Build (Microsoft.AspNetCore.Http, Version=3.1.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
at MyAssembly.Services.RuntimeMiddlewareService+<>c__DisplayClass2_0.<Use>b__1 (MyAssembly, Version=1.0.12.0, Culture=neutral, PublicKeyToken=nullMyAssembly, Version=1.0.12.0, Culture=neutral, PublicKeyToken=null: /home/vsts/work/1/s/src/MyAssembly/Services/RuntimeMiddlewareService.csDeploymentManagementProxy, Version=1.0.12.0, Culture=neutral, PublicKeyToken=null: 14)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke (Microsoft.AspNetCore.StaticFiles, Version=3.1.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware+<Invoke>d__5.MoveNext (Swashbuckle.AspNetCore.SwaggerUI, Version=5.4.1.0, Culture=neutral, PublicKeyToken=4232c99127b3c254)
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware+<Invoke>d__4.MoveNext (Swashbuckle.AspNetCore.Swagger, Version=5.4.1.0, Culture=neutral, PublicKeyToken=62657d7474907593)
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware+<Invoke>d__5.MoveNext (Microsoft.AspNetCore.Authorization.Policy, Version=3.1.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware+<Invoke>d__6.MoveNext (Microsoft.AspNetCore.Authentication, Version=3.1.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e)
at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1+<ProcessRequestAsync>d__2.MoveNext (Microsoft.AspNetCore.Server.IIS, Version=3.1.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
我不得不进行大量挖掘才能找到根本原因:
从 ApplicationBuilder 的 Build 方法开始
https://github.com/dotnet/aspnetcore/blob/4ef204e13b88c0734e0e94a1cc4c0ef05f40849e/src/Http/Http/src/Builder/ApplicationBuilder.cs#L82
private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
_components.Add(middleware);
return this;
}
public RequestDelegate Build()
{
RequestDelegate app = context =>
{
// If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
// This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
var endpoint = context.GetEndpoint();
var endpointRequestDelegate = endpoint?.RequestDelegate;
if (endpointRequestDelegate != null)
{
var message =
$"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " +
$"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
$"routing.";
throw new InvalidOperationException(message);
}
context.Response.StatusCode = 404;
return Task.CompletedTask;
};
foreach (var component in _components.Reverse())
{
app = component(app);
}
return app;
}
要注意的行是:
foreach (var component in _components.Reverse())
Reverse 致电ReverseIterator
https://github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs#L911
public static IEnumerable<TSource> Reverse<TSource>(this IEnumerable<TSource> source) {
if (source == null) throw Error.ArgumentNull("source");
return ReverseIterator<TSource>(source);
}
static IEnumerable<TSource> ReverseIterator<TSource>(IEnumerable<TSource> source) {
Buffer<TSource> buffer = new Buffer<TSource>(source);
for (int i = buffer.count - 1; i >= 0; i--) yield return buffer.items[i];
}
创建一个Buffer<TElement>
https://github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs#L2664
struct Buffer<TElement>
{
internal TElement[] items;
internal int count;
internal Buffer(IEnumerable<TElement> source) {
TElement[] items = null;
int count = 0;
ICollection<TElement> collection = source as ICollection<TElement>;
if (collection != null) {
count = collection.Count;
if (count > 0) {
items = new TElement[count];
collection.CopyTo(items, 0);
}
}
else {
foreach (TElement item in source) {
if (items == null) {
items = new TElement[4];
}
else if (items.Length == count) {
TElement[] newItems = new TElement[checked(count * 2)];
Array.Copy(items, 0, newItems, 0, count);
items = newItems;
}
items[count] = item;
count++;
}
}
this.items = items;
this.count = count;
}
internal TElement[] ToArray() {
if (count == 0) return new TElement[0];
if (items.Length == count) return items;
TElement[] result = new TElement[count];
Array.Copy(items, 0, result, 0, count);
return result;
}
}
考虑以下几行:
if (collection != null) {
count = collection.Count;
if (count > 0) {
items = new TElement[count];
collection.CopyTo(items, 0);
}
}
集合是通过引用传递的。似乎在获取集合计数和创建相同大小的新数组之间的某个地方,集合的大小增加了超出TElement 数组的大小,导致错误“目标数组不够长。检查目标索引,长度和数组的下限。(参数'destinationArray')。这将表明存在线程安全问题并解释间歇性错误。
下一个问题是,线程安全问题的根源在哪里?
这对我个人来说还是个谜。我的应用程序中的RuntimeMiddlewareService 是在启动时创建的单例。并且它的Configure 函数肯定会被调用,但不够频繁(据我所知)来解释错误。所以 _middleware Func 可能会在其他地方被调用。也就是说,在对 Use 和 Build 的调用周围添加一个锁定对象解决了这个问题,可能是因为线程 1 正在调用 Build 而线程 2 刚刚调用 Use(这向 collection 添加了一个新项目) .
public class RuntimeMiddlewareService : IRuntimeMiddlewareService
{
private static readonly object _middlewareLock = new object();
private Func<RequestDelegate, RequestDelegate> _middleware;
private IApplicationBuilder _appBuilder;
public void Use(IApplicationBuilder app)
=> _appBuilder = app.Use(next => context => _middleware == null ? next(context) : _middleware(next)(context));
public void Configure(Action<IApplicationBuilder> action)
{
var app = _appBuilder.New();
action(app);
_middleware = next =>
{
lock (_middlewareLock)
{
return app.Use(_ => next).Build();
}
};
}
}