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 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
from typing import Any, Literal, Optional
|
|
3
|
+
|
|
4
|
+
from typing_extensions import TypeAlias
|
|
5
|
+
|
|
6
|
+
_CUSTOM_ERROR_START = 1000
|
|
7
|
+
_ERROR_CODE_MASK = 0x0000_FFFF
|
|
8
|
+
_ERROR_FLAGS_MASK = 0xFFFF_0000
|
|
9
|
+
|
|
10
|
+
_RETRY_HERE_FLAG = 0x0001_0000
|
|
11
|
+
"""
|
|
12
|
+
Request that failed should be retried on the same node where the call failed
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
_RETRY_OTHER_FLAG = 0x0002_0000
|
|
16
|
+
"""
|
|
17
|
+
Request that failed should be retried on other applicable nodes.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_RETRY_ANY_FLAG = 0x0003_0000
|
|
21
|
+
"""
|
|
22
|
+
Request that failed can be retried either on this node or any other applicable node.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _init_names(cls: Any) -> Any:
|
|
27
|
+
cls._init_names()
|
|
28
|
+
return cls
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_RetryFlagsLiteral: TypeAlias = Literal["any", "other", "here"]
|
|
32
|
+
_FlagsMapping: dict[_RetryFlagsLiteral, int] = {
|
|
33
|
+
"any": _RETRY_ANY_FLAG,
|
|
34
|
+
"here": _RETRY_HERE_FLAG,
|
|
35
|
+
"other": _RETRY_OTHER_FLAG,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_flags(retry: Optional[_RetryFlagsLiteral] = None) -> int:
|
|
40
|
+
if retry is None:
|
|
41
|
+
return 0x0
|
|
42
|
+
return _FlagsMapping.get(retry, 0x0)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _error_code(code: int, retry: Optional[_RetryFlagsLiteral] = None) -> int:
|
|
46
|
+
return _get_flags(retry) | code
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# TODO: revisit codes
|
|
50
|
+
@_init_names
|
|
51
|
+
class ErrorCode:
|
|
52
|
+
"""
|
|
53
|
+
Error codes for different failures that may occur while servicing requests.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
UNKNOWN = 0
|
|
57
|
+
"""
|
|
58
|
+
Unknown error. Probably something very bad happened on the server. The message and detail of the error should
|
|
59
|
+
contain diagnostic information.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
BAD_ARGUMENT = _error_code(1)
|
|
63
|
+
"""
|
|
64
|
+
User error: input received from the client failed server-side validations.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
BAD_REQUEST = _error_code(2)
|
|
68
|
+
"""
|
|
69
|
+
User error: client sent request to a node that just does not know how to deal with it at all.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
UNSUPPORTED_SERVICE = _error_code(3)
|
|
73
|
+
"""
|
|
74
|
+
User error: client wants to generate data using a service that is not running on the contacted server.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
INVALID_TICKET = _error_code(4)
|
|
78
|
+
"""
|
|
79
|
+
User error: client wants to get data using an invalid ticket.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
COMMAND_FAILED = _error_code(5)
|
|
83
|
+
"""
|
|
84
|
+
Service run into an error while performing the command.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
COMMAND_CANCELLED = _error_code(6)
|
|
88
|
+
"""
|
|
89
|
+
Command was cancelled by the user.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
COMMAND_CANCEL_NOT_POSSIBLE = _error_code(7)
|
|
93
|
+
"""
|
|
94
|
+
User wanted to cancel the command but it was not possible - probably because the
|
|
95
|
+
command already finished or is at a stage where it cannot be cancelled.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
COMMAND_RESULT_CONSUMED = _error_code(8)
|
|
99
|
+
"""
|
|
100
|
+
User wanted to cancel the command but it was not possible - probably because the
|
|
101
|
+
command already finished or is at a stage where it cannot be cancelled.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
TIMEOUT = _error_code(100)
|
|
105
|
+
"""
|
|
106
|
+
Request has timed out.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
POLL = _error_code(101, retry="here")
|
|
110
|
+
"""
|
|
111
|
+
Request has timed out but can be retried on the same node using the contents of RetryInfo,
|
|
112
|
+
which are serialized and stored in the error body.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
BACKPRESSURE = _error_code(102, retry="any")
|
|
116
|
+
"""
|
|
117
|
+
Server dropped the request as it is currently in state where it must exert backpressure.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
INTERNAL_ERROR = _error_code(500, retry="other")
|
|
121
|
+
"""
|
|
122
|
+
Internal server error.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
NOT_READY = _error_code(501, retry="any")
|
|
126
|
+
"""
|
|
127
|
+
Server is not yet ready to serve requests.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
SHUTTING_DOWN = _error_code(502, retry="other")
|
|
131
|
+
"""
|
|
132
|
+
Server is shutting down.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
NOT_IMPLEMENTED = _error_code(503)
|
|
136
|
+
"""
|
|
137
|
+
Functionality not implemented
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def name(cls, code: int) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Gets error name for the provided code. This may be used for debug purposes or when printing
|
|
144
|
+
somewhat more meaningful error messages to the end users.
|
|
145
|
+
|
|
146
|
+
:param code: error code
|
|
147
|
+
:return: error name
|
|
148
|
+
"""
|
|
149
|
+
if code < 0:
|
|
150
|
+
return f"unknown ({code})"
|
|
151
|
+
|
|
152
|
+
status = code & _ERROR_CODE_MASK
|
|
153
|
+
res = cls._GENERATED_NAMES.get(status, "unknown") # type: ignore
|
|
154
|
+
|
|
155
|
+
if status >= _CUSTOM_ERROR_START:
|
|
156
|
+
res = "custom"
|
|
157
|
+
|
|
158
|
+
return f"{res} ({status})"
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def is_retry_here(cls, code: int) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
:param code: error code
|
|
164
|
+
:return: true if the request can be retried either on the node where it originally failed
|
|
165
|
+
or on any other applicable node
|
|
166
|
+
"""
|
|
167
|
+
return code & _RETRY_HERE_FLAG == _RETRY_HERE_FLAG
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def is_retry_other(cls, code: int) -> bool:
|
|
171
|
+
"""
|
|
172
|
+
:param code: error code
|
|
173
|
+
:return: true if the request can be retried, but only on other nodes - e.g. not on the same
|
|
174
|
+
node where it just failed
|
|
175
|
+
"""
|
|
176
|
+
return code & _RETRY_OTHER_FLAG == _RETRY_OTHER_FLAG
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def is_retry_any(cls, code: int) -> bool:
|
|
180
|
+
"""
|
|
181
|
+
:param code: error code
|
|
182
|
+
:return: true if the request can be retried on any node; both the original node where the
|
|
183
|
+
call failed and the other applicable are valid candidates
|
|
184
|
+
"""
|
|
185
|
+
return code & _RETRY_ANY_FLAG == _RETRY_ANY_FLAG
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def is_retryable(cls, code: int) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
:param code: error code
|
|
191
|
+
:return: true if the request can be retried - either on the node where it originally failed
|
|
192
|
+
or on other applicable nodes
|
|
193
|
+
"""
|
|
194
|
+
return code & _RETRY_ANY_FLAG > 0
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def _init_names(cls) -> None:
|
|
198
|
+
names = {}
|
|
199
|
+
codes = {}
|
|
200
|
+
|
|
201
|
+
for key in dir(cls):
|
|
202
|
+
value = getattr(cls, key)
|
|
203
|
+
if isinstance(value, int) and not key.startswith("_"):
|
|
204
|
+
status = value & _ERROR_CODE_MASK
|
|
205
|
+
names[status] = key.lower()
|
|
206
|
+
codes[key] = value
|
|
207
|
+
|
|
208
|
+
setattr(cls, "_GENERATED_NAMES", names)
|
|
209
|
+
setattr(cls, "_CODE_MAPPING", codes)
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
import base64
|
|
3
|
+
import traceback
|
|
4
|
+
from typing import Callable, Optional, Union
|
|
5
|
+
|
|
6
|
+
import orjson
|
|
7
|
+
import pyarrow.flight
|
|
8
|
+
|
|
9
|
+
from gooddata_flight_server.errors.error_code import ErrorCode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ErrorInfo:
|
|
13
|
+
"""
|
|
14
|
+
Error info that should be used to construct Flight RPC errors. Never create Flight RPC Errors directly, instead use
|
|
15
|
+
this class to construct a detailed error and then use one of the to_*_error() factories.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
msg: str,
|
|
21
|
+
detail: Optional[str] = None,
|
|
22
|
+
body: Optional[bytes] = None,
|
|
23
|
+
code: int = 0,
|
|
24
|
+
) -> None:
|
|
25
|
+
self._msg = msg
|
|
26
|
+
self._detail: Optional[str] = detail
|
|
27
|
+
self._body: Optional[bytes] = body
|
|
28
|
+
self._code: int = code
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def msg(self) -> str:
|
|
32
|
+
"""
|
|
33
|
+
:return: human readable error message
|
|
34
|
+
"""
|
|
35
|
+
return self._msg
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def code(self) -> int:
|
|
39
|
+
"""
|
|
40
|
+
:return: error code for categorization; see ErrorCode for more
|
|
41
|
+
"""
|
|
42
|
+
return self._code
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def detail(self) -> Optional[str]:
|
|
46
|
+
"""
|
|
47
|
+
:return: a human-readable error detail; if included may help with error diagnostics
|
|
48
|
+
"""
|
|
49
|
+
return self._detail
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def body(self) -> Optional[bytes]:
|
|
53
|
+
"""
|
|
54
|
+
:return: error body, suitable for programmatic consumption; used by server to send structured information
|
|
55
|
+
which the client code may want to work with
|
|
56
|
+
"""
|
|
57
|
+
return self._body
|
|
58
|
+
|
|
59
|
+
def with_msg(self, msg: str) -> "ErrorInfo":
|
|
60
|
+
"""
|
|
61
|
+
Updates error message.
|
|
62
|
+
|
|
63
|
+
:param msg: new message
|
|
64
|
+
:return: self, for call chaining sakes
|
|
65
|
+
"""
|
|
66
|
+
self._msg = msg
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def with_detail(self, detail: Optional[str] = None) -> "ErrorInfo":
|
|
70
|
+
"""
|
|
71
|
+
Updates or resets the error detail.
|
|
72
|
+
|
|
73
|
+
:param detail: detail to set; if None, the detail stored in the meta will be removed; default is None
|
|
74
|
+
:return: self, for call chaining sakes
|
|
75
|
+
"""
|
|
76
|
+
self._detail = detail
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def with_body(self, body: Optional[Union[bytes, str]]) -> "ErrorInfo":
|
|
80
|
+
"""
|
|
81
|
+
Updates or resets the error body.
|
|
82
|
+
|
|
83
|
+
:param body: body to set; if None, the body stored in the meta will be removed; default is None
|
|
84
|
+
:return: self, for call chaining sakes
|
|
85
|
+
"""
|
|
86
|
+
if isinstance(body, str):
|
|
87
|
+
self._body = body.encode("utf-8")
|
|
88
|
+
else:
|
|
89
|
+
self._body = body
|
|
90
|
+
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def with_code(self, code: int = 0) -> "ErrorInfo":
|
|
94
|
+
"""
|
|
95
|
+
Updates or resets the error code.
|
|
96
|
+
|
|
97
|
+
Resetting error code means setting the code to 0 - the error code for unknown errors.
|
|
98
|
+
|
|
99
|
+
:param code: code to set; default is 0
|
|
100
|
+
:return: self, for call chaining sakes
|
|
101
|
+
"""
|
|
102
|
+
self._code = code
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def to_bytes(self) -> bytes:
|
|
106
|
+
"""
|
|
107
|
+
:return: binary representation of the meta; this can be sent over the wire (for instance in extra_info)
|
|
108
|
+
"""
|
|
109
|
+
_json = {
|
|
110
|
+
"msg": self._msg,
|
|
111
|
+
"detail": self._detail,
|
|
112
|
+
"code": self._code,
|
|
113
|
+
"body": base64.b64encode(self._body).decode("ascii") if self._body else None,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return orjson.dumps(_json)
|
|
117
|
+
|
|
118
|
+
def to_user_error(self) -> pyarrow.flight.FlightError:
|
|
119
|
+
"""
|
|
120
|
+
Returns this error meta wrapped into a flight error that communicates failure due to bad user input.
|
|
121
|
+
|
|
122
|
+
Since the Flight RPC does not have a dedicated user error, this method creates a FlightServerError.
|
|
123
|
+
For a long time the code was creating FlightCancelledError - which somewhat fits (server cancels
|
|
124
|
+
request because it is wrong) - it is saner to only use FlightCancelledError for the actual
|
|
125
|
+
cancellation as it is described by Flight RPC itself.
|
|
126
|
+
|
|
127
|
+
:return: this error meta wrapped into a Flight Error that communicates user error
|
|
128
|
+
"""
|
|
129
|
+
return pyarrow.flight.FlightServerError(self.msg, extra_info=self.to_bytes())
|
|
130
|
+
|
|
131
|
+
def to_server_error(self) -> pyarrow.flight.FlightServerError:
|
|
132
|
+
"""
|
|
133
|
+
:return: this error meta wrapped into a FlightServerError
|
|
134
|
+
"""
|
|
135
|
+
return pyarrow.flight.FlightServerError(self.msg, extra_info=self.to_bytes())
|
|
136
|
+
|
|
137
|
+
def to_timeout_error(self) -> pyarrow.flight.FlightTimedOutError:
|
|
138
|
+
"""
|
|
139
|
+
:return: this error meta wrapped into a FlightTimedOutError
|
|
140
|
+
"""
|
|
141
|
+
return pyarrow.flight.FlightTimedOutError(self.msg, extra_info=self.to_bytes())
|
|
142
|
+
|
|
143
|
+
def to_internal_error(self) -> pyarrow.flight.FlightInternalError:
|
|
144
|
+
"""
|
|
145
|
+
:return: this error meta wrapped into a FlightInternalError
|
|
146
|
+
"""
|
|
147
|
+
return pyarrow.flight.FlightInternalError(self.msg, extra_info=self.to_bytes())
|
|
148
|
+
|
|
149
|
+
def to_cancelled_error(self) -> pyarrow.flight.FlightCancelledError:
|
|
150
|
+
"""
|
|
151
|
+
:return: this error meta wrapped into a FlightCancelledError
|
|
152
|
+
"""
|
|
153
|
+
return pyarrow.flight.FlightCancelledError(self.msg, extra_info=self.to_bytes())
|
|
154
|
+
|
|
155
|
+
def to_unavailable_error(self) -> pyarrow.flight.FlightUnavailableError:
|
|
156
|
+
"""
|
|
157
|
+
:return: this error meta wrapped into a FlightUnavailableError
|
|
158
|
+
"""
|
|
159
|
+
return pyarrow.flight.FlightUnavailableError(self.msg, extra_info=self.to_bytes())
|
|
160
|
+
|
|
161
|
+
def to_unauthenticated_error(
|
|
162
|
+
self,
|
|
163
|
+
) -> pyarrow.flight.FlightUnauthenticatedError:
|
|
164
|
+
"""
|
|
165
|
+
:return: this error meta wrapped into a FlightUnauthenticatedError
|
|
166
|
+
"""
|
|
167
|
+
return pyarrow.flight.FlightUnauthenticatedError(self.msg, extra_info=self.to_bytes())
|
|
168
|
+
|
|
169
|
+
def to_unauthorized_error(self) -> pyarrow.flight.FlightUnauthorizedError:
|
|
170
|
+
"""
|
|
171
|
+
:return: this error meta wrapped into a FlightUnauthorizedError
|
|
172
|
+
"""
|
|
173
|
+
return pyarrow.flight.FlightUnauthorizedError(self.msg, extra_info=self.to_bytes())
|
|
174
|
+
|
|
175
|
+
def to_flight_error(
|
|
176
|
+
self,
|
|
177
|
+
error_factory: Callable[[str, Optional[bytes]], pyarrow.flight.FlightError],
|
|
178
|
+
) -> pyarrow.flight.FlightError:
|
|
179
|
+
"""
|
|
180
|
+
Uses the provided error factory - which can be for example the FlightError subclass, to create an
|
|
181
|
+
exception.
|
|
182
|
+
|
|
183
|
+
:param error_factory: factory to create FlightError
|
|
184
|
+
:return: new instance of error
|
|
185
|
+
"""
|
|
186
|
+
return error_factory(self.msg, self.to_bytes())
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def from_bytes(val: bytes) -> "ErrorInfo":
|
|
190
|
+
"""
|
|
191
|
+
Read error metadata from binary representation.
|
|
192
|
+
|
|
193
|
+
:param val: binary representation
|
|
194
|
+
:return: new error meta
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
_json = orjson.loads(val)
|
|
198
|
+
body: Optional[bytes] = base64.b64decode(_json["body"]) if _json.get("body") is not None else None
|
|
199
|
+
|
|
200
|
+
return ErrorInfo(
|
|
201
|
+
msg=_json.get("msg"),
|
|
202
|
+
detail=_json.get("detail"),
|
|
203
|
+
body=body,
|
|
204
|
+
code=_json.get("code"),
|
|
205
|
+
)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
raise ValueError(f"Unable to parser ErrorInfo from binary representation: '{str(e)}'.")
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def from_pyarrow_error(error: pyarrow.flight.FlightError) -> "ErrorInfo":
|
|
211
|
+
"""
|
|
212
|
+
Reads ErrorInfo that is serialized inside the provided PyArrows FlightError (extra_info).
|
|
213
|
+
If the extra error info is not included or is malformed, a placeholder with error code unknown will
|
|
214
|
+
be returned.
|
|
215
|
+
|
|
216
|
+
:param error: flight error
|
|
217
|
+
:return: new instance
|
|
218
|
+
"""
|
|
219
|
+
extra_info = error.extra_info
|
|
220
|
+
|
|
221
|
+
if extra_info is None or not len(extra_info):
|
|
222
|
+
return ErrorInfo(
|
|
223
|
+
msg=f"Call failed with error that does not contain ErrorInfo. The error message was: {str(error)}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
return ErrorInfo.from_bytes(extra_info)
|
|
228
|
+
except Exception:
|
|
229
|
+
return ErrorInfo(
|
|
230
|
+
msg=f"Call failed with error that does not contain ErrorInfo. The error message was: {str(error)}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def maybe_from_pyarrow_error(
|
|
235
|
+
error: pyarrow.flight.FlightError,
|
|
236
|
+
) -> Optional["ErrorInfo"]:
|
|
237
|
+
"""
|
|
238
|
+
Reads ErrorInfo that may be serialized inside the provided PyArrow's FlightError (in extra_info).
|
|
239
|
+
This method will return None if the error info is not present.
|
|
240
|
+
|
|
241
|
+
:param error: flight error
|
|
242
|
+
:return: new instance, None if the flight error info is not included
|
|
243
|
+
"""
|
|
244
|
+
extra_info = error.extra_info
|
|
245
|
+
|
|
246
|
+
if extra_info is None or not len(extra_info):
|
|
247
|
+
# careful: when there is no extra info available, PyArrow will return
|
|
248
|
+
# empty bytes. testing for both scenarios just in case
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
return ErrorInfo.from_bytes(extra_info)
|
|
253
|
+
except Exception:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def for_exc(
|
|
258
|
+
code: int,
|
|
259
|
+
e: BaseException,
|
|
260
|
+
extra_msg: Optional[str] = None,
|
|
261
|
+
include_traceback: bool = True,
|
|
262
|
+
) -> "ErrorInfo":
|
|
263
|
+
"""
|
|
264
|
+
A convenience factory which creates error meta as a response to some exception happening.
|
|
265
|
+
|
|
266
|
+
The meta msg will be set to message included in the exception; this can be optionally augmented
|
|
267
|
+
using extra_msg parameter. If extra_msg is present then:
|
|
268
|
+
|
|
269
|
+
- if the message in the exception is empty, the extra msg will be used as-is
|
|
270
|
+
- if the message in the exception is not empty, the extra msg will serve as prefix, or a lead in
|
|
271
|
+
and the resulting msg in meta will be in format "extra_msg: exception_msg"
|
|
272
|
+
|
|
273
|
+
Note: the above implies that it is better not to include trailing punctuation in the extra_msg.
|
|
274
|
+
|
|
275
|
+
The traceback from the exception will be included automatically - unless you pass the include_traceback
|
|
276
|
+
as False.
|
|
277
|
+
|
|
278
|
+
:param code: error code
|
|
279
|
+
:param e: exception that caused this whole unfortunate situation
|
|
280
|
+
:param extra_msg: extra message to using during construction of error msg (see method docs for more info)
|
|
281
|
+
:param include_traceback: whether to include exception traceback as the error detail; default is true
|
|
282
|
+
:return: new error meta
|
|
283
|
+
"""
|
|
284
|
+
msg = str(e)
|
|
285
|
+
if not len(msg) and extra_msg is not None:
|
|
286
|
+
msg = extra_msg + "."
|
|
287
|
+
elif len(msg) and extra_msg is not None:
|
|
288
|
+
msg = f"{extra_msg}: {msg}"
|
|
289
|
+
|
|
290
|
+
if include_traceback:
|
|
291
|
+
detail: Optional[str] = "".join(traceback.format_exception(None, e, e.__traceback__))
|
|
292
|
+
else:
|
|
293
|
+
detail = None
|
|
294
|
+
|
|
295
|
+
return ErrorInfo(msg=msg, detail=detail, code=code)
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def for_reason(code: int, msg: str) -> "ErrorInfo":
|
|
299
|
+
"""
|
|
300
|
+
A convenience factory which creates error meta that should be included in errors that fail
|
|
301
|
+
request for some arbitrary reason. For example server does input validation and finds
|
|
302
|
+
it incorrect.
|
|
303
|
+
|
|
304
|
+
:param code: error code
|
|
305
|
+
:param msg: error message, will be used as-is
|
|
306
|
+
:return: new error meta
|
|
307
|
+
"""
|
|
308
|
+
return ErrorInfo(msg=msg, code=code)
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def for_response(code: int, msg: str, body: bytes) -> "ErrorInfo":
|
|
312
|
+
"""
|
|
313
|
+
A convenience factory which creates error meta that should be included in errors for which
|
|
314
|
+
server wants to send some structured body to the client - so that client can inspect the body
|
|
315
|
+
and act accordingly.
|
|
316
|
+
|
|
317
|
+
For example client comes to get data with some ticket. The request times out and the server
|
|
318
|
+
wants to tell the client to retry the DoGet later but with a different ticket.
|
|
319
|
+
|
|
320
|
+
:param code: error code
|
|
321
|
+
:param msg: error message, will be used as-is
|
|
322
|
+
:param body: response body
|
|
323
|
+
:return: new error meta
|
|
324
|
+
"""
|
|
325
|
+
return ErrorInfo(msg=msg, body=body, code=code)
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def bad_argument(msg: str) -> pyarrow.flight.FlightError:
|
|
329
|
+
"""
|
|
330
|
+
A convenience factory that creates a flight error as a response to bad input received from the user.
|
|
331
|
+
|
|
332
|
+
:param msg: message to use as-is in the error
|
|
333
|
+
:return: an instance of FlightError - ready to raise
|
|
334
|
+
"""
|
|
335
|
+
return ErrorInfo(msg=msg, code=ErrorCode.BAD_ARGUMENT).to_user_error()
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def poll(
|
|
339
|
+
flight_info: Optional[pyarrow.flight.FlightInfo] = None,
|
|
340
|
+
retry_descriptor: Optional[pyarrow.flight.FlightDescriptor] = None,
|
|
341
|
+
cancel_descriptor: Optional[pyarrow.flight.FlightDescriptor] = None,
|
|
342
|
+
) -> pyarrow.flight.FlightTimedOutError:
|
|
343
|
+
"""
|
|
344
|
+
Convenience factory that creates FlightTimedOut error with POLL error code and `RetryInfo` which
|
|
345
|
+
includes the provided values.
|
|
346
|
+
|
|
347
|
+
:param flight_info: flight info available so far
|
|
348
|
+
:param retry_descriptor: descriptor to use for retry
|
|
349
|
+
:param cancel_descriptor: descriptor to use for cancellation
|
|
350
|
+
:return: new flight error - ready to raise
|
|
351
|
+
"""
|
|
352
|
+
retry_info = RetryInfo(
|
|
353
|
+
flight_info=flight_info,
|
|
354
|
+
retry_descriptor=retry_descriptor,
|
|
355
|
+
cancel_descriptor=cancel_descriptor,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return ErrorInfo(
|
|
359
|
+
msg="Work in progress. Retry.",
|
|
360
|
+
code=ErrorCode.POLL,
|
|
361
|
+
body=retry_info.to_bytes(),
|
|
362
|
+
).to_timeout_error()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class RetryInfo:
|
|
366
|
+
"""
|
|
367
|
+
This message is included in the body of ErrorInfo when the GetFlightInfo fails with
|
|
368
|
+
'POLL' error code.
|
|
369
|
+
|
|
370
|
+
This happens for long-running commands which are submitted via GetFlightInfo
|
|
371
|
+
and take arbitrary time to complete. Clients typically do not want to wait forever
|
|
372
|
+
and instead poll on the status of the work
|
|
373
|
+
|
|
374
|
+
It includes the information necessary for the client to either continue or cancel.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
def __init__(
|
|
378
|
+
self,
|
|
379
|
+
flight_info: Optional[pyarrow.flight.FlightInfo] = None,
|
|
380
|
+
retry_descriptor: Optional[pyarrow.flight.FlightDescriptor] = None,
|
|
381
|
+
cancel_descriptor: Optional[pyarrow.flight.FlightDescriptor] = None,
|
|
382
|
+
) -> None:
|
|
383
|
+
self._flight_info = flight_info
|
|
384
|
+
self._retry_descriptor = retry_descriptor
|
|
385
|
+
self._cancel_descriptor = cancel_descriptor
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def flight_info(self) -> Optional[pyarrow.flight.FlightInfo]:
|
|
389
|
+
"""
|
|
390
|
+
FlightInfo available at the time of the poll timeout. The information
|
|
391
|
+
may be incomplete. The full FlightInfo is built in cumulative fashion - the subsequent
|
|
392
|
+
retries will add more information as it becomes available.
|
|
393
|
+
|
|
394
|
+
Note: this is always full image of the FlightInfo available at the moment. It is
|
|
395
|
+
not a delta containing just the new information available since the last call.
|
|
396
|
+
|
|
397
|
+
:return: flight info
|
|
398
|
+
"""
|
|
399
|
+
return self._flight_info
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def retry_descriptor(self) -> Optional[pyarrow.flight.FlightDescriptor]:
|
|
403
|
+
"""
|
|
404
|
+
Returns descriptor that the client should use to retry the GetFlightInfo call
|
|
405
|
+
in order to see whether the command has completed.
|
|
406
|
+
|
|
407
|
+
Note: the retry descriptor will not be present if the command has completed.
|
|
408
|
+
|
|
409
|
+
:return: flight descriptor if retry is possible
|
|
410
|
+
"""
|
|
411
|
+
return self._retry_descriptor
|
|
412
|
+
|
|
413
|
+
@property
|
|
414
|
+
def cancel_descriptor(self) -> Optional[pyarrow.flight.FlightDescriptor]:
|
|
415
|
+
"""
|
|
416
|
+
Returns descriptor that the client can use to cancel the command that is
|
|
417
|
+
in progress.
|
|
418
|
+
|
|
419
|
+
Note: the retry descriptor will not be present if the command is not cancellable.
|
|
420
|
+
|
|
421
|
+
:return: flight descriptor if cancellation is possible
|
|
422
|
+
"""
|
|
423
|
+
return self._cancel_descriptor
|
|
424
|
+
|
|
425
|
+
def to_bytes(self) -> bytes:
|
|
426
|
+
"""
|
|
427
|
+
:return: binary representation of the retry info
|
|
428
|
+
"""
|
|
429
|
+
_json = {
|
|
430
|
+
"flight_info": base64.b64encode(self._flight_info.serialize()).decode("ascii")
|
|
431
|
+
if self._flight_info is not None
|
|
432
|
+
else None,
|
|
433
|
+
"retry_descriptor": base64.b64encode(self._retry_descriptor.serialize()).decode("ascii")
|
|
434
|
+
if self._retry_descriptor is not None
|
|
435
|
+
else None,
|
|
436
|
+
"cancel_descriptor": base64.b64encode(self._cancel_descriptor.serialize()).decode("ascii")
|
|
437
|
+
if self._cancel_descriptor is not None
|
|
438
|
+
else None,
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return orjson.dumps(_json)
|
|
442
|
+
|
|
443
|
+
@staticmethod
|
|
444
|
+
def from_bytes(val: bytes) -> "RetryInfo":
|
|
445
|
+
"""
|
|
446
|
+
Reads the retry info from binary representation, as received in the error body.
|
|
447
|
+
|
|
448
|
+
:param val: binary representation
|
|
449
|
+
:return: new instance of retry info
|
|
450
|
+
"""
|
|
451
|
+
_json = orjson.loads(val)
|
|
452
|
+
try:
|
|
453
|
+
flight_info = (
|
|
454
|
+
pyarrow.flight.FlightDescriptor.deserialize(base64.b64decode(_json["flight_info"]))
|
|
455
|
+
if _json.get("flight_info") is not None
|
|
456
|
+
else None
|
|
457
|
+
)
|
|
458
|
+
retry_descriptor = (
|
|
459
|
+
pyarrow.flight.FlightDescriptor.deserialize(base64.b64decode(_json["retry_descriptor"]))
|
|
460
|
+
if _json.get("retry_descriptor") is not None
|
|
461
|
+
else None
|
|
462
|
+
)
|
|
463
|
+
cancel_descriptor = (
|
|
464
|
+
pyarrow.flight.FlightDescriptor.deserialize(base64.b64decode(_json["cancel_descriptor"]))
|
|
465
|
+
if _json.get("cancel_descriptor") is not None
|
|
466
|
+
else None
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
return RetryInfo(
|
|
470
|
+
flight_info=flight_info,
|
|
471
|
+
retry_descriptor=retry_descriptor,
|
|
472
|
+
cancel_descriptor=cancel_descriptor,
|
|
473
|
+
)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
raise ValueError(f"Unable to decode retry info from binary representation: {str(e)}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# (C) 2024 GoodData Corporation
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ServerStartupInterrupted(Exception):
|
|
5
|
+
"""
|
|
6
|
+
This exception is thrown when server startup was interrupted due to unrecoverable condition. Different init and
|
|
7
|
+
preparation logic may raise this to stop the server startup. The message included in the exception will be
|
|
8
|
+
printed to stderr and server stops with exit code 1.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlightMethodsModuleError(ServerStartupInterrupted):
|
|
13
|
+
"""
|
|
14
|
+
This exception is thrown whenever there is an error related the flight methods module: loading the module, finding
|
|
15
|
+
the factory, validating the factory, etc.
|
|
16
|
+
"""
|