gooddata-flight-server 1.29.2.dev2__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 +230 -0
  26. gooddata_flight_server/server/flight_rpc/flight_service.py +281 -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.29.2.dev2.data/scripts/gooddata-flight-server +10 -0
  45. gooddata_flight_server-1.29.2.dev2.dist-info/LICENSE.txt +7 -0
  46. gooddata_flight_server-1.29.2.dev2.dist-info/METADATA +737 -0
  47. gooddata_flight_server-1.29.2.dev2.dist-info/RECORD +49 -0
  48. gooddata_flight_server-1.29.2.dev2.dist-info/WHEEL +5 -0
  49. gooddata_flight_server-1.29.2.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,200 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from collections.abc import Generator
3
+ from typing import Optional
4
+
5
+ import pyarrow.flight
6
+ import structlog
7
+
8
+ from gooddata_flight_server.errors.error_info import ErrorCode, ErrorInfo
9
+ from gooddata_flight_server.server.flight_rpc.flight_middleware import (
10
+ CallFinalizer,
11
+ CallInfo,
12
+ )
13
+ from gooddata_flight_server.tasks.task_executor import TaskExecutor
14
+ from gooddata_flight_server.tasks.task_result import FlightDataTaskResult
15
+
16
+ _LOGGER = structlog.get_logger("gooddata_flight_server.rpc")
17
+
18
+
19
+ class FlightServerMethods:
20
+ """
21
+ Base class for implementations of Flight RPC server methods. This class contains a couple of utility
22
+ methods that may be useful in subclasses.
23
+
24
+ Typings reverse-engineered from PyArrow's Cython code.
25
+ """
26
+
27
+ @staticmethod
28
+ def call_info_middleware(
29
+ from_context: pyarrow.flight.ServerCallContext,
30
+ ) -> CallInfo:
31
+ """
32
+ Utility method to obtain CallInfo middleware from the call context. The
33
+ CallInfo middleware can be used to access Flight's CallInfo AND all headers
34
+ that were passed during the call.
35
+
36
+ :param from_context: server call context
37
+ :return: middleware
38
+ """
39
+ mw = from_context.get_middleware(CallInfo.MiddlewareName)
40
+ assert isinstance(mw, CallInfo)
41
+
42
+ return mw
43
+
44
+ @staticmethod
45
+ def call_finalizer_middleware(
46
+ from_context: pyarrow.flight.ServerCallContext,
47
+ ) -> CallFinalizer:
48
+ """
49
+ Utility method to obtain CallFinalizer middleware from the call context. The
50
+ CallFinalizer middleware can be used to register functions that should be
51
+ called after the entire Flight RPC completes.
52
+
53
+ See CallFinalizer documentation for more details..
54
+
55
+ :param from_context: server call context
56
+ :return: middleware
57
+ """
58
+ mw = from_context.get_middleware(CallFinalizer.MiddlewareName)
59
+ assert isinstance(mw, CallFinalizer)
60
+
61
+ return mw
62
+
63
+ @staticmethod
64
+ def do_get_task_result(
65
+ context: pyarrow.flight.ServerCallContext, task_executor: TaskExecutor, task_id: str
66
+ ) -> pyarrow.flight.FlightDataStream:
67
+ """
68
+ Utility method that creates a FlightDataStream from a result of a task that was
69
+ previously executed. You typically want to use this in implementation of `do_get`.
70
+
71
+ This method ensures that once the data is sent out, all necessary locks it previously
72
+ acquired to protect the data are freed. Single-use results will be closed once they
73
+ are sent out. The method uses current's call finalizer middleware to accomplish this.
74
+
75
+ :param context: server call context
76
+ :param task_executor: task executor where the task run
77
+ :param task_id: task identifier
78
+ :return: FlightDataStream, can be returned as-is as result of do_get
79
+ """
80
+ try:
81
+ task_result = task_executor.wait_for_result(task_id)
82
+ if task_result is None:
83
+ raise ErrorInfo.for_reason(
84
+ ErrorCode.INVALID_TICKET,
85
+ f"Unable to serve data for task '{task_id}'. The task result is not present.",
86
+ ).to_user_error()
87
+
88
+ if task_result.error is not None:
89
+ raise task_result.error.as_flight_error()
90
+
91
+ if task_result.cancelled:
92
+ raise ErrorInfo.for_reason(
93
+ ErrorCode.COMMAND_CANCELLED,
94
+ f"FlexFun invocation was cancelled. Invocation task was: '{task_result.task_id}'.",
95
+ ).to_server_error()
96
+
97
+ result = task_result.result
98
+ if not isinstance(result, FlightDataTaskResult):
99
+ raise ErrorInfo.for_reason(
100
+ ErrorCode.INTERNAL_ERROR,
101
+ f"An internal error has occurred while attempting read result for '{task_id}'."
102
+ f"While the result exists, it is of an unexpected type: {type(result).__name__} ",
103
+ ).to_internal_error()
104
+
105
+ rlock, data = result.acquire_data()
106
+
107
+ def _on_end(_: Optional[pyarrow.ArrowException]) -> None:
108
+ """
109
+ Once the request that streams the data out is done, make sure
110
+ to release the read-lock. Single-use results are closed at
111
+ this point because the data cannot be read again anyway.
112
+ """
113
+ rlock.release()
114
+
115
+ if result.single_use_data:
116
+ # note: results with single-use data can only ever have one active
117
+ # reader (e.g. this one). since the rlock is now released the
118
+ # close will proceed without chance of being blocked
119
+ try:
120
+ result.close()
121
+ except Exception:
122
+ # log and sink these Exceptions - not much to do
123
+ _LOGGER.error("do_get_close_failed", exc_info=True)
124
+
125
+ finalizer = FlightServerMethods.call_finalizer_middleware(context)
126
+ finalizer.register_on_end(_on_end)
127
+
128
+ if isinstance(data, pyarrow.Table):
129
+ _LOGGER.info("do_get_table", task_id=task_id, num_rows=data.num_rows)
130
+
131
+ return pyarrow.flight.RecordBatchStream(data)
132
+ elif isinstance(data, pyarrow.RecordBatchReader):
133
+ _LOGGER.info("do_get_reader", task_id=task_id)
134
+
135
+ return pyarrow.flight.RecordBatchStream(data)
136
+
137
+ _LOGGER.info("do_get_generator", task_id=task_id)
138
+ return pyarrow.flight.GeneratorStream(data)
139
+ except Exception:
140
+ _LOGGER.error("do_get_failed", exc_info=True)
141
+ raise
142
+
143
+ ###################################################################
144
+ # Flight RPC methods - to be implemented as needed by
145
+ # subclasses.
146
+ ###################################################################
147
+
148
+ def list_flights(
149
+ self, context: pyarrow.flight.ServerCallContext, criteria: bytes
150
+ ) -> Generator[pyarrow.flight.FlightInfo, None, None]:
151
+ raise NotImplementedError
152
+
153
+ def get_flight_info(
154
+ self,
155
+ context: pyarrow.flight.ServerCallContext,
156
+ descriptor: pyarrow.flight.FlightDescriptor,
157
+ ) -> pyarrow.flight.FlightInfo:
158
+ raise NotImplementedError
159
+
160
+ def get_schema(
161
+ self,
162
+ context: pyarrow.flight.ServerCallContext,
163
+ descriptor: pyarrow.flight.FlightDescriptor,
164
+ ) -> pyarrow.flight.SchemaResult:
165
+ raise NotImplementedError
166
+
167
+ def do_put(
168
+ self,
169
+ context: pyarrow.flight.ServerCallContext,
170
+ descriptor: pyarrow.flight.FlightDescriptor,
171
+ reader: pyarrow.flight.MetadataRecordBatchReader,
172
+ writer: pyarrow.flight.FlightMetadataWriter,
173
+ ) -> None:
174
+ raise NotImplementedError
175
+
176
+ def do_get(
177
+ self,
178
+ context: pyarrow.flight.ServerCallContext,
179
+ ticket: pyarrow.flight.Ticket,
180
+ ) -> pyarrow.flight.FlightDataStream:
181
+ raise NotImplementedError
182
+
183
+ def do_exchange(
184
+ self,
185
+ context: pyarrow.flight.ServerCallContext,
186
+ descriptor: pyarrow.flight.FlightDescriptor,
187
+ reader: pyarrow.flight.MetadataRecordBatchReader,
188
+ writer: pyarrow.flight.MetadataRecordBatchWriter,
189
+ ) -> None:
190
+ raise NotImplementedError
191
+
192
+ def list_actions(self, context: pyarrow.flight.ServerCallContext) -> list[tuple[str, str]]:
193
+ raise NotImplementedError
194
+
195
+ def do_action(
196
+ self,
197
+ context: pyarrow.flight.ServerCallContext,
198
+ action: pyarrow.flight.Action,
199
+ ) -> Generator[pyarrow.flight.Result, None, None]:
200
+ raise NotImplementedError
@@ -0,0 +1,321 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import abc
3
+ import os
4
+ import platform
5
+ import signal
6
+ from abc import abstractmethod
7
+ from threading import Condition, Thread
8
+ from typing import Any, Optional
9
+
10
+ import pyarrow
11
+ import structlog
12
+ from prometheus_client import start_http_server
13
+
14
+ from gooddata_flight_server._version import __version__
15
+ from gooddata_flight_server.config.config import ServerConfig
16
+ from gooddata_flight_server.health.health_check_http_server import (
17
+ SERVER_MODULE_DEBUG_NAME,
18
+ HealthCheckHttpServer,
19
+ )
20
+ from gooddata_flight_server.health.server_health_monitor import (
21
+ ModuleHealthStatus,
22
+ ServerHealthMonitor,
23
+ )
24
+ from gooddata_flight_server.utils.otel_tracing import SERVER_TRACER
25
+
26
+ # DEV ONLY - heap usage debugger
27
+ #
28
+ # uncomment to turn on, then uncomment the dump done at the time of server stop()
29
+ # from guppy import hpy
30
+ # h = hpy()
31
+
32
+ DEFAULT_LOGGING_INI = os.path.join(os.path.dirname(__file__), "default.logging.ini")
33
+
34
+
35
+ class ServerBase(abc.ABC):
36
+ """
37
+ Server base class. Template class which takes care of the infrastructure and other boring
38
+ stuff and lets subclasses focus on just the value added services.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ config: ServerConfig,
44
+ ):
45
+ self._logger = structlog.get_logger("gooddata_flight_server.server")
46
+ self._config = config
47
+ self._health = ServerHealthMonitor(
48
+ trim_interval=config.malloc_trim_interval_sec,
49
+ )
50
+
51
+ # main server thread; this is responsible for starting all sub-services,
52
+ # don't want the main thread to be blocked
53
+ self._main_thread = Thread(
54
+ name="gooddata_flight_server.server",
55
+ target=self._server_main,
56
+ daemon=True,
57
+ )
58
+
59
+ # main server waits for this condition, once notified, it will stop or abort all sub-services
60
+ self._stop_cond = Condition()
61
+ # main server notifies on this condition once all sub-services are started
62
+ self._start_cond = Condition()
63
+ self._started = False
64
+ self._startup_interrupted: Optional[Exception] = None
65
+ self._stop = False
66
+ self._abort = False
67
+
68
+ #
69
+ # Server template
70
+ #
71
+
72
+ def _sig_handler(self, sig: Any, frame: Any) -> None:
73
+ self._logger.info("server_shutdown_initiated")
74
+ self.stop()
75
+
76
+ def _health_check_http_server_start(self) -> None:
77
+ """
78
+ If configured, start health checks HTTP server.
79
+
80
+ :return: nothing
81
+ """
82
+ if self._config.health_check_host is not None:
83
+ self._logger.debug(
84
+ "health_check_starting",
85
+ host=self._config.health_check_host,
86
+ port=self._config.health_check_port,
87
+ )
88
+
89
+ HealthCheckHttpServer(
90
+ host=self._config.health_check_host,
91
+ port=self._config.health_check_port,
92
+ server_health_monitor=self._health,
93
+ )
94
+
95
+ def _metrics_server_start(self) -> None:
96
+ """
97
+ If configured, start Prometheus metric server.
98
+
99
+ :return: nothing
100
+ """
101
+ if self._config.metrics_host is not None:
102
+ self._logger.debug(
103
+ "metrics_starting",
104
+ host=self._config.metrics_host,
105
+ port=self._config.metrics_port,
106
+ )
107
+
108
+ start_http_server(
109
+ addr=self._config.metrics_host,
110
+ port=self._config.metrics_port,
111
+ )
112
+
113
+ self._logger.info(
114
+ "metrics_started",
115
+ host=self._config.metrics_host,
116
+ port=self._config.metrics_port,
117
+ )
118
+
119
+ def _server_main(self) -> None:
120
+ self._logger.info(
121
+ "server_startup",
122
+ platform=platform.platform(terse=True),
123
+ python_version=platform.python_version(),
124
+ arrow_version=pyarrow.__version__,
125
+ server_version=__version__,
126
+ config=self._config.without_tls(),
127
+ )
128
+
129
+ try:
130
+ self._pre_startup()
131
+ except Exception as e:
132
+ self._startup_interrupted = e
133
+ return
134
+
135
+ with self._stop_cond:
136
+ if self._stop:
137
+ return
138
+
139
+ try:
140
+ self._metrics_server_start()
141
+ self._health_check_http_server_start()
142
+
143
+ self._startup_services()
144
+ self._health.set_module_status(SERVER_MODULE_DEBUG_NAME, ModuleHealthStatus.OK)
145
+
146
+ with self._start_cond:
147
+ self._started = True
148
+ self._start_cond.notify_all()
149
+
150
+ with self._stop_cond:
151
+ while not self._stop:
152
+ self._stop_cond.wait()
153
+
154
+ if not self._abort:
155
+ self._shutdown_services()
156
+ else:
157
+ self._abort_services()
158
+ except Exception as e:
159
+ self._startup_interrupted = e
160
+
161
+ # wake up anyone who may be waiting for the server to start
162
+ with self._start_cond:
163
+ self._start_cond.notify_all()
164
+
165
+ return
166
+ finally:
167
+ self._logger.info("server_main_finished")
168
+
169
+ #
170
+ # Context
171
+ #
172
+
173
+ @property
174
+ def logger(self) -> structlog.stdlib.BoundLogger:
175
+ """
176
+ :return: server's main logger
177
+ """
178
+ return self._logger
179
+
180
+ @property
181
+ def health(self) -> ServerHealthMonitor:
182
+ """
183
+ :return: server's health monitor, this is initialized from the very beginning of the server's life
184
+ """
185
+ return self._health
186
+
187
+ #
188
+ # Lifecycle
189
+ #
190
+ @SERVER_TRACER.start_as_current_span("server_start")
191
+ def start(self) -> None:
192
+ """
193
+ Starts the server. This spins of the main server thread where it all happens. The start() method
194
+ returns immediately.
195
+
196
+ :return: nothing
197
+ """
198
+ signal.signal(signal.SIGINT, self._sig_handler)
199
+ signal.signal(signal.SIGTERM, self._sig_handler)
200
+
201
+ self._main_thread.start()
202
+
203
+ def stop(self) -> None:
204
+ """
205
+ Gracefully stops the server. This method will not block; see wait_for_stop().
206
+
207
+ :return: nothing
208
+ """
209
+ # DEV ONLY - heap usage debugger; also uncomment the imports
210
+ # print(h.heap())
211
+
212
+ with self._stop_cond:
213
+ self._stop = True
214
+ self._stop_cond.notify_all()
215
+
216
+ def abort(self) -> None:
217
+ """
218
+ Triggers hard-stop of the server. This is typically done as a response to unrecoverable failure that
219
+ warrants for quick and dirty shutdown.
220
+
221
+ On hard stop, the server will only try to do bare minimum in an attempt to clean up bare essentials.
222
+
223
+ :return:
224
+ """
225
+ with self._stop_cond:
226
+ self._abort = True
227
+ self._stop = True
228
+ self._stop_cond.notify_all()
229
+
230
+ def aborted(self) -> bool:
231
+ """
232
+ :return: True if the server aborted due to some serious failure
233
+ """
234
+ return self._abort
235
+
236
+ def wait_for_start(self, timeout: Optional[float] = None) -> bool:
237
+ """
238
+ Waits until server and all its services are up and running.
239
+
240
+ :param timeout: time in fractions of seconds
241
+ :return: true if started, false if not
242
+ """
243
+ with self._start_cond:
244
+ completed = self._start_cond.wait_for(
245
+ lambda: self._started is True or self._startup_interrupted is not None,
246
+ timeout=timeout,
247
+ )
248
+ if not completed:
249
+ return False
250
+
251
+ if self._startup_interrupted is not None:
252
+ raise self._startup_interrupted
253
+
254
+ return True
255
+
256
+ def wait_for_stop(self, timeout: Optional[float] = None) -> bool:
257
+ """
258
+ Waits until the main server thread stops. If the server startup encountered error (and never started), then
259
+ that error will be raised.
260
+
261
+ :param timeout: time to wait
262
+ :return: True if the main server thread finished; false if it is still running
263
+ """
264
+ self._main_thread.join(timeout=timeout)
265
+
266
+ if self._startup_interrupted is not None:
267
+ self._logger.fatal("server_startup_interrupted", exc_info=self._startup_interrupted)
268
+
269
+ raise self._startup_interrupted
270
+
271
+ return not self._main_thread.is_alive()
272
+
273
+ #
274
+ # template methods to be implemented by subclasses
275
+ #
276
+
277
+ def _pre_startup(self) -> None:
278
+ """
279
+ Perform any work before the actual startup logic is initiated. This may gracefully interrupt the server
280
+ startup if you raise exception created using the `_make_interrupt_exc()` method.
281
+
282
+ :return: nothing
283
+ """
284
+
285
+ @abstractmethod
286
+ def _startup_services(self) -> None:
287
+ """
288
+ Create and start up any services that the concrete server type requires. After the method completes,
289
+ the server should be up and running, ready to serve clients.
290
+
291
+ If this method raises an exception, then the server start will be interrupted and the server will exit.
292
+ You may gracefully interrupt the server startup if you raise exception created using the `make_interrupt_exc()`
293
+ method.
294
+
295
+ :return: nothing
296
+ """
297
+ raise NotImplementedError
298
+
299
+ @abstractmethod
300
+ def _shutdown_services(self) -> None:
301
+ """
302
+ Gracefully shutdown any services that the concrete server type uses. This method should block until all
303
+ the necessary services are stopped.
304
+
305
+ :return: nothing
306
+ """
307
+ raise NotImplementedError
308
+
309
+ @abstractmethod
310
+ def _abort_services(self) -> None:
311
+ """
312
+ Abort services.
313
+
314
+ This method is called on unrecoverable errors when server just needs to shut down as soon as possible.
315
+ During abort, the server should try and do most important sanity (if any) and exit asap.
316
+
317
+ Attempting more complex and possibly blocking operations is not a good idea.
318
+
319
+ :return: nothing
320
+ """
321
+ raise NotImplementedError
@@ -0,0 +1,116 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from typing import Union
3
+
4
+ import pyarrow.flight
5
+ from dynaconf import Dynaconf
6
+
7
+ from gooddata_flight_server.config.config import ServerConfig, read_config
8
+ from gooddata_flight_server.exceptions import FlightMethodsModuleError
9
+ from gooddata_flight_server.server.base import FlightServerMethodsFactory, ServerContext
10
+ from gooddata_flight_server.server.flight_rpc.flight_service import FlightRpcService
11
+ from gooddata_flight_server.server.flight_rpc.server_methods import FlightServerMethods
12
+ from gooddata_flight_server.server.server_base import DEFAULT_LOGGING_INI, ServerBase
13
+ from gooddata_flight_server.tasks.task_executor import TaskExecutor
14
+ from gooddata_flight_server.tasks.thread_task_executor import ThreadTaskExecutor
15
+ from gooddata_flight_server.utils.logging import init_logging
16
+ from gooddata_flight_server.utils.otel_tracing import initialize_otel_tracing
17
+
18
+
19
+ class GoodDataFlightServer(ServerBase):
20
+ def __init__(
21
+ self,
22
+ settings: Dynaconf,
23
+ config: ServerConfig,
24
+ methods: Union[FlightServerMethods, FlightServerMethodsFactory],
25
+ ):
26
+ super().__init__(config)
27
+
28
+ self._settings = settings
29
+ self._methods = methods if isinstance(methods, FlightServerMethods) else None
30
+ self._methods_factory = methods if not isinstance(methods, FlightServerMethods) else None
31
+
32
+ self._flight_service = FlightRpcService(config=config)
33
+ self._location = pyarrow.flight.Location(self._flight_service.client_url)
34
+
35
+ # TODO: make metric prefix configurable
36
+ self._task_executor = ThreadTaskExecutor(
37
+ metric_prefix="gdfs",
38
+ task_threads=config.task_threads,
39
+ result_close_threads=config.task_close_threads,
40
+ keep_results_for=config.task_result_ttl_sec,
41
+ )
42
+
43
+ @property
44
+ def location(self) -> pyarrow.flight.Location:
45
+ """
46
+ Server's location - this should be sent in all infos returned via Flight RPC.
47
+
48
+ :return: location
49
+ """
50
+ return self._location
51
+
52
+ @property
53
+ def task_executor(self) -> TaskExecutor:
54
+ """
55
+
56
+ :return:
57
+ """
58
+ return self._task_executor
59
+
60
+ def _startup_services(self) -> None:
61
+ server_ctx = ServerContext(
62
+ settings=self._settings,
63
+ config=self._config,
64
+ location=self._location,
65
+ task_executor=self._task_executor,
66
+ health=self.health,
67
+ )
68
+
69
+ self._flight_service.start(server_ctx)
70
+
71
+ if self._methods_factory is not None:
72
+ self.logger.info("flight_service_init_methods")
73
+
74
+ try:
75
+ self._methods = self._methods_factory(server_ctx)
76
+ if not isinstance(self._methods, FlightServerMethods):
77
+ raise FlightMethodsModuleError(
78
+ f"The provided FlightMethodsFactory has a valid signature but returned an invalid result of type "
79
+ f"{type(self._methods)}. Make sure the factory function returns an instance of FlightServerMethods."
80
+ )
81
+ except Exception as e:
82
+ self.logger.critical("flight_service_init_failed", exc_info=e)
83
+ raise
84
+
85
+ assert self._methods is not None
86
+ self._flight_service.switch_to_serving(self._methods)
87
+
88
+ self.logger.info("rpc_enabled", methods=type(self._methods).__name__)
89
+
90
+ def _shutdown_services(self) -> None:
91
+ self._flight_service.stop()
92
+ self._flight_service.wait_for_stop()
93
+
94
+ def _abort_services(self) -> None:
95
+ self._flight_service.stop()
96
+
97
+
98
+ def create_server(
99
+ methods: Union[FlightServerMethods, FlightServerMethodsFactory],
100
+ config_files: tuple[str, ...] = (),
101
+ logging_config: str = DEFAULT_LOGGING_INI,
102
+ dev_log: bool = True,
103
+ ) -> "GoodDataFlightServer":
104
+ settings, config = read_config(files=config_files)
105
+
106
+ init_logging(
107
+ logging_config,
108
+ dev_log=dev_log,
109
+ event_key=config.log_event_key_name,
110
+ trace_ctx_keys=config.log_trace_keys,
111
+ add_trace_ctx=config.otel_config.exporter_type is not None,
112
+ )
113
+
114
+ initialize_otel_tracing(config=config.otel_config)
115
+
116
+ return GoodDataFlightServer(settings=settings, config=config, methods=methods)
@@ -0,0 +1 @@
1
+ # (C) 2024 GoodData Corporation
@@ -0,0 +1,21 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from typing import Any, Union
3
+
4
+ import pyarrow
5
+ from typing_extensions import TypeAlias
6
+
7
+ # TODO: may be move to some more 'common' place
8
+ ArrowData: TypeAlias = Union[pyarrow.lib.Table, pyarrow.lib.RecordBatchReader]
9
+
10
+
11
+ class TaskWaitTimeoutError(TimeoutError):
12
+ """
13
+ This exception is thrown when TaskExecutor's wait_for_result() times out.
14
+
15
+ The exception includes task identifier and the payload that was used to
16
+ start the task.
17
+ """
18
+
19
+ def __init__(self, task_id: str, cmd: Any) -> None:
20
+ self.task_id = task_id
21
+ self.cmd = cmd