Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

IApiFilter丢失AsyncLocal值 #261

Open
pengweiqhca opened this issue Jun 21, 2024 · 5 comments
Open

IApiFilter丢失AsyncLocal值 #261

pengweiqhca opened this issue Jun 21, 2024 · 5 comments
Labels

Comments

@pengweiqhca
Copy link
Contributor

private sealed class TestFilter : IApiFilter
{
    private static readonly AsyncLocal<bool> V = new();

    public Task OnRequestAsync(ApiRequestContext context)
    {
        V.Value = true;

        return Task.CompletedTask;
    }

    public Task OnResponseAsync(ApiResponseContext context)
    {
        Assert.True(V.Value); // 此处将失败,应该成功

        return Task.CompletedTask;
    }
}

解决办法需要重构ApiRequestExecuter

下面是可能的解决办法(未验证,仅限参考):

private static Func<ApiRequestContext, Task<ApiResponseContext>> BuildFilterPipe(ApiRequestContext context)
{
    Func<ApiRequestContext, Task<ApiResponseContext>> executor = static async request =>
    {
        using var requestAbortedLinker = new CancellationTokenLinker(request.HttpContext.CancellationTokens);

        return await ApiRequestSender.SendAsync(request, requestAbortedLinker.Token).ConfigureAwait(false);
    };

    executor = context.ActionDescriptor.FilterAttributes.Reverse()
        .Aggregate(executor, (current, filter) => Build(filter, current));

    executor = context.HttpContext.HttpApiOptions.GlobalFilters.Reverse()
        .Aggregate(executor, (current, filter) => Build(filter, current));

    static Func<ApiRequestContext, Task<ApiResponseContext>> Build(IApiFilter filter,
        Func<ApiRequestContext, Task<ApiResponseContext>> next)
    {
        return async context =>
        {
            await filter.OnRequestAsync(context).ConfigureAwait(false);

            var response = await next(context);

            await filter.OnResponseAsync(response).ConfigureAwait(false);

            return response;
        };
    }

    return executor;
}
@xljiulang
Copy link
Collaborator

return async context =>
{
    await filter.OnRequestAsync(context).ConfigureAwait(false);
    var response = await next(context);
    await filter.OnResponseAsync(response).ConfigureAwait(false);
    return response;
};

这里的处理不是正确的:

  1. 在OnRequestAsync里创建的AsyncLocal不会影响到 async context{}方法的执行上下文,然而OnResponseAsync捕捉的是 async context{}方法的执行上下文。
  2. 并不是每个filter要发起一笔请求,而是一笔请求需要多个filter来协同处理

正确的实现,应该是每个OnXXXAsync()的逻辑(不只是Filter的),包装为一个中间件,然后按照现在的foreach先后顺序,把他们串起来编排,m1调用m2,m2调用m3这样的逻辑传递。考虑到这个编排过程依赖于ApiRequestContext自身的ActionDescriptor和HttpContext.HttpApiOptions两个重要属性,这就只能每次请求时编排和创建与Action关联的中间件委托,不管使用不使用Action来做中间件委托的缓存,都会带来一些性能损耗。

@pengweiqhca
Copy link
Contributor Author

pengweiqhca commented Jun 22, 2024

那可以添加一个如下的IApiPipeFilter接口,使用者只需要实现IApiPipeFilter即可

static Func<ApiRequestContext, Task<ApiResponseContext>> Build(IApiFilter filter,
    Func<ApiRequestContext, Task<ApiResponseContext>> next) =>
    context => (filter as IApiPipeFilter ?? new ApiFilterWrapper(filter)).ExecuteAsync(context, next);

interface IApiPipeFilter : IApiFilter
{
  Task<ApiResponseContext> ExecuteAsync(ApiRequestContext context, Func<ApiRequestContext, Task<ApiResponseContext>> next);

  Task IApiFilter.OnRequestAsync(ApiRequestContext context) => Task.CompletedTask;

  Task IApiFilter.OnResponseAsync(ApiResponseContext context) => Task.CompletedTask;
}

class ApiFilterWrapper(IApiFilter filter) : IApiPipeFilter
{
  public async Task<ApiResponseContext> ExecuteAsync(ApiRequestContext context, Func<ApiRequestContext, Task<ApiResponseContext>> next)
  {
      await filter.OnRequestAsync(context);
      var response = await next(context);
      await filter.OnResponseAsync(response);
      return response;
  }
}

private sealed class TestFilter : IApiPipeFilter
{
    private static readonly AsyncLocal<bool> V = new();

    public async Task<ApiResponseContext> ExecuteAsync(ApiRequestContext context, Func<ApiRequestContext, Task<ApiResponseContext>> next)
    {
        V.Value = true;

        var response = await next(context);
        
        Assert.True(V.Value);

        return response;
    }
}

@xljiulang
Copy link
Collaborator

好想法,我们可以在WebApiClientCore.Abstractions项目新建一个public delegate Task<ApiResponseContext> RequestDelegate(ApiRequestContext request);的请求委托,并为ApiActionDescriptor增加一个public RequestDelegate RequestDelegate {get;}的属性, 创建和修改RequestDelegate属性的过程,在DefaultApiActionDescriptor中实现,步骤为:

1.注入IOptionsMonitor获取GlobalFilters,拿GlobalFilters和Attribute类型的Filter以及ApiRequestExecutor(去掉了filter执行)一起build出RequestDelegate ;
2. 监听IOptionsMonitor的变化,变化之后执行步骤1的逻辑;

这样做之后,至少在Filter,应该能够使用AsyncLocal了

xljiulang added a commit that referenced this issue Jun 22, 2024
@xljiulang
Copy link
Collaborator

xljiulang commented Jun 23, 2024

@pengweiqhca

public async Task<ApiResponseContext> ExecuteAsync(ApiRequestContext context, Func<ApiRequestContext, Task<ApiResponseContext>> next)
  {
      await filter.OnRequestAsync(context);
      var response = await next(context);
      await filter.OnResponseAsync(response);
      return response;
  }

上面这种在OnRequestAsync里异步之后给AsyncLocal设置值,在OnResponseAsync里是获取不到的。

    public async Task<ApiResponseContext> ExecuteAsync(ApiRequestContext context, Func<ApiRequestContext, Task<ApiResponseContext>> next)
    {
        V.Value = true;

        var response = await next(context);
        
        Assert.True(V.Value);

        return response;
    }

上面这种代码,完全不需要AsyncLocal(你可以把V当作是普通的Nullable<bool>类型),任何访问的变量,可以被捕获变成async await的上下文。

@pengweiqhca
Copy link
Contributor Author

第二种AsyncLocal是要在HTTP管道中使用到AsyncLocal值,此处只是用于举例

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants