blob: 2a5c2dddcee46f57bca5450c75db2e432a48749f [file]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Apache.Arrow.Flight.Middleware.Interfaces;
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace Apache.Arrow.Flight.Middleware.Interceptors;
public sealed class ClientInterceptorAdapter : Interceptor
{
private readonly IReadOnlyList<IFlightClientMiddlewareFactory> _factories;
public ClientInterceptorAdapter(IEnumerable<IFlightClientMiddlewareFactory> factories)
{
_factories = factories?.ToList() ?? throw new ArgumentNullException(nameof(factories));
}
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
where TRequest : class
where TResponse : class
{
var options = InterceptCall(context, out var middlewares);
var newContext = new ClientInterceptorContext<TRequest, TResponse>(
context.Method,
context.Host,
options);
var call = continuation(request, newContext);
return new AsyncUnaryCall<TResponse>(
HandleResponse(call.ResponseAsync, call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers,
call.Dispose, middlewares),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose
);
}
public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
where TRequest : class
where TResponse : class
{
var callOptions = InterceptCall(context, out var middlewares);
var newContext = new ClientInterceptorContext<TRequest, TResponse>(
context.Method, context.Host, callOptions);
var call = continuation(request, newContext);
var responseHeadersTask = call.ResponseHeadersAsync.ContinueWith(task =>
{
if (task.IsFaulted)
{
throw task.Exception!;
}
if (task.IsCanceled)
{
throw new TaskCanceledException(task);
}
var headers = task.Result;
var ch = new CallHeaders(headers);
foreach (var m in middlewares)
m?.OnHeadersReceived(ch);
return headers;
});
var wrappedResponseStream = new MiddlewareResponseStream<TResponse>(
call.ResponseStream,
call,
middlewares);
return new AsyncServerStreamingCall<TResponse>(
wrappedResponseStream,
responseHeadersTask,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private CallOptions InterceptCall<TRequest, TResponse>(
ClientInterceptorContext<TRequest, TResponse> context,
out List<IFlightClientMiddleware> middlewareList)
where TRequest : class
where TResponse : class
{
var callInfo = new CallInfo(context.Method.FullName, context.Method.Type);
var headers = context.Options.Headers ?? new Metadata();
middlewareList = new List<IFlightClientMiddleware>();
var callHeaders = new CallHeaders(headers);
foreach (var factory in _factories)
{
var middleware = factory.OnCallStarted(callInfo);
middleware?.OnBeforeSendingHeaders(callHeaders);
middlewareList.Add(middleware);
}
return context.Options.WithHeaders(headers);
}
private async Task<TResponse> HandleResponse<TResponse>(
Task<TResponse> responseTask,
Task<Metadata> headersTask,
Func<Status> getStatus,
Func<Metadata> getTrailers,
Action dispose,
List<IFlightClientMiddleware> middlewares)
{
var nonNullMiddlewares = (middlewares ?? new List<IFlightClientMiddleware>())
.Where(m => m != null)
.ToList();
var hasMiddlewares = nonNullMiddlewares.Count > 0;
var completionNotified = false;
try
{
// Always await headers to surface faults; only materialize CallHeaders if needed.
var headers = await headersTask.ConfigureAwait(false);
if (hasMiddlewares)
{
var ch = new CallHeaders(headers);
foreach (var m in nonNullMiddlewares)
m.OnHeadersReceived(ch);
}
var response = await responseTask.ConfigureAwait(false);
// Single completion notification
NotifyCompletionOnce();
return response;
}
catch
{
// Completion on failure (only once)
NotifyCompletionOnce();
throw;
}
finally
{
dispose?.Invoke();
}
void NotifyCompletionOnce()
{
if (completionNotified || !hasMiddlewares) return;
completionNotified = true;
var status = getStatus();
var trailers = getTrailers();
foreach (var m in nonNullMiddlewares)
m.OnCallCompleted(status, trailers);
}
}
}