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,115 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import threading
3
+ from typing import Callable, TypeVar
4
+
5
+ from prometheus_client import Counter, Gauge, Summary
6
+ from prometheus_client.metrics import MetricWrapperBase
7
+
8
+ _TMetric = TypeVar("_TMetric", bound=MetricWrapperBase)
9
+
10
+
11
+ class TaskExecutorMetrics:
12
+ """
13
+ Facade to access prometheus metrics that the task executor maintains.
14
+
15
+ Note that this is somewhat more convoluted because:
16
+
17
+ 1. The TaskExecutor can produce metrics for various task types so metric names have to
18
+ be variable (based on prefix)
19
+
20
+ 2. Prometheus does not like double-registration of metrics - this is something that
21
+ definitely happens during various tests.
22
+
23
+ Thus, for each metric, the class maintains a static mapping (prefix -> actual instance) and
24
+ every time the class is instantiated with particular prefix, the constructor will get existing
25
+ or create new instances.
26
+ """
27
+
28
+ _QueueSize: dict[str, Gauge] = {}
29
+ _CloseQueueSize: dict[str, Gauge] = {}
30
+ _WaitTime: dict[str, Summary] = {}
31
+ _TaskE2EDuration: dict[str, Summary] = {}
32
+ _TaskDuration: dict[str, Summary] = {}
33
+ _TaskErrors: dict[str, Counter] = {}
34
+ _TaskCancelled: dict[str, Counter] = {}
35
+ _TaskCompleted: dict[str, Counter] = {}
36
+ _MapLock = threading.Lock()
37
+
38
+ @staticmethod
39
+ def _get_or_create(d: dict[str, _TMetric], prefix: str, create_fun: Callable[[], _TMetric]) -> _TMetric:
40
+ with TaskExecutorMetrics._MapLock:
41
+ existing = d.get(prefix)
42
+ if existing is not None:
43
+ return existing
44
+
45
+ new = create_fun()
46
+ d[prefix] = new
47
+ return new
48
+
49
+ def __init__(self, prefix: str) -> None:
50
+ self._prefix = prefix
51
+
52
+ self.queue_size = self._get_or_create(
53
+ TaskExecutorMetrics._QueueSize,
54
+ prefix,
55
+ lambda: Gauge(f"{prefix}_task_queue", "Number of tasks waiting in queue."),
56
+ )
57
+
58
+ self.close_queue_size = self._get_or_create(
59
+ TaskExecutorMetrics._CloseQueueSize,
60
+ prefix,
61
+ lambda: Gauge(
62
+ f"{prefix}_close_queue",
63
+ "Number of task execution results waiting in the queue to be closed and cleaned up.",
64
+ ),
65
+ )
66
+
67
+ self.wait_time = self._get_or_create(
68
+ TaskExecutorMetrics._WaitTime,
69
+ prefix,
70
+ lambda: Summary(
71
+ f"{prefix}_task_wait",
72
+ "Time a task spends waiting in queue before it is executed.",
73
+ ),
74
+ )
75
+
76
+ self.task_duration = self._get_or_create(
77
+ TaskExecutorMetrics._TaskDuration,
78
+ prefix,
79
+ lambda: Summary(
80
+ f"{prefix}_task_duration",
81
+ "Duration of task run itself (does not include wait or prerequisite resolution duration).",
82
+ ),
83
+ )
84
+
85
+ self.task_e2e_duration = self._get_or_create(
86
+ TaskExecutorMetrics._TaskE2EDuration,
87
+ prefix,
88
+ lambda: Summary(
89
+ f"{prefix}_task_e2e_duration",
90
+ "End-to-end duration of the task execution. Includes prerequisite resolution duration and "
91
+ "time spent in queue. This is the duration as observed by the callers.",
92
+ ),
93
+ )
94
+
95
+ self.task_errors = self._get_or_create(
96
+ TaskExecutorMetrics._TaskErrors,
97
+ prefix,
98
+ lambda: Counter(f"{prefix}_task_error", "Number of failed tasks."),
99
+ )
100
+
101
+ self.task_cancelled = self._get_or_create(
102
+ TaskExecutorMetrics._TaskCancelled,
103
+ prefix,
104
+ lambda: Counter(f"{prefix}_task_cancelled", "Number of cancelled tasks."),
105
+ )
106
+
107
+ self.task_completed = self._get_or_create(
108
+ TaskExecutorMetrics._TaskCompleted,
109
+ prefix,
110
+ lambda: Counter(
111
+ f"{prefix}_task_completed",
112
+ "Number of completed tasks - this includes all tasks regardless "
113
+ "of how their execution completed (success, failure, cancel).",
114
+ ),
115
+ )
@@ -0,0 +1,193 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import abc
3
+ import threading
4
+ import uuid
5
+ from concurrent.futures import CancelledError
6
+ from typing import Optional, Union, final
7
+
8
+ from gooddata_flight_server.tasks.task_error import TaskError
9
+ from gooddata_flight_server.tasks.task_result import TaskResult
10
+
11
+
12
+ class Task(abc.ABC):
13
+ """
14
+ Abstract base class for executable tasks.
15
+
16
+ This class provides the essential boilerplate and declares a single `run` method which
17
+ should be implemented by subclasses. The `run` does the actual work and returns its
18
+ result.
19
+
20
+ The Task design is such that it allows for runtime cancel-ability:
21
+
22
+ - task can be flagged as cancellable or not (default is True)
23
+ - when cancellable, the `cancel` method can be used to indicate that the task
24
+ should cancel
25
+ - this turns on the cancelled indicator
26
+
27
+ A cancellable task should test the cancelled indicator after each significant
28
+ step and bail-out by raising CancelledError
29
+
30
+ If the `run` method is entering a point of no return (e.g. cancel / rollback is
31
+ no longer feasible), then it must first switch the task to be non-cancellable
32
+ using the `switch_non_cancellable` - this may raise CancelledError if the `run`
33
+ was raced and someone cancelled the task.
34
+ """
35
+
36
+ __slots__ = (
37
+ "_task_id",
38
+ "_cmd",
39
+ "_cancel_lock",
40
+ "_cancelled",
41
+ "_cancellable",
42
+ "_triggers",
43
+ )
44
+
45
+ def __init__(
46
+ self,
47
+ cmd: bytes,
48
+ cancellable: bool = True,
49
+ task_id: Optional[str] = None,
50
+ ):
51
+ self._task_id = task_id or uuid.uuid4().hex
52
+ self._cmd = cmd
53
+ self._cancel_lock = threading.Lock()
54
+ self._cancelled = False
55
+ self._cancellable = cancellable
56
+
57
+ @final
58
+ @property
59
+ def task_id(self) -> str:
60
+ return self._task_id
61
+
62
+ @final
63
+ @property
64
+ def cmd(self) -> bytes:
65
+ return self._cmd
66
+
67
+ @final
68
+ @property
69
+ def cancelled(self) -> bool:
70
+ """
71
+ :return: true if the running task was cancelled
72
+ """
73
+ with self._cancel_lock:
74
+ return self._cancelled
75
+
76
+ def check_cancelled(self) -> None:
77
+ """
78
+ Checks whether task got cancelled - if so, raises CancelledError.
79
+
80
+ This is utility method that may be used in Task.run() to perform
81
+ cancellation checks.
82
+
83
+ :return: nothing
84
+ """
85
+ if self.cancelled:
86
+ raise CancelledError()
87
+
88
+ @final
89
+ def cancel(self) -> bool:
90
+ """
91
+ Try to cancel an *already running* task. Depending on the state of the task,
92
+ this may or may not be possible.
93
+
94
+ If the cancel succeeds, it is guaranteed that the task has no side-effects on
95
+ the rest of the system - it is as if it never run.
96
+
97
+ :return: True if cancel was successful, False if not
98
+ """
99
+ with self._cancel_lock:
100
+ if not self._cancellable:
101
+ return False
102
+
103
+ first_cancel = not self._cancelled
104
+ self._cancelled = True
105
+
106
+ if first_cancel:
107
+ try:
108
+ self.on_task_cancel()
109
+ except Exception:
110
+ pass
111
+
112
+ return True
113
+
114
+ @final
115
+ def switch_non_cancellable(self) -> None:
116
+ """
117
+ Switch the task to non-cancellable state.
118
+
119
+ If the task got cancelled, raises CancelledError() at this point.
120
+ Otherwise, sets the non-cancellable flag and returns.
121
+
122
+ :return: nothing
123
+ :raises: CancelledError if the switch is not possible because the task got cancelled already
124
+ """
125
+ with self._cancel_lock:
126
+ if self._cancelled:
127
+ raise CancelledError()
128
+
129
+ self._cancellable = False
130
+
131
+ def on_task_cancel(self) -> None:
132
+ """
133
+ This method will be called when a task is cancelled. That is, when it is still in
134
+ cancellable state and someone calls the cancel() for the first time.
135
+
136
+ The concrete implementation may optionally override this method to do something
137
+ special on cancellation - like cascading the cancellation to further sub-components.
138
+
139
+ Important: this method should not block.
140
+
141
+ :return: nothing
142
+ """
143
+ return
144
+
145
+ def on_task_error(self, error: TaskError) -> Optional[TaskError]:
146
+ """
147
+ This method will be called when a task fails with and raises an exception. It
148
+ will be called after executor creates an instance of TaskError from the
149
+ exception, and BEFORE it performs logging / tracking of the exception.
150
+
151
+ The concrete implementation may optionally override this method to do something
152
+ with the TaskError that was created by the executor. For example:
153
+
154
+ - intercept automatically generated TaskError and replace / modify it
155
+ (for example categorize client errors)
156
+
157
+ - do custom logging / tracking of the error
158
+
159
+ For convenience, if this method returns None, the executor will use the original
160
+ task error instance as-is.
161
+
162
+ :param error: TaskError as categorized by the executor
163
+ :return: None if the original `error` should be used, otherwise an instance of
164
+ TaskError
165
+ """
166
+ return error
167
+
168
+ @abc.abstractmethod
169
+ def run(self) -> Union[TaskResult, TaskError]:
170
+ """
171
+ Runs the task.
172
+
173
+ This method should be implemented by subclasses and do work according to payload
174
+ included in the `cmd`. Upon successful completion, the method should return a
175
+ TaskResult - either FlightPathTaskResult (when task produced a flight path) or
176
+ FlightDataTaskResult (when task created a live result).
177
+
178
+ Upon failure, the task has two options - use whichever is more convenient:
179
+
180
+ - either raise an exception: in this case the TaskExecutor will analyze and
181
+ convert the exception to TaskError (with error codes and everything) using
182
+ the built-in logic; the Task's `on_task_error` method will be called with
183
+ the TaskError created using the standard error handling logic
184
+
185
+ - return TaskError: this will be used by the TaskExecutor as-is. This option
186
+ is useful in situations when the task wants to do more elaborate
187
+ error handling / logging / reporting.
188
+
189
+ :return: result of the task
190
+ :raise Exception
191
+ :raise CancelledError: when the task's run was cancelled
192
+ """
193
+ raise NotImplementedError
@@ -0,0 +1,60 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import dataclasses
3
+ from dataclasses import dataclass
4
+ from typing import Callable, Optional
5
+
6
+ import pyarrow.flight
7
+
8
+ from gooddata_flight_server.errors.error_info import ErrorInfo
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class TaskError:
13
+ """
14
+ Detail about failed task execution. The original Exception that was raised and
15
+ failed the task is intentionally _not_ stored here.
16
+
17
+ That is because by storing the exception and the included traceback, the code
18
+ would also hold onto stack frames and all local variables bound to them - which
19
+ can in return hold onto _a lot_ of memory.
20
+
21
+ See: https://cosmicpercolator.com/2016/01/13/exception-leaks-in-python-2-and-3/
22
+ See also: https://github.com/apache/arrow/issues/36540
23
+
24
+ Also note, that clearing the traceback as hinted in above article helps somewhat
25
+ but is not ideal when working with FlightErrors. While testing and measuring, I
26
+ have found that even a freshly constructed FlightError (for example a freshly constructed
27
+ copy of the original exception's message + extra_info) has some non-trivial overhead.
28
+ Unsure why is that, and I'm not going to spend more time to investigate :)
29
+
30
+ Therefore, I have converged to this approach where the task error contains a
31
+ ErrorInfo (all essential detail) and a factory function to create the
32
+ actual FlightError.
33
+
34
+ The code that is supposed to raise the actual exception to the client will
35
+ instantiate the exception when needed.
36
+ """
37
+
38
+ error_info: ErrorInfo
39
+ error_factory: Callable[[str, Optional[bytes]], pyarrow.flight.FlightError]
40
+
41
+ client_error: bool = False
42
+ """
43
+ indicates whether the task failed because client provided invalid input.
44
+
45
+ this will be used purely for logging / tracking purposes. e.g. tasks failed due to client
46
+ providing bad input are logged as info and the task executor does not bump error counter
47
+ metrics
48
+ """
49
+
50
+ def as_flight_error(self) -> pyarrow.flight.FlightError:
51
+ """
52
+ :return: new instance of FlightError that should be raised
53
+ """
54
+ return self.error_info.to_flight_error(self.error_factory)
55
+
56
+ def to_client_error(self) -> "TaskError":
57
+ """
58
+ :return: creates a copy of this instance with `client_error` indicator set to True
59
+ """
60
+ return dataclasses.replace(self, client_error=True)
@@ -0,0 +1,96 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import abc
3
+ from typing import Optional
4
+
5
+ from gooddata_flight_server.tasks.task import Task
6
+ from gooddata_flight_server.tasks.task_result import TaskExecutionResult
7
+
8
+
9
+ class TaskAttributes:
10
+ TaskId = "gooddata_flight_server.task_id"
11
+ TaskCancelled = "gooddata_flight_server.task_cancelled"
12
+ TaskError = "gooddata_flight_server.task_error"
13
+ TaskErrorCode = "gooddata_flight_server.task_error.code"
14
+ TaskErrorMsg = "gooddata_flight_server.task_error.msg"
15
+ TaskErrorDetail = "gooddata_flight_server.task_error.detail"
16
+
17
+
18
+ class TaskExecutor(abc.ABC):
19
+ """
20
+ Declares interface for Task Executors. These allow asynchronous execution
21
+ of tasks which 'somehow' generate flight data.
22
+
23
+ The methods on this interface are designed to support a pollable
24
+ GetFlightInfo -> DoGet flows. A task can be submitted, polled for
25
+ completion or cancelled.
26
+
27
+ Once the task finishes (with any outcome), a TaskExecutionResult is available
28
+ and describes the outcome. On success, the execution result contains
29
+ a reference to task's actual result.
30
+
31
+ The execution result and the task's actual result are retained in the
32
+ executor for a limited (configurable) amount of time.
33
+
34
+ The actual task result is either:
35
+
36
+ - FlightDataTaskResult - which represents data that was 'generated' by the task
37
+ somehow and is available for single or repeated reads
38
+ - FlightPathTaskResult - which represent data stored under some flight path;
39
+ typically, flight commands include option to sink result under a flight path and
40
+ if that is the case the task will generate data and store it and then return
41
+ the pointer
42
+ """
43
+
44
+ @abc.abstractmethod
45
+ def submit(
46
+ self,
47
+ task: Task,
48
+ ) -> None:
49
+ """
50
+ Submit a new task that will perform all work as described in the provided command.
51
+
52
+ :param task: task to run
53
+ :return: nothing, task is always submitted
54
+ """
55
+ raise NotImplementedError
56
+
57
+ @abc.abstractmethod
58
+ def wait_for_result(self, task_id: str, timeout: Optional[float] = None) -> Optional[TaskExecutionResult]:
59
+ """
60
+ Wait for the task with the provided task id to finish.
61
+
62
+ If a task already finished and this service still has records describing it's result, then
63
+ the saved result is returned immediately.
64
+
65
+ Otherwise, if a task is pending, the method will block (optionally with timeout) until
66
+ the task completes. If it does not complete in given timeframe, the code will raise TimeoutError.
67
+
68
+ Note: if the task was cancelled, the result will have 'cancelled' indicator flag set to True.
69
+
70
+ :param task_id: task id to wait for
71
+ :param timeout: time to wait for completion
72
+ :raise TaskWaitTimeoutError: if the wait for task completion timed out
73
+ :return: result or None if there is no such task
74
+ """
75
+ raise NotImplementedError
76
+
77
+ @abc.abstractmethod
78
+ def cancel(self, task_id: str) -> bool:
79
+ """
80
+ Try to cancel a task - either by dropping it from the task queue or by cancelling
81
+ a running task or dropping a result of already finished task.
82
+
83
+ :param task_id: task id to cancel
84
+ :return: true if cancelled, false if cancel not possible (no such task or task not cancellable anymore)
85
+ """
86
+ raise NotImplementedError
87
+
88
+ @abc.abstractmethod
89
+ def close_result(self, task_id: str) -> bool:
90
+ """
91
+ Try to close result of a previously completed task.
92
+
93
+ :param task_id: task id, whose result to close
94
+ :return: true if result closed, false if no result for that task id
95
+ """
96
+ raise NotImplementedError