gooddata-flight-server 1.34.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of gooddata-flight-server might be problematic. Click here for more details.

Files changed (49) hide show
  1. gooddata_flight_server/__init__.py +23 -0
  2. gooddata_flight_server/_version.py +7 -0
  3. gooddata_flight_server/cli.py +137 -0
  4. gooddata_flight_server/config/__init__.py +1 -0
  5. gooddata_flight_server/config/config.py +536 -0
  6. gooddata_flight_server/errors/__init__.py +1 -0
  7. gooddata_flight_server/errors/error_code.py +209 -0
  8. gooddata_flight_server/errors/error_info.py +475 -0
  9. gooddata_flight_server/exceptions.py +16 -0
  10. gooddata_flight_server/health/__init__.py +1 -0
  11. gooddata_flight_server/health/health_check_http_server.py +103 -0
  12. gooddata_flight_server/health/server_health_monitor.py +83 -0
  13. gooddata_flight_server/metrics.py +16 -0
  14. gooddata_flight_server/py.typed +1 -0
  15. gooddata_flight_server/server/__init__.py +1 -0
  16. gooddata_flight_server/server/auth/__init__.py +1 -0
  17. gooddata_flight_server/server/auth/auth_middleware.py +83 -0
  18. gooddata_flight_server/server/auth/token_verifier.py +62 -0
  19. gooddata_flight_server/server/auth/token_verifier_factory.py +55 -0
  20. gooddata_flight_server/server/auth/token_verifier_impl.py +41 -0
  21. gooddata_flight_server/server/base.py +63 -0
  22. gooddata_flight_server/server/default.logging.ini +28 -0
  23. gooddata_flight_server/server/flight_rpc/__init__.py +1 -0
  24. gooddata_flight_server/server/flight_rpc/flight_middleware.py +162 -0
  25. gooddata_flight_server/server/flight_rpc/flight_server.py +228 -0
  26. gooddata_flight_server/server/flight_rpc/flight_service.py +279 -0
  27. gooddata_flight_server/server/flight_rpc/server_methods.py +200 -0
  28. gooddata_flight_server/server/server_base.py +321 -0
  29. gooddata_flight_server/server/server_main.py +116 -0
  30. gooddata_flight_server/tasks/__init__.py +1 -0
  31. gooddata_flight_server/tasks/base.py +21 -0
  32. gooddata_flight_server/tasks/metrics.py +115 -0
  33. gooddata_flight_server/tasks/task.py +193 -0
  34. gooddata_flight_server/tasks/task_error.py +60 -0
  35. gooddata_flight_server/tasks/task_executor.py +96 -0
  36. gooddata_flight_server/tasks/task_result.py +363 -0
  37. gooddata_flight_server/tasks/temporal_container.py +247 -0
  38. gooddata_flight_server/tasks/thread_task_executor.py +639 -0
  39. gooddata_flight_server/utils/__init__.py +1 -0
  40. gooddata_flight_server/utils/libc_utils.py +35 -0
  41. gooddata_flight_server/utils/logging.py +158 -0
  42. gooddata_flight_server/utils/methods_discovery.py +98 -0
  43. gooddata_flight_server/utils/otel_tracing.py +142 -0
  44. gooddata_flight_server-1.34.1.dev1.data/scripts/gooddata-flight-server +10 -0
  45. gooddata_flight_server-1.34.1.dev1.dist-info/LICENSE.txt +7 -0
  46. gooddata_flight_server-1.34.1.dev1.dist-info/METADATA +749 -0
  47. gooddata_flight_server-1.34.1.dev1.dist-info/RECORD +49 -0
  48. gooddata_flight_server-1.34.1.dev1.dist-info/WHEEL +5 -0
  49. gooddata_flight_server-1.34.1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,162 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from typing import Any, Callable, Optional
3
+
4
+ import opentelemetry.context as otelctx
5
+ import opentelemetry.propagate as otelpropagate
6
+ import pyarrow.flight
7
+ import structlog
8
+ from opentelemetry import trace
9
+ from opentelemetry.semconv.trace import SpanAttributes
10
+ from opentelemetry.trace import SpanKind, StatusCode, use_span
11
+ from typing_extensions import TypeAlias
12
+
13
+ from gooddata_flight_server.utils.otel_tracing import SERVER_TRACER
14
+
15
+ _LOGGER = structlog.get_logger("gooddata_flight_server.rpc")
16
+
17
+
18
+ class CallInfo(pyarrow.flight.ServerMiddleware):
19
+ """
20
+ Call Info middleware holds information about the current call:
21
+
22
+ - RPC method info
23
+ - headers used on RPC Call
24
+ """
25
+
26
+ MiddlewareName = "call_info"
27
+
28
+ def __init__(self, info: pyarrow.flight.CallInfo, headers: dict[str, list[str]]):
29
+ super().__init__()
30
+
31
+ self._info = info
32
+ self._headers = headers
33
+
34
+ @property
35
+ def info(self) -> pyarrow.flight.CallInfo:
36
+ """
37
+ :return: Flight's CallInfo with detail about the current call
38
+ """
39
+ return self._info
40
+
41
+ @property
42
+ def headers(self) -> dict[str, list[str]]:
43
+ """
44
+ :return: headers provided by the caller
45
+ """
46
+ return self._headers
47
+
48
+
49
+ OnEndCallbackFn: TypeAlias = Callable[[Optional[pyarrow.ArrowException]], Any]
50
+
51
+
52
+ class CallFinalizer(pyarrow.flight.ServerMiddleware):
53
+ """
54
+ Call Finalizer middleware can be used by method implementations to register
55
+ functions that should be called after the entire Flight RPC call finishes.
56
+
57
+ This may be important especially for the DoGet and DoExchange methods where the
58
+ method implementation prepares the stream and hands it over to PyArrow's Flight RPC.
59
+
60
+ It is often the case that the stream is backed by data which has its lifecycle
61
+ managed by the server. In these cases, the server needs to know the data is actually not
62
+ used anymore (so that it can release locks, free up the data or do whatever it needs
63
+ to do).
64
+ """
65
+
66
+ MiddlewareName = "call_finalizer"
67
+
68
+ def __init__(self) -> None:
69
+ super().__init__()
70
+
71
+ self._on_end: list[OnEndCallbackFn] = []
72
+
73
+ def register_on_end(self, fun: OnEndCallbackFn) -> None:
74
+ """
75
+ Register a function that should be called once the call completes. It is possible to call this
76
+ multiple times and register multiple functions.
77
+
78
+ IMPORTANT: the function that you register here should be quick, do minimal blocking and have low chance
79
+ of hanging. The function will be called out from the gRPC server's thread; if the call hangs, server's thread
80
+ will be blocked.
81
+
82
+ :param fun: function to register, it will be called with one argument: exception,
83
+ which is either None on success or pyarrow.ArrowException on failure
84
+ :return: nothing
85
+ """
86
+ self._on_end.append(fun)
87
+
88
+ def call_completed(self, exception: Optional[pyarrow.lib.ArrowException]) -> None:
89
+ try:
90
+ for fun in self._on_end:
91
+ fun(exception)
92
+ except Exception:
93
+ _LOGGER.critical("call_finalization_failed", exc_info=True)
94
+
95
+
96
+ class OtelMiddleware(pyarrow.flight.ServerMiddleware):
97
+ MiddlewareName = "otel_middleware"
98
+
99
+ def __init__(
100
+ self, info: pyarrow.flight.CallInfo, headers: dict[str, list[str]], extract_context: bool = False
101
+ ) -> None:
102
+ super().__init__()
103
+ method_name = info.method.name
104
+
105
+ if extract_context:
106
+ self._otel_ctx = otelpropagate.extract(headers)
107
+ else:
108
+ self._otel_ctx = otelctx.get_current()
109
+
110
+ self._otel_span = SERVER_TRACER.start_span(
111
+ f"{method_name}",
112
+ kind=SpanKind.SERVER,
113
+ context=self._otel_ctx,
114
+ attributes={
115
+ SpanAttributes.RPC_SYSTEM: "grpc",
116
+ SpanAttributes.RPC_SERVICE: "FlightRPC",
117
+ SpanAttributes.RPC_METHOD: method_name,
118
+ },
119
+ )
120
+
121
+ # note: the code does not set context / use span at this point because
122
+ # the middleware creation is done in a separate call, before the actual
123
+ # method invocation.
124
+ #
125
+ # the context has to be set & span used during the actual Flight RPC
126
+ # method handling
127
+
128
+ @property
129
+ def call_tracing(self) -> tuple[otelctx.Context, trace.Span]:
130
+ """
131
+ :return: tracing context & span for the current call
132
+ """
133
+ return self._otel_ctx, self._otel_span
134
+
135
+ def call_completed(self, exception: Optional[pyarrow.lib.ArrowException]) -> None:
136
+ # OpenTelemetry context/span restore;
137
+ #
138
+ # this has to happen because this method is done from thread managed
139
+ # by gRPC server / Flight & during a separate call after the request handling
140
+ # completes - any context juggling done previously is lost.
141
+ #
142
+ # - attach context extracted over the wire (see constructor)
143
+ # - use current method call's span (created during middleware init)
144
+ #
145
+ # code can then complete the span accordingly and finally detach the context
146
+ old_ctx = otelctx.attach(self._otel_ctx)
147
+ try:
148
+ with use_span(
149
+ self._otel_span,
150
+ end_on_exit=False,
151
+ record_exception=False,
152
+ set_status_on_exception=False,
153
+ ):
154
+ if exception is None:
155
+ self._otel_span.set_status(StatusCode.OK)
156
+ else:
157
+ self._otel_span.set_status(StatusCode.ERROR)
158
+ self._otel_span.record_exception(exception)
159
+
160
+ self._otel_span.end()
161
+ finally:
162
+ otelctx.detach(old_ctx)
@@ -0,0 +1,228 @@
1
+ # (C) 2024 GoodData Corporation
2
+ """
3
+ This is a thin wrapper around the PyArrow FlightServerBase. It exists to provide a typed interface.
4
+
5
+ There are two main pieces:
6
+
7
+ - FlightServer - this is an extension of pyarrow.flight.FlightServerBase; the sole purpose here is to decouple
8
+ implementation of the technical parts and the actual handling of Flight RPC Methods
9
+ - FlightServerMethods - base class containing typed definitions of all Flight RPC Methods
10
+ """
11
+
12
+ from collections.abc import Generator
13
+ from typing import Any, Callable, Optional, TypeVar, Union
14
+
15
+ import opentelemetry.context as otelctx
16
+ import opentelemetry.trace as oteltrace
17
+ import pyarrow.flight
18
+ from typing_extensions import Concatenate, ParamSpec, TypeAlias
19
+
20
+ from gooddata_flight_server.server.flight_rpc.flight_middleware import OtelMiddleware
21
+ from gooddata_flight_server.server.flight_rpc.server_methods import (
22
+ FlightServerMethods,
23
+ )
24
+
25
+ FlightServerLocation: TypeAlias = Union[str, bytes, Optional[tuple[str, int]], pyarrow.flight.Location]
26
+ FlightTlsCertificates: TypeAlias = list[tuple[bytes, bytes]]
27
+ FlightMiddlewares: TypeAlias = dict[str, pyarrow.flight.ServerMiddlewareFactory]
28
+
29
+ T = TypeVar("T")
30
+ P = ParamSpec("P")
31
+
32
+
33
+ def rpc_decorator() -> Callable[
34
+ [Callable[Concatenate[Any, pyarrow.flight.ServerCallContext, P], T]],
35
+ Callable[Concatenate[Any, pyarrow.flight.ServerCallContext, P], T],
36
+ ]:
37
+ def _factory(
38
+ fun: Callable[Concatenate[Any, pyarrow.flight.ServerCallContext, P], T],
39
+ ) -> Callable[Concatenate[Any, pyarrow.flight.ServerCallContext, P], T]:
40
+ def _decorator(
41
+ self: Any,
42
+ context: pyarrow.flight.ServerCallContext,
43
+ *args: P.args,
44
+ **kwargs: P.kwargs,
45
+ ) -> T:
46
+ otel_middleware = context.get_middleware(OtelMiddleware.MiddlewareName)
47
+ if otel_middleware is not None:
48
+ otel_ctx, otel_span = otel_middleware.call_tracing
49
+ else:
50
+ otel_ctx = otelctx.get_current()
51
+ otel_span = oteltrace.INVALID_SPAN
52
+
53
+ old_otel_ctx = otelctx.attach(otel_ctx)
54
+ try:
55
+ with oteltrace.use_span(
56
+ otel_span,
57
+ end_on_exit=False,
58
+ record_exception=False,
59
+ set_status_on_exception=False,
60
+ ):
61
+ return fun(self, context, *args, **kwargs)
62
+ finally:
63
+ otelctx.detach(old_otel_ctx)
64
+
65
+ return _decorator
66
+
67
+ return _factory
68
+
69
+
70
+ class FlightServer(pyarrow.flight.FlightServerBase):
71
+ """
72
+ Creates AND starts the Flight RPC Server. To handle the RPC methods, the server will call out to the
73
+ `methods` object. The `methods` object can be switched from outside, thus allowing caller code to switch
74
+ implementation of the RPC methods at its own discretion. Typical use case for this is to keep returning
75
+ `UNAVAILABLE` until the entire server starts, then switch to the real implementation.
76
+
77
+ Please note, that the Flight RPC Server actually starts as soon as this class is created. The `serve` and
78
+ `serve_with_signals` are merely a synchronization mechanism to wait until the server gets stopped.
79
+
80
+ When managing the lifecycle of the Flight RPC from outside, it is better not to use the serve() methods.
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ methods: Optional[FlightServerMethods] = None,
86
+ location: Optional[FlightServerLocation] = None,
87
+ auth_handler: Optional[pyarrow.flight.ServerAuthHandler] = None,
88
+ tls_certificates: Optional[FlightTlsCertificates] = None,
89
+ verify_client: Optional[bool] = None,
90
+ root_certificates: Optional[bytes] = None,
91
+ middleware: Optional[FlightMiddlewares] = None,
92
+ ):
93
+ """
94
+ Create Flight server
95
+
96
+ :param methods: implementation of Flight RPC methods to use; when None provided, will use implementation
97
+ that raises NotImplementedError for any method call
98
+ :param location : str, tuple or Location optional, default None
99
+ Location to serve on. Either a gRPC URI like `grpc://localhost:port`,
100
+ a tuple of (host, port) pair, or a Location instance.
101
+ If None is passed then the server will be started on localhost with a
102
+ system provided random port.
103
+ :param auth_handler : ServerAuthHandler optional, default None
104
+ An authentication mechanism to use. May be None.
105
+ :param tls_certificates : list optional, default None
106
+ A list of (certificate, key) pairs.
107
+ :param verify_client : boolean optional, default False
108
+ If True, then enable mutual TLS: require the client to present
109
+ a client certificate, and validate the certificate.
110
+ :param root_certificates : bytes optional, default None
111
+ If enabling mutual TLS, this specifies the PEM-encoded root
112
+ certificate used to validate client certificates.
113
+ :param middleware : list optional, default None
114
+ A dictionary of :class:`ServerMiddlewareFactory` items. The
115
+ keys are used to retrieve the middleware instance during calls
116
+ (see :meth:`ServerCallContext.get_middleware`).
117
+ """
118
+ super().__init__(
119
+ location=location,
120
+ auth_handler=auth_handler,
121
+ tls_certificates=tls_certificates,
122
+ verify_client=verify_client,
123
+ root_certificates=root_certificates,
124
+ middleware=middleware,
125
+ )
126
+
127
+ self._methods = methods or FlightServerMethods()
128
+
129
+ def switch_methods(self, methods: FlightServerMethods) -> None:
130
+ """
131
+ Switch methods used to handle Flight RPC requests. New requests will be handled using these methods.
132
+
133
+ :param methods: new set of methods to use
134
+ :return:
135
+ """
136
+ self._methods = methods
137
+
138
+ def serve(self) -> None:
139
+ """
140
+ Serve waits until the underlying server has stopped. This blocks current thread.
141
+
142
+ Note that the underlying Flight RPC Service starts up immediately when this class is constructed so this is
143
+ really just a synchronization mechanism.
144
+
145
+ :return: nothing
146
+ """
147
+ self.wait()
148
+
149
+ def serve_with_signals(self) -> None:
150
+ """
151
+ Serve waits until the underlying server has stopped OR until INT or TERM signal.
152
+ This blocks current thread.
153
+
154
+ When signal is received, the server will shut down as soon as current requests are
155
+ done and this method will return.
156
+
157
+ Note that the underlying Flight RPC Service starts up immediately when this class is
158
+ constructed so this is really just a synchronization mechanism.
159
+
160
+ :return: nothing
161
+ """
162
+ return self.serve()
163
+
164
+ #
165
+ # Delegates to methods impl
166
+ #
167
+
168
+ @rpc_decorator()
169
+ def list_flights(
170
+ self, context: pyarrow.flight.ServerCallContext, criteria: bytes
171
+ ) -> Generator[pyarrow.flight.FlightInfo, None, None]:
172
+ return self._methods.list_flights(context, criteria)
173
+
174
+ @rpc_decorator()
175
+ def get_flight_info(
176
+ self,
177
+ context: pyarrow.flight.ServerCallContext,
178
+ descriptor: pyarrow.flight.FlightDescriptor,
179
+ ) -> pyarrow.flight.FlightInfo:
180
+ return self._methods.get_flight_info(context, descriptor)
181
+
182
+ @rpc_decorator()
183
+ def get_schema(
184
+ self,
185
+ context: pyarrow.flight.ServerCallContext,
186
+ descriptor: pyarrow.flight.FlightDescriptor,
187
+ ) -> pyarrow.flight.SchemaResult:
188
+ return self._methods.get_schema(context, descriptor)
189
+
190
+ @rpc_decorator()
191
+ def do_put(
192
+ self,
193
+ context: pyarrow.flight.ServerCallContext,
194
+ descriptor: pyarrow.flight.FlightDescriptor,
195
+ reader: pyarrow.flight.MetadataRecordBatchReader,
196
+ writer: pyarrow.flight.FlightMetadataWriter,
197
+ ) -> None:
198
+ return self._methods.do_put(context, descriptor, reader, writer)
199
+
200
+ @rpc_decorator()
201
+ def do_get(
202
+ self,
203
+ context: pyarrow.flight.ServerCallContext,
204
+ ticket: pyarrow.flight.Ticket,
205
+ ) -> pyarrow.flight.FlightDataStream:
206
+ return self._methods.do_get(context, ticket)
207
+
208
+ @rpc_decorator()
209
+ def do_exchange(
210
+ self,
211
+ context: pyarrow.flight.ServerCallContext,
212
+ descriptor: pyarrow.flight.FlightDescriptor,
213
+ reader: pyarrow.flight.MetadataRecordBatchReader,
214
+ writer: pyarrow.flight.MetadataRecordBatchWriter,
215
+ ) -> None:
216
+ return self._methods.do_exchange(context, descriptor, reader, writer)
217
+
218
+ @rpc_decorator()
219
+ def list_actions(self, context: pyarrow.flight.ServerCallContext) -> list[tuple[str, str]]:
220
+ return self._methods.list_actions(context)
221
+
222
+ @rpc_decorator()
223
+ def do_action(
224
+ self,
225
+ context: pyarrow.flight.ServerCallContext,
226
+ action: pyarrow.flight.Action,
227
+ ) -> Generator[pyarrow.flight.Result, None, None]:
228
+ return self._methods.do_action(context, action)
@@ -0,0 +1,279 @@
1
+ # (C) 2024 GoodData Corporation
2
+ #
3
+ # mypy: no-strict-optional
4
+
5
+ from threading import Thread
6
+ from typing import Optional
7
+
8
+ import pyarrow.flight
9
+ import structlog
10
+
11
+ from gooddata_flight_server.config.config import AuthenticationMethod, ServerConfig
12
+ from gooddata_flight_server.errors.error_code import ErrorCode
13
+ from gooddata_flight_server.errors.error_info import ErrorInfo
14
+ from gooddata_flight_server.server.auth.auth_middleware import TokenAuthMiddleware, TokenAuthMiddlewareFactory
15
+ from gooddata_flight_server.server.auth.token_verifier_factory import create_token_verification_strategy
16
+ from gooddata_flight_server.server.base import ServerContext
17
+ from gooddata_flight_server.server.flight_rpc.flight_middleware import (
18
+ CallFinalizer,
19
+ CallInfo,
20
+ OtelMiddleware,
21
+ )
22
+ from gooddata_flight_server.server.flight_rpc.flight_server import FlightServer
23
+ from gooddata_flight_server.server.flight_rpc.server_methods import (
24
+ FlightServerMethods,
25
+ )
26
+
27
+
28
+ def _get_flight_server_locations(config: ServerConfig) -> tuple[str, str]:
29
+ transport = "grpc+tls" if config.use_tls else "grpc"
30
+
31
+ return (
32
+ f"{transport}://{config.listen_host}:{config.listen_port}",
33
+ f"{transport}://{config.advertise_host}:{config.advertise_port}",
34
+ )
35
+
36
+
37
+ class _AvailabilityMiddlewareFactory(pyarrow.flight.ServerMiddlewareFactory):
38
+ """
39
+ Optionally rejects calls with FlightUnavailableError & some reason.
40
+
41
+ If unavailable_reason is set -> reject. Otherwise, let the request through.
42
+ """
43
+
44
+ def __init__(self, unavailable_reason: Optional[ErrorInfo] = None):
45
+ super().__init__()
46
+
47
+ self.unavailable_reason: Optional[ErrorInfo] = unavailable_reason
48
+
49
+ def start_call(
50
+ self, info: pyarrow.flight.CallInfo, headers: dict[str, list[str]]
51
+ ) -> Optional[pyarrow.flight.ServerMiddleware]:
52
+ if self.unavailable_reason is None:
53
+ return None
54
+
55
+ raise self.unavailable_reason.to_unavailable_error()
56
+
57
+
58
+ class _CallInfoMiddlewareFactory(pyarrow.flight.ServerMiddlewareFactory):
59
+ def start_call(
60
+ self, info: pyarrow.flight.CallInfo, headers: dict[str, list[str]]
61
+ ) -> Optional[pyarrow.flight.ServerMiddleware]:
62
+ return CallInfo(info, headers)
63
+
64
+
65
+ class _CallFinalizerMiddlewareFactory(pyarrow.flight.ServerMiddlewareFactory):
66
+ def start_call(
67
+ self, info: pyarrow.flight.CallInfo, headers: dict[str, list[str]]
68
+ ) -> Optional[pyarrow.flight.ServerMiddleware]:
69
+ return CallFinalizer()
70
+
71
+
72
+ class _OtelMiddlewareFactory(pyarrow.flight.ServerMiddlewareFactory):
73
+ def __init__(self, extract_context: bool) -> None:
74
+ super().__init__()
75
+ self._extract_context = extract_context
76
+
77
+ def start_call(
78
+ self, info: pyarrow.flight.CallInfo, headers: dict[str, list[str]]
79
+ ) -> Optional[pyarrow.flight.ServerMiddleware]:
80
+ return OtelMiddleware(info, headers, self._extract_context)
81
+
82
+
83
+ class FlightRpcService:
84
+ """
85
+ Service that exposes Flight RPC.
86
+
87
+ Handles Flight server start/stop & importantly also allows switching running server from
88
+ available to unavailable state; typical use case is to reject any RPC during startup/shutdown
89
+ of the server or loss of connection to the cluster.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ config: ServerConfig,
95
+ methods: FlightServerMethods = FlightServerMethods(),
96
+ ):
97
+ self._config = config
98
+ self._methods = methods
99
+
100
+ (
101
+ self._listen_url,
102
+ self._client_url,
103
+ ) = _get_flight_server_locations(config=config)
104
+
105
+ self._logger = structlog.get_logger("gooddata_flight_server.rpc")
106
+
107
+ self._availability = _AvailabilityMiddlewareFactory(
108
+ unavailable_reason=ErrorInfo.for_reason(ErrorCode.NOT_READY, "Try again later.")
109
+ )
110
+ # internal mutable state
111
+ # server starts immediately when constructed (PyArrow stuff); thus defer
112
+ # construction until start() is called
113
+ self._server: Optional[FlightServer] = None
114
+ self._flight_shutdown_thread: Optional[Thread] = None
115
+ self._stopped = False
116
+
117
+ def _initialize_authentication(
118
+ self, ctx: ServerContext
119
+ ) -> Optional[tuple[str, pyarrow.flight.ServerMiddlewareFactory]]:
120
+ if self._config.authentication_method == AuthenticationMethod.NoAuth:
121
+ if self._config.use_mutual_tls:
122
+ return None
123
+
124
+ if "127.0.0.1" not in self._config.listen_host and "localhost" not in self._config.listen_host:
125
+ print("!" * 72)
126
+ print("!!! Your server is configured without authentication and ")
127
+ print("!!! it seems it is listening on a non-loopback interface. ")
128
+ print("!!! The server may be reachable from public network. ")
129
+ print(f"!!! Listening on: {self._config.listen_host}. ")
130
+ print("!" * 72)
131
+
132
+ self._logger.warning("insecure_warning", listen_url=self._config.listen_host)
133
+
134
+ return None
135
+
136
+ verification = create_token_verification_strategy(ctx)
137
+
138
+ return TokenAuthMiddleware.MiddlewareName, TokenAuthMiddlewareFactory(
139
+ ctx.config.token_header_name, verification
140
+ )
141
+
142
+ def _initialize_otel_tracing(
143
+ self, ctx: ServerContext
144
+ ) -> Optional[tuple[str, pyarrow.flight.ServerMiddlewareFactory]]:
145
+ if self._config.otel_config.exporter_type is None:
146
+ return None
147
+
148
+ return OtelMiddleware.MiddlewareName, _OtelMiddlewareFactory(
149
+ self._config.otel_config.extract_context_from_headers
150
+ )
151
+
152
+ def start(self, ctx: ServerContext) -> None:
153
+ """
154
+ Starts the server. This will start the Flight RPC Server bound to configured host and port.
155
+ The server will be returning UNAVAILABLE for all methods until it is switched to serving.
156
+
157
+ See `switch_to_serving`.
158
+
159
+ :return: nothing
160
+ """
161
+ # massaging before sending these out to PyArrow
162
+ tls_certificates = (
163
+ [self._config.tls_cert_and_key]
164
+ if self._config.use_tls and self._config.tls_cert_and_key is not None
165
+ else None
166
+ )
167
+
168
+ self._logger.info(
169
+ "flight_service_start",
170
+ listen_url=self._listen_url,
171
+ client_url=self._client_url,
172
+ tls=tls_certificates is not None,
173
+ )
174
+
175
+ middleware = {
176
+ "_availability": self._availability,
177
+ CallInfo.MiddlewareName: _CallInfoMiddlewareFactory(),
178
+ CallFinalizer.MiddlewareName: _CallFinalizerMiddlewareFactory(),
179
+ }
180
+
181
+ auth_middleware = self._initialize_authentication(ctx)
182
+ if auth_middleware is not None:
183
+ middleware[auth_middleware[0]] = auth_middleware[1]
184
+
185
+ otel_middleware = self._initialize_otel_tracing(ctx)
186
+ if otel_middleware is not None:
187
+ middleware[otel_middleware[0]] = otel_middleware[1]
188
+
189
+ # server starts right as it is constructed
190
+ # the serve() method does not have to be called; moreover, it should not be called by the server
191
+ # as it makes PyArrow to install signal handlers that interfere with quiver server's handlers
192
+ #
193
+ # see: https://github.com/apache/arrow/issues/11932
194
+ self._server = FlightServer(
195
+ methods=self._methods,
196
+ location=self._listen_url,
197
+ tls_certificates=tls_certificates,
198
+ verify_client=self._config.use_mutual_tls,
199
+ root_certificates=self._config.tls_root_cert,
200
+ middleware=middleware,
201
+ )
202
+
203
+ def switch_to_serving(self, methods: FlightServerMethods) -> None:
204
+ """
205
+ Switches the Flight RPC server to serving mode.
206
+
207
+ :param methods: implementation of the Flight RPC methods
208
+ :return: nothing
209
+ """
210
+ if self._server is None:
211
+ raise AssertionError("Flight server was never started")
212
+
213
+ self._methods = methods
214
+ self._server.switch_methods(methods)
215
+ self._availability.unavailable_reason = None
216
+
217
+ def switch_to_unavailable(self, err: ErrorInfo) -> None:
218
+ """
219
+ Switches the Flight RPC server to unavailable mode. All new Flight RPC method
220
+ calls will be returning UNAVAILABLE from now on.
221
+
222
+ :param err: error info to include in the FlightUnavailableError
223
+ :return: nothing
224
+ """
225
+ if self._server is None:
226
+ raise AssertionError("Flight server was not started")
227
+
228
+ self._availability.unavailable_reason = err
229
+
230
+ def stop(self) -> None:
231
+ """
232
+ Stops service. This method will switch the Flight RPC server to return
233
+ 'UNAVAILABLE' for all following calls and then initiates shutdown of the server.
234
+
235
+ The shutdown is done asynchronously. Use `wait_for_stop` to sync for completion.
236
+ Since the server shutdown will wait until existing clients finish it may take longer.
237
+
238
+ :return:
239
+ """
240
+ if self._server is None:
241
+ raise AssertionError("Flight server was never started")
242
+
243
+ if self._flight_shutdown_thread is not None:
244
+ return
245
+
246
+ # do not handle any more new requests
247
+ self.switch_to_unavailable(ErrorInfo.for_reason(ErrorCode.SHUTTING_DOWN, "Server is shutting down."))
248
+
249
+ # trigger server shutdown in separate thread because shutdown blocks and violates the contract
250
+ # for quiver's long-running services
251
+ self._flight_shutdown_thread = Thread(
252
+ name="flight_service_shutdown",
253
+ daemon=True,
254
+ target=self._shutdown_server,
255
+ )
256
+ self._flight_shutdown_thread.start()
257
+
258
+ def _shutdown_server(self) -> None:
259
+ self._logger.info("flight_service_shutdown")
260
+ # this will block until server stops
261
+ self._server.shutdown()
262
+ self._logger.info("flight_service_finished")
263
+
264
+ def wait_for_stop(self, timeout: Optional[float] = None) -> bool:
265
+ if self._flight_shutdown_thread is None:
266
+ # this is really some mess in the caller code.. did not call stop() but tries to wait for it..
267
+ raise AssertionError("Flight server stop() was not issued yet attempting to wait for the server to stop.")
268
+
269
+ if self._flight_shutdown_thread.is_alive():
270
+ self._flight_shutdown_thread.join(timeout=timeout)
271
+
272
+ return not self._flight_shutdown_thread.is_alive()
273
+
274
+ @property
275
+ def client_url(self) -> str:
276
+ """
277
+ :return: location URL to advertise to clients
278
+ """
279
+ return self._client_url