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.
- gooddata_flight_server/__init__.py +23 -0
- gooddata_flight_server/_version.py +7 -0
- gooddata_flight_server/cli.py +137 -0
- gooddata_flight_server/config/__init__.py +1 -0
- gooddata_flight_server/config/config.py +536 -0
- gooddata_flight_server/errors/__init__.py +1 -0
- gooddata_flight_server/errors/error_code.py +209 -0
- gooddata_flight_server/errors/error_info.py +475 -0
- gooddata_flight_server/exceptions.py +16 -0
- gooddata_flight_server/health/__init__.py +1 -0
- gooddata_flight_server/health/health_check_http_server.py +103 -0
- gooddata_flight_server/health/server_health_monitor.py +83 -0
- gooddata_flight_server/metrics.py +16 -0
- gooddata_flight_server/py.typed +1 -0
- gooddata_flight_server/server/__init__.py +1 -0
- gooddata_flight_server/server/auth/__init__.py +1 -0
- gooddata_flight_server/server/auth/auth_middleware.py +83 -0
- gooddata_flight_server/server/auth/token_verifier.py +62 -0
- gooddata_flight_server/server/auth/token_verifier_factory.py +55 -0
- gooddata_flight_server/server/auth/token_verifier_impl.py +41 -0
- gooddata_flight_server/server/base.py +63 -0
- gooddata_flight_server/server/default.logging.ini +28 -0
- gooddata_flight_server/server/flight_rpc/__init__.py +1 -0
- gooddata_flight_server/server/flight_rpc/flight_middleware.py +162 -0
- gooddata_flight_server/server/flight_rpc/flight_server.py +228 -0
- gooddata_flight_server/server/flight_rpc/flight_service.py +279 -0
- gooddata_flight_server/server/flight_rpc/server_methods.py +200 -0
- gooddata_flight_server/server/server_base.py +321 -0
- gooddata_flight_server/server/server_main.py +116 -0
- gooddata_flight_server/tasks/__init__.py +1 -0
- gooddata_flight_server/tasks/base.py +21 -0
- gooddata_flight_server/tasks/metrics.py +115 -0
- gooddata_flight_server/tasks/task.py +193 -0
- gooddata_flight_server/tasks/task_error.py +60 -0
- gooddata_flight_server/tasks/task_executor.py +96 -0
- gooddata_flight_server/tasks/task_result.py +363 -0
- gooddata_flight_server/tasks/temporal_container.py +247 -0
- gooddata_flight_server/tasks/thread_task_executor.py +639 -0
- gooddata_flight_server/utils/__init__.py +1 -0
- gooddata_flight_server/utils/libc_utils.py +35 -0
- gooddata_flight_server/utils/logging.py +158 -0
- gooddata_flight_server/utils/methods_discovery.py +98 -0
- gooddata_flight_server/utils/otel_tracing.py +142 -0
- gooddata_flight_server-1.34.1.dev1.data/scripts/gooddata-flight-server +10 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/LICENSE.txt +7 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/METADATA +749 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/RECORD +49 -0
- gooddata_flight_server-1.34.1.dev1.dist-info/WHEEL +5 -0
- gooddata_flight_server-1.34.1.dev1.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"FlexConnect function 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
|