qplotter 1.6.0__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.
- qplot/__init__.py +46 -0
- qplot/__main__.py +232 -0
- qplot/_auxiliary_paths.py +67 -0
- qplot/_build_info.json +1 -0
- qplot/_metadata.py +75 -0
- qplot/_repair.py +14 -0
- qplot/_shutdown_supervisor.py +4049 -0
- qplot/_version.py +12 -0
- qplot/_windows_shutdown_job.py +1166 -0
- qplot/configuration/__init__.py +3 -0
- qplot/configuration/config.py +578 -0
- qplot/configuration/config_schema.json +357 -0
- qplot/configuration/fonts.py +3 -0
- qplot/configuration/scripts.py +255 -0
- qplot/configuration/themes/__init__.py +13 -0
- qplot/configuration/themes/_base.py +592 -0
- qplot/configuration/themes/blank.py +17 -0
- qplot/configuration/themes/dark.py +54 -0
- qplot/configuration/themes/light.py +54 -0
- qplot/datahandling/LoadFromDB.py +224 -0
- qplot/datahandling/__init__.py +47 -0
- qplot/datahandling/_trusted_live_helper.py +812 -0
- qplot/datahandling/_trusted_live_protocol.py +1745 -0
- qplot/datahandling/database.py +2340 -0
- qplot/datahandling/dimensions.py +43 -0
- qplot/datahandling/file_identity.py +461 -0
- qplot/datahandling/parameter_data.py +149 -0
- qplot/datahandling/plot_progress.py +17 -0
- qplot/datahandling/qcodes_cache.py +277 -0
- qplot/datahandling/qcodes_compat.py +14 -0
- qplot/datahandling/readSQL.py +2442 -0
- qplot/datahandling/readonly.py +2566 -0
- qplot/datahandling/source_activity.py +21 -0
- qplot/datahandling/trusted_array_preview.py +148 -0
- qplot/datahandling/trusted_derived_cache.py +1605 -0
- qplot/datahandling/trusted_derived_rendering.py +876 -0
- qplot/datahandling/trusted_heatmap.py +246 -0
- qplot/datahandling/trusted_live.py +2808 -0
- qplot/datahandling/trusted_live_queries.py +5907 -0
- qplot/datahandling/trusted_live_service.py +1919 -0
- qplot/datahandling/trusted_live_supervisor.py +2161 -0
- qplot/datahandling/trusted_plot.py +283 -0
- qplot/datahandling/trusted_presentation.py +1065 -0
- qplot/datahandling/trusted_snapshot.py +1132 -0
- qplot/datahandling/trusted_work_coordinator.py +2157 -0
- qplot/datahandling/trusted_work_scheduler.py +1204 -0
- qplot/diagnostics.py +241 -0
- qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
- qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
- qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
- qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
- qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
- qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
- qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
- qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
- qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
- qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
- qplot/testdata.py +1804 -0
- qplot/tools/__init__.py +13 -0
- qplot/tools/finite_means.py +123 -0
- qplot/tools/general.py +139 -0
- qplot/tools/heatmap_geometry.py +391 -0
- qplot/tools/operation_registry.py +179 -0
- qplot/tools/plot_tools.py +650 -0
- qplot/tools/sample_statistics.py +111 -0
- qplot/tools/worker.py +2838 -0
- qplot/windows/__init__.py +9 -0
- qplot/windows/_colorbar.py +422 -0
- qplot/windows/_commands.py +523 -0
- qplot/windows/_config_persistence.py +123 -0
- qplot/windows/_database_actions.py +4933 -0
- qplot/windows/_dataset_handle.py +221 -0
- qplot/windows/_display_precision.py +48 -0
- qplot/windows/_dragdrop.py +70 -0
- qplot/windows/_export_paths.py +1167 -0
- qplot/windows/_help.py +210 -0
- qplot/windows/_mixed_appearance.py +303 -0
- qplot/windows/_mixed_plots.py +461 -0
- qplot/windows/_native_averaging.py +323 -0
- qplot/windows/_native_transforms.py +551 -0
- qplot/windows/_plot1d_snap.py +612 -0
- qplot/windows/_plot1d_traces.py +2236 -0
- qplot/windows/_plot2d_colorbar.py +1127 -0
- qplot/windows/_plot2d_colorbar_dialog.py +786 -0
- qplot/windows/_plot2d_layers.py +2291 -0
- qplot/windows/_plot2d_sweeps.py +712 -0
- qplot/windows/_plotWin.py +2082 -0
- qplot/windows/_plot_actions.py +2703 -0
- qplot/windows/_plot_annotation.py +186 -0
- qplot/windows/_plot_appearance.py +283 -0
- qplot/windows/_plot_axis_scaling.py +1521 -0
- qplot/windows/_plot_data_viewbox.py +94 -0
- qplot/windows/_plot_drop_axes.py +224 -0
- qplot/windows/_plot_export.py +1445 -0
- qplot/windows/_plot_feedback.py +112 -0
- qplot/windows/_plot_marquee.py +659 -0
- qplot/windows/_plot_refresh.py +950 -0
- qplot/windows/_plot_source_promotion.py +178 -0
- qplot/windows/_plot_state.py +237 -0
- qplot/windows/_plot_status.py +115 -0
- qplot/windows/_plot_tiling.py +35 -0
- qplot/windows/_plot_transform_labels.py +60 -0
- qplot/windows/_preferences.py +724 -0
- qplot/windows/_refresh_interval.py +172 -0
- qplot/windows/_run_controls.py +789 -0
- qplot/windows/_shortcuts.py +47 -0
- qplot/windows/_subplots/__init__.py +8 -0
- qplot/windows/_subplots/subplot1d.py +532 -0
- qplot/windows/_subplots/subplot2d.py +919 -0
- qplot/windows/_svg_export.py +209 -0
- qplot/windows/_trusted_derived_qt.py +1759 -0
- qplot/windows/_widgets/__init__.py +23 -0
- qplot/windows/_widgets/_run_formatting.py +387 -0
- qplot/windows/_widgets/details_tables.py +1119 -0
- qplot/windows/_widgets/dropbox.py +290 -0
- qplot/windows/_widgets/operations.py +460 -0
- qplot/windows/_widgets/preview.py +2997 -0
- qplot/windows/_widgets/run_list_items.py +488 -0
- qplot/windows/_widgets/toolbar.py +111 -0
- qplot/windows/_widgets/treeWidgets.py +2572 -0
- qplot/windows/_window_controls.py +414 -0
- qplot/windows/main.py +1632 -0
- qplot/windows/plot1d.py +313 -0
- qplot/windows/plot2d.py +1935 -0
- qplotter-1.6.0.dist-info/METADATA +322 -0
- qplotter-1.6.0.dist-info/RECORD +130 -0
- qplotter-1.6.0.dist-info/WHEEL +5 -0
- qplotter-1.6.0.dist-info/entry_points.txt +4 -0
- qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
- qplotter-1.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2157 @@
|
|
|
1
|
+
"""Qt-independent owner/executor boundary for trusted Stage 5B derived work."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from array import array
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
12
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
|
13
|
+
from concurrent.futures import TimeoutError as FutureTimeout
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import StrEnum
|
|
16
|
+
from typing import TypeAlias
|
|
17
|
+
|
|
18
|
+
from qplot.datahandling.file_identity import DatabaseInstance
|
|
19
|
+
from qplot.datahandling.trusted_derived_cache import TrustedDerivedDiskCache
|
|
20
|
+
from qplot.datahandling.trusted_derived_rendering import (
|
|
21
|
+
DerivedPayload,
|
|
22
|
+
render_trusted_derived_payload,
|
|
23
|
+
validate_trusted_derived_payload,
|
|
24
|
+
)
|
|
25
|
+
from qplot.datahandling.trusted_live import (
|
|
26
|
+
TrustedLiveBusyTimeoutError,
|
|
27
|
+
TrustedLiveCancelledError,
|
|
28
|
+
TrustedLiveDeadlineExceededError,
|
|
29
|
+
TrustedLiveSourceChangedError,
|
|
30
|
+
)
|
|
31
|
+
from qplot.datahandling.trusted_live_queries import (
|
|
32
|
+
TrustedDerivedSourceObservation,
|
|
33
|
+
TrustedSelectedRunDetail,
|
|
34
|
+
TrustedSourceRevision,
|
|
35
|
+
trusted_derived_source_revision,
|
|
36
|
+
)
|
|
37
|
+
from qplot.datahandling.trusted_live_service import (
|
|
38
|
+
TrustedLiveReadService,
|
|
39
|
+
TrustedReadQueueFullError,
|
|
40
|
+
TrustedReadRequestCancelledError,
|
|
41
|
+
TrustedReadRequestDeadlineError,
|
|
42
|
+
TrustedReadSessionFailedError,
|
|
43
|
+
)
|
|
44
|
+
from qplot.datahandling.trusted_work_scheduler import (
|
|
45
|
+
CompletionDisposition,
|
|
46
|
+
ScheduledWork,
|
|
47
|
+
SchedulerLifecycle,
|
|
48
|
+
SchedulerSnapshot,
|
|
49
|
+
TrustedCacheWorkKey,
|
|
50
|
+
TrustedRunTier,
|
|
51
|
+
TrustedRunWorkSource,
|
|
52
|
+
TrustedWorkKind,
|
|
53
|
+
TrustedWorkScheduler,
|
|
54
|
+
TrustedWorkState,
|
|
55
|
+
WorkFormat,
|
|
56
|
+
WorkPublication,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
TRUSTED_DERIVED_DEFAULT_DEADLINE_SECONDS = 15.0
|
|
60
|
+
TRUSTED_DERIVED_MAX_REUSED_SOURCE_BYTES = 8 * 1024 * 1024
|
|
61
|
+
TRUSTED_DERIVED_MAX_REUSED_SOURCES = 512
|
|
62
|
+
TRUSTED_DERIVED_BACKGROUND_AGING_CLAIM_BOUND = 8
|
|
63
|
+
TRUSTED_DERIVED_SAME_TIER_PROGRESSIVE_CLAIM_BOUND = 3
|
|
64
|
+
TRUSTED_DERIVED_PROGRESSIVE_MIN_INTERVAL_SECONDS = 0.01
|
|
65
|
+
_PROGRESSIVE_ACTIVE_BIT = 1
|
|
66
|
+
_PROGRESSIVE_SERVED_BIT = 2
|
|
67
|
+
_RETRY_KIND_COUNT = len(TrustedWorkKind)
|
|
68
|
+
|
|
69
|
+
WakeupCallback: TypeAlias = Callable[[], None]
|
|
70
|
+
PublicationCallback: TypeAlias = Callable[[WorkPublication], None]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TrustedDerivedErrorCategory(StrEnum):
|
|
74
|
+
"""Bounded worker outcome classification safe to marshal to the owner."""
|
|
75
|
+
|
|
76
|
+
TRANSIENT = "transient"
|
|
77
|
+
PERMANENT = "permanent"
|
|
78
|
+
STALE = "stale"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class TrustedDerivedErrorRecord:
|
|
83
|
+
"""Non-executable bounded error data; never retains traceback objects."""
|
|
84
|
+
|
|
85
|
+
category: TrustedDerivedErrorCategory
|
|
86
|
+
code: str
|
|
87
|
+
message: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class TrustedSelectedDetailPublication:
|
|
92
|
+
"""One ephemeral selected-detail result with complete owner-side fencing."""
|
|
93
|
+
|
|
94
|
+
generation: int
|
|
95
|
+
key: TrustedCacheWorkKey
|
|
96
|
+
run_index: int
|
|
97
|
+
run_id: int
|
|
98
|
+
run_guid: str
|
|
99
|
+
helper_incarnation: int
|
|
100
|
+
selection_generation: int
|
|
101
|
+
detail: TrustedSelectedRunDetail
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
SelectedDetailCallback: TypeAlias = Callable[[TrustedSelectedDetailPublication], None]
|
|
105
|
+
ErrorCallback: TypeAlias = Callable[[ScheduledWork, TrustedDerivedErrorRecord], None]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class _SelectedDetailAttempt:
|
|
110
|
+
database_instance: DatabaseInstance
|
|
111
|
+
generation: int
|
|
112
|
+
source_revision: TrustedSourceRevision
|
|
113
|
+
helper_incarnation: int
|
|
114
|
+
selection_generation: int
|
|
115
|
+
run_index: int
|
|
116
|
+
run_id: int
|
|
117
|
+
run_guid: str
|
|
118
|
+
cancelled: threading.Event
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _JobDeadlineExceeded(TimeoutError):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True, slots=True)
|
|
126
|
+
class TrustedDerivedRun:
|
|
127
|
+
"""Stable run-table entry used by the coordinator and persistent broker."""
|
|
128
|
+
|
|
129
|
+
run_id: int
|
|
130
|
+
run_guid: str
|
|
131
|
+
source_revision: TrustedSourceRevision
|
|
132
|
+
|
|
133
|
+
def __post_init__(self) -> None:
|
|
134
|
+
if type(self.run_id) is not int or self.run_id <= 0:
|
|
135
|
+
raise ValueError("run_id must be a positive integer.")
|
|
136
|
+
if not self.run_guid:
|
|
137
|
+
raise ValueError("run_guid must be non-empty.")
|
|
138
|
+
if not isinstance(self.source_revision, TrustedSourceRevision):
|
|
139
|
+
raise TypeError("source_revision must be TrustedSourceRevision.")
|
|
140
|
+
|
|
141
|
+
def scheduler_source(self) -> TrustedRunWorkSource:
|
|
142
|
+
return TrustedRunWorkSource(self.run_guid, self.source_revision)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True, slots=True)
|
|
146
|
+
class _ExecutionResult:
|
|
147
|
+
work: ScheduledWork
|
|
148
|
+
key: TrustedCacheWorkKey
|
|
149
|
+
payload: DerivedPayload
|
|
150
|
+
observation: TrustedDerivedSourceObservation | None
|
|
151
|
+
observation_revision: TrustedSourceRevision | None
|
|
152
|
+
observation_size: int
|
|
153
|
+
cache_hit: bool
|
|
154
|
+
selected_detail_attempt: _SelectedDetailAttempt | None = None
|
|
155
|
+
selected_detail: TrustedSelectedRunDetail | None = None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True, slots=True)
|
|
159
|
+
class _ProgressiveExecutionResult:
|
|
160
|
+
"""One metadata-only layout page which is not yet publishable."""
|
|
161
|
+
|
|
162
|
+
work: ScheduledWork
|
|
163
|
+
key: TrustedCacheWorkKey
|
|
164
|
+
observation: TrustedDerivedSourceObservation
|
|
165
|
+
observation_revision: TrustedSourceRevision
|
|
166
|
+
observation_size: int
|
|
167
|
+
selected_detail_attempt: _SelectedDetailAttempt | None = None
|
|
168
|
+
selected_detail: TrustedSelectedRunDetail | None = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True)
|
|
172
|
+
class _ExecutionFailure:
|
|
173
|
+
work: ScheduledWork
|
|
174
|
+
error: TrustedDerivedErrorRecord
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True, slots=True)
|
|
178
|
+
class _ActiveClaim:
|
|
179
|
+
work: ScheduledWork
|
|
180
|
+
future: Future[_WorkerResult]
|
|
181
|
+
source_namespace: bytes
|
|
182
|
+
deadline: float
|
|
183
|
+
selected_detail_attempt: _SelectedDetailAttempt | None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
_SuccessfulResult: TypeAlias = _ExecutionResult | _ProgressiveExecutionResult
|
|
187
|
+
_WorkerResult: TypeAlias = _SuccessfulResult | _ExecutionFailure
|
|
188
|
+
_RetainedObservation: TypeAlias = tuple[
|
|
189
|
+
TrustedDerivedSourceObservation,
|
|
190
|
+
TrustedSourceRevision,
|
|
191
|
+
int,
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class _RetryWakeupNotifier:
|
|
196
|
+
"""Own at most one backoff timer which only invokes the UI notifier.
|
|
197
|
+
|
|
198
|
+
The timer thread never reads scheduler/coordinator state. Owner-thread
|
|
199
|
+
generation changes synchronously cancel its opaque token before returning.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, wakeup: WakeupCallback | None) -> None:
|
|
203
|
+
self._wakeup = wakeup
|
|
204
|
+
self._lock = threading.Lock()
|
|
205
|
+
self._timer: threading.Timer | None = None
|
|
206
|
+
self._deadline: float | None = None
|
|
207
|
+
self._serial = 0
|
|
208
|
+
|
|
209
|
+
def schedule(self, deadline: float) -> None:
|
|
210
|
+
if self._wakeup is None:
|
|
211
|
+
return
|
|
212
|
+
with self._lock:
|
|
213
|
+
if self._deadline == deadline and self._timer is not None:
|
|
214
|
+
return
|
|
215
|
+
prior = self._timer
|
|
216
|
+
self._serial += 1
|
|
217
|
+
serial = self._serial
|
|
218
|
+
self._deadline = deadline
|
|
219
|
+
timer = threading.Timer(
|
|
220
|
+
max(0.0, deadline - time.monotonic()),
|
|
221
|
+
self._fire,
|
|
222
|
+
args=(serial,),
|
|
223
|
+
)
|
|
224
|
+
timer.daemon = True
|
|
225
|
+
self._timer = timer
|
|
226
|
+
if prior is not None:
|
|
227
|
+
prior.cancel()
|
|
228
|
+
timer.start()
|
|
229
|
+
|
|
230
|
+
def cancel(self) -> None:
|
|
231
|
+
with self._lock:
|
|
232
|
+
self._serial += 1
|
|
233
|
+
timer = self._timer
|
|
234
|
+
self._timer = None
|
|
235
|
+
self._deadline = None
|
|
236
|
+
if timer is not None:
|
|
237
|
+
timer.cancel()
|
|
238
|
+
|
|
239
|
+
def _fire(self, serial: int) -> None:
|
|
240
|
+
with self._lock:
|
|
241
|
+
if serial != self._serial or self._timer is None:
|
|
242
|
+
return
|
|
243
|
+
assert self._deadline is not None
|
|
244
|
+
remaining = self._deadline - time.monotonic()
|
|
245
|
+
if remaining > 0.0:
|
|
246
|
+
# Some Windows timer implementations can wake slightly before
|
|
247
|
+
# their requested monotonic deadline. A one-shot early
|
|
248
|
+
# notification would be consumed while the coordinator still
|
|
249
|
+
# refuses to pump, stranding pending retry work. Retain the
|
|
250
|
+
# same opaque serial and re-arm for the exact remainder.
|
|
251
|
+
timer = threading.Timer(remaining, self._fire, args=(serial,))
|
|
252
|
+
timer.daemon = True
|
|
253
|
+
self._timer = timer
|
|
254
|
+
timer.start()
|
|
255
|
+
return
|
|
256
|
+
self._timer = None
|
|
257
|
+
self._deadline = None
|
|
258
|
+
# Invoke while holding the notifier-only lock so cancel() cannot
|
|
259
|
+
# return while an obsolete callback is still about to run.
|
|
260
|
+
assert self._wakeup is not None
|
|
261
|
+
self._wakeup()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class _ProgressiveWakeupNotifier(_RetryWakeupNotifier):
|
|
265
|
+
"""Own the sole coalesced timer used to pace progressive page restarts."""
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TrustedWorkCoordinator:
|
|
269
|
+
"""Execute exactly one lazy scheduler claim on one controlled worker.
|
|
270
|
+
|
|
271
|
+
The constructing thread owns every scheduler call. The worker callback
|
|
272
|
+
puts at most one completion into ``_completions`` and invokes ``wakeup``;
|
|
273
|
+
Stage 5C can map that callback to a queued Qt signal without changing this
|
|
274
|
+
backend. ``poll`` is the only completion/publication path.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
database_instance: DatabaseInstance,
|
|
280
|
+
runs: Sequence[TrustedDerivedRun],
|
|
281
|
+
service: TrustedLiveReadService,
|
|
282
|
+
*,
|
|
283
|
+
cache: TrustedDerivedDiskCache | None = None,
|
|
284
|
+
formats: Mapping[TrustedWorkKind, WorkFormat] | None = None,
|
|
285
|
+
wakeup: WakeupCallback | None = None,
|
|
286
|
+
on_publish: PublicationCallback | None = None,
|
|
287
|
+
on_selected_detail: SelectedDetailCallback | None = None,
|
|
288
|
+
on_error: ErrorCallback | None = None,
|
|
289
|
+
deadline_seconds: float = TRUSTED_DERIVED_DEFAULT_DEADLINE_SECONDS,
|
|
290
|
+
own_service: bool = False,
|
|
291
|
+
) -> None:
|
|
292
|
+
if not isinstance(service, TrustedLiveReadService):
|
|
293
|
+
raise TypeError("service must be TrustedLiveReadService.")
|
|
294
|
+
if service.database_instance != database_instance:
|
|
295
|
+
raise ValueError("The service and coordinator database instances differ.")
|
|
296
|
+
if not 0 < deadline_seconds <= 300:
|
|
297
|
+
raise ValueError("deadline_seconds must be from zero through 300 seconds.")
|
|
298
|
+
self._owner_thread_id = threading.get_ident()
|
|
299
|
+
self._runs = self._validated_runs(runs)
|
|
300
|
+
self._service = service
|
|
301
|
+
self._cache = cache or TrustedDerivedDiskCache(enabled=True)
|
|
302
|
+
self._wakeup = wakeup
|
|
303
|
+
self._on_publish = on_publish
|
|
304
|
+
self._on_selected_detail = on_selected_detail
|
|
305
|
+
self._on_error = on_error
|
|
306
|
+
self._deadline_seconds = float(deadline_seconds)
|
|
307
|
+
self._own_service = bool(own_service)
|
|
308
|
+
self._scheduler = TrustedWorkScheduler(
|
|
309
|
+
database_instance,
|
|
310
|
+
tuple(run.scheduler_source() for run in self._runs),
|
|
311
|
+
formats=formats,
|
|
312
|
+
on_publish=self._publish,
|
|
313
|
+
)
|
|
314
|
+
self._executor = ThreadPoolExecutor(
|
|
315
|
+
max_workers=1,
|
|
316
|
+
thread_name_prefix="qplot-trusted-derived",
|
|
317
|
+
)
|
|
318
|
+
self._completions: queue.Queue[_WorkerResult] = queue.Queue(maxsize=1)
|
|
319
|
+
self._active: _ActiveClaim | None = None
|
|
320
|
+
self._selected_index: int | None = None
|
|
321
|
+
self._selection_generation = 0
|
|
322
|
+
self._helper_incarnation = 0
|
|
323
|
+
self._accepted_selected_detail: (
|
|
324
|
+
tuple[int, int, int, int, str, TrustedSourceRevision] | None
|
|
325
|
+
) = None
|
|
326
|
+
self._run_indices_by_guid = {
|
|
327
|
+
run.run_guid: index for index, run in enumerate(self._runs)
|
|
328
|
+
}
|
|
329
|
+
self._reused_observations: OrderedDict[str, _RetainedObservation] = (
|
|
330
|
+
OrderedDict()
|
|
331
|
+
)
|
|
332
|
+
self._reused_observation_bytes = 0
|
|
333
|
+
# One byte per stable run is the complete continuation-ready state.
|
|
334
|
+
# Its served bit is compared with the global round epoch, allowing a
|
|
335
|
+
# lazy full rotation without materialising an O(run-count) ready deque.
|
|
336
|
+
self._progressive_state = bytearray(len(self._runs))
|
|
337
|
+
self._progressive_round_epoch = False
|
|
338
|
+
self._progressive_active_count = 0
|
|
339
|
+
self._progressive_remaining_cursor = 0
|
|
340
|
+
self._progressive_restart_deferred = False
|
|
341
|
+
self._progressive_not_before = 0.0
|
|
342
|
+
self._progressive_notifier = _ProgressiveWakeupNotifier(wakeup)
|
|
343
|
+
self._foreground_claims_since_background_metadata = 0
|
|
344
|
+
self._age_progressive_background_next = True
|
|
345
|
+
self._ordinary_claims_since_progressive_page = 0
|
|
346
|
+
self._memory_payload: tuple[TrustedCacheWorkKey, DerivedPayload] | None = None
|
|
347
|
+
self._deferred_invalidations: set[int] = set()
|
|
348
|
+
retry_slot_count = len(self._runs) * _RETRY_KIND_COUNT
|
|
349
|
+
self._retry_attempts = bytearray(retry_slot_count)
|
|
350
|
+
self._retry_deadlines = array("d", [0.0]) * retry_slot_count
|
|
351
|
+
self._retry_notifier = _RetryWakeupNotifier(wakeup)
|
|
352
|
+
self._invalidation_serial = 0
|
|
353
|
+
self._closed = False
|
|
354
|
+
self._executor_joined = False
|
|
355
|
+
self._configure_cache(database_instance)
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def scheduler(self) -> TrustedWorkScheduler:
|
|
359
|
+
"""Expose owner-thread state for Stage 5C diagnostics, not worker use."""
|
|
360
|
+
|
|
361
|
+
self._require_owner()
|
|
362
|
+
return self._scheduler
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def active(self) -> bool:
|
|
366
|
+
self._require_owner()
|
|
367
|
+
return self._active is not None
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def runs(self) -> tuple[TrustedDerivedRun, ...]:
|
|
371
|
+
"""Return the current stable table, including refined revisions."""
|
|
372
|
+
|
|
373
|
+
self._require_owner()
|
|
374
|
+
return self._runs
|
|
375
|
+
|
|
376
|
+
def snapshot(self) -> SchedulerSnapshot:
|
|
377
|
+
self._require_owner()
|
|
378
|
+
return self._scheduler.snapshot()
|
|
379
|
+
|
|
380
|
+
def start(self) -> None:
|
|
381
|
+
self._require_owner()
|
|
382
|
+
self._require_open()
|
|
383
|
+
self._pump()
|
|
384
|
+
|
|
385
|
+
def select_run(self, run_index: int | None) -> None:
|
|
386
|
+
self._require_owner()
|
|
387
|
+
if run_index == self._selected_index:
|
|
388
|
+
self._scheduler.select_run(run_index)
|
|
389
|
+
self._pump()
|
|
390
|
+
return
|
|
391
|
+
# Validate before changing the selected-detail epoch. A rejected UI
|
|
392
|
+
# index must leave every previously accepted detail fence intact.
|
|
393
|
+
self._scheduler.select_run(run_index)
|
|
394
|
+
self._selection_changed(run_index)
|
|
395
|
+
self._pump()
|
|
396
|
+
|
|
397
|
+
def set_priority(
|
|
398
|
+
self,
|
|
399
|
+
selected_index: int | None,
|
|
400
|
+
visible_indices: Sequence[int],
|
|
401
|
+
*,
|
|
402
|
+
pump: bool = True,
|
|
403
|
+
) -> None:
|
|
404
|
+
"""Atomically apply selection and viewport state at one work boundary."""
|
|
405
|
+
|
|
406
|
+
self._require_owner()
|
|
407
|
+
prior_visible = self._scheduler.snapshot().visible_indices
|
|
408
|
+
selection_changed = selected_index != self._selected_index
|
|
409
|
+
self._scheduler.set_priority(selected_index, visible_indices)
|
|
410
|
+
if selection_changed:
|
|
411
|
+
self._selection_changed(selected_index)
|
|
412
|
+
self._viewport_changed(
|
|
413
|
+
prior_visible, self._scheduler.snapshot().visible_indices
|
|
414
|
+
)
|
|
415
|
+
if pump:
|
|
416
|
+
self._pump()
|
|
417
|
+
|
|
418
|
+
def _selection_changed(self, run_index: int | None) -> None:
|
|
419
|
+
"""Advance selected-detail state after scheduler validation succeeds."""
|
|
420
|
+
|
|
421
|
+
self._cancel_selected_detail_attempt()
|
|
422
|
+
self._selection_generation += 1
|
|
423
|
+
self._selected_index = run_index
|
|
424
|
+
self._accepted_selected_detail = None
|
|
425
|
+
self._progressive_remaining_cursor = 0
|
|
426
|
+
if run_index is not None and self._progressive_is_active(run_index):
|
|
427
|
+
# An explicit new selection gets a page at the next boundary even
|
|
428
|
+
# if this run already consumed its current round quantum. This is
|
|
429
|
+
# the sole interaction-driven escape from autonomous 10 ms pacing.
|
|
430
|
+
self._mark_progressive_unserved(run_index)
|
|
431
|
+
self._clear_progressive_pacing()
|
|
432
|
+
if (
|
|
433
|
+
run_index is not None
|
|
434
|
+
and self._scheduler.state_for(run_index, TrustedWorkKind.METADATA)
|
|
435
|
+
is TrustedWorkState.COMPLETED
|
|
436
|
+
):
|
|
437
|
+
run = self._runs[run_index]
|
|
438
|
+
self._scheduler.request_completed_work(
|
|
439
|
+
run_index,
|
|
440
|
+
TrustedWorkKind.METADATA,
|
|
441
|
+
database_instance=self._scheduler.database_instance,
|
|
442
|
+
generation=self._scheduler.generation,
|
|
443
|
+
run_guid=run.run_guid,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def set_visible_range(self, start: int, stop: int) -> None:
|
|
447
|
+
self._require_owner()
|
|
448
|
+
prior_visible = self._scheduler.snapshot().visible_indices
|
|
449
|
+
self._scheduler.set_visible_range(start, stop)
|
|
450
|
+
self._viewport_changed(
|
|
451
|
+
prior_visible, self._scheduler.snapshot().visible_indices
|
|
452
|
+
)
|
|
453
|
+
self._pump()
|
|
454
|
+
|
|
455
|
+
def set_visible_indices(self, indices: Sequence[int]) -> None:
|
|
456
|
+
self._require_owner()
|
|
457
|
+
prior_visible = self._scheduler.snapshot().visible_indices
|
|
458
|
+
self._scheduler.set_visible_indices(indices)
|
|
459
|
+
self._viewport_changed(
|
|
460
|
+
prior_visible, self._scheduler.snapshot().visible_indices
|
|
461
|
+
)
|
|
462
|
+
self._pump()
|
|
463
|
+
|
|
464
|
+
def _viewport_changed(
|
|
465
|
+
self,
|
|
466
|
+
prior_visible: Sequence[int],
|
|
467
|
+
visible_indices: Sequence[int],
|
|
468
|
+
) -> None:
|
|
469
|
+
"""Make newly visible continuations eligible at the next boundary."""
|
|
470
|
+
|
|
471
|
+
prior = frozenset(prior_visible)
|
|
472
|
+
current = frozenset(visible_indices)
|
|
473
|
+
if current == prior:
|
|
474
|
+
return
|
|
475
|
+
self._progressive_remaining_cursor = 0
|
|
476
|
+
promoted = False
|
|
477
|
+
for run_index in current - prior:
|
|
478
|
+
if self._progressive_is_active(run_index):
|
|
479
|
+
self._mark_progressive_unserved(run_index)
|
|
480
|
+
promoted = True
|
|
481
|
+
if promoted:
|
|
482
|
+
self._clear_progressive_pacing()
|
|
483
|
+
|
|
484
|
+
def reconcile_runs(
|
|
485
|
+
self,
|
|
486
|
+
runs: Sequence[TrustedDerivedRun],
|
|
487
|
+
*,
|
|
488
|
+
priority: tuple[int | None, Sequence[int]] | None = None,
|
|
489
|
+
) -> None:
|
|
490
|
+
"""Append runs and optionally adopt one atomic post-append priority."""
|
|
491
|
+
|
|
492
|
+
self._require_owner()
|
|
493
|
+
updated = self._validated_runs(runs)
|
|
494
|
+
old_count = len(self._runs)
|
|
495
|
+
if len(updated) < old_count or updated[:old_count] != self._runs:
|
|
496
|
+
raise ValueError("Coordinator run reconciliation must be append-only.")
|
|
497
|
+
prior_visible = self._scheduler.snapshot().visible_indices
|
|
498
|
+
self._runs = updated
|
|
499
|
+
self._run_indices_by_guid = {
|
|
500
|
+
run.run_guid: index for index, run in enumerate(updated)
|
|
501
|
+
}
|
|
502
|
+
self._progressive_state.extend(
|
|
503
|
+
b"\0" * (len(updated) - len(self._progressive_state))
|
|
504
|
+
)
|
|
505
|
+
retry_additions = (len(updated) - old_count) * _RETRY_KIND_COUNT
|
|
506
|
+
if retry_additions:
|
|
507
|
+
self._retry_attempts.extend(b"\0" * retry_additions)
|
|
508
|
+
self._retry_deadlines.extend([0.0] * retry_additions)
|
|
509
|
+
self._scheduler.reconcile_runs(tuple(run.scheduler_source() for run in updated))
|
|
510
|
+
if priority is not None:
|
|
511
|
+
selected_index, visible_indices = priority
|
|
512
|
+
selection_changed = selected_index != self._selected_index
|
|
513
|
+
self._scheduler.set_priority(selected_index, visible_indices)
|
|
514
|
+
if selection_changed:
|
|
515
|
+
self._selection_changed(selected_index)
|
|
516
|
+
self._viewport_changed(
|
|
517
|
+
prior_visible,
|
|
518
|
+
self._scheduler.snapshot().visible_indices,
|
|
519
|
+
)
|
|
520
|
+
self._pump()
|
|
521
|
+
|
|
522
|
+
def source_changed(self, run_index: int) -> None:
|
|
523
|
+
"""Coalesce appends to active work; publish its prefix before refreshing."""
|
|
524
|
+
|
|
525
|
+
self._require_owner()
|
|
526
|
+
self._require_open()
|
|
527
|
+
if not 0 <= run_index < len(self._runs):
|
|
528
|
+
raise IndexError("run_index is outside the stable run table.")
|
|
529
|
+
if run_index == self._selected_index:
|
|
530
|
+
self._invalidate_selected_detail()
|
|
531
|
+
if self._active is not None and self._active.work.run_index == run_index:
|
|
532
|
+
self._deferred_invalidations.add(run_index)
|
|
533
|
+
return
|
|
534
|
+
self._invalidate_now(run_index, preserve_all_retries=True)
|
|
535
|
+
self._pump()
|
|
536
|
+
|
|
537
|
+
def update_format(self, kind: TrustedWorkKind, work_format: WorkFormat) -> None:
|
|
538
|
+
self._require_owner()
|
|
539
|
+
self._scheduler.update_format(kind, work_format)
|
|
540
|
+
self._clear_retry_kind(kind)
|
|
541
|
+
self._memory_payload = None
|
|
542
|
+
self._pump()
|
|
543
|
+
|
|
544
|
+
def request_completed_work(
|
|
545
|
+
self,
|
|
546
|
+
run_index: int,
|
|
547
|
+
kind: TrustedWorkKind,
|
|
548
|
+
*,
|
|
549
|
+
database_instance: DatabaseInstance,
|
|
550
|
+
generation: int,
|
|
551
|
+
run_guid: str,
|
|
552
|
+
prioritize: bool = False,
|
|
553
|
+
) -> bool:
|
|
554
|
+
"""Replay one exact completed item, normally from the disk cache."""
|
|
555
|
+
|
|
556
|
+
self._require_owner()
|
|
557
|
+
self._require_open()
|
|
558
|
+
accepted = self._scheduler.request_completed_work(
|
|
559
|
+
run_index,
|
|
560
|
+
kind,
|
|
561
|
+
database_instance=database_instance,
|
|
562
|
+
generation=generation,
|
|
563
|
+
run_guid=run_guid,
|
|
564
|
+
)
|
|
565
|
+
if accepted:
|
|
566
|
+
if prioritize:
|
|
567
|
+
self._scheduler.select_run(run_index)
|
|
568
|
+
self._memory_payload = None
|
|
569
|
+
self._pump()
|
|
570
|
+
return accepted
|
|
571
|
+
|
|
572
|
+
def switch_database(
|
|
573
|
+
self,
|
|
574
|
+
database_instance: DatabaseInstance,
|
|
575
|
+
runs: Sequence[TrustedDerivedRun],
|
|
576
|
+
service: TrustedLiveReadService,
|
|
577
|
+
*,
|
|
578
|
+
own_service: bool | None = None,
|
|
579
|
+
priority: tuple[int | None, Sequence[int]] | None = None,
|
|
580
|
+
defer_start: bool = False,
|
|
581
|
+
) -> None:
|
|
582
|
+
self._require_owner()
|
|
583
|
+
self._require_open()
|
|
584
|
+
if service.database_instance != database_instance:
|
|
585
|
+
raise ValueError("The replacement service is bound to another database.")
|
|
586
|
+
prior_service = self._service
|
|
587
|
+
prior_owned = self._own_service
|
|
588
|
+
updated = self._validated_runs(runs)
|
|
589
|
+
self._cancel_selected_detail_attempt()
|
|
590
|
+
self._selection_generation += 1
|
|
591
|
+
self._helper_incarnation += 1
|
|
592
|
+
self._selected_index = None
|
|
593
|
+
self._accepted_selected_detail = None
|
|
594
|
+
self._runs = updated
|
|
595
|
+
self._run_indices_by_guid = {
|
|
596
|
+
run.run_guid: index for index, run in enumerate(updated)
|
|
597
|
+
}
|
|
598
|
+
self._service = service
|
|
599
|
+
if own_service is not None:
|
|
600
|
+
self._own_service = bool(own_service)
|
|
601
|
+
self._clear_reused_observations()
|
|
602
|
+
self._progressive_state = bytearray(len(updated))
|
|
603
|
+
self._progressive_round_epoch = False
|
|
604
|
+
self._progressive_active_count = 0
|
|
605
|
+
self._progressive_remaining_cursor = 0
|
|
606
|
+
self._progressive_restart_deferred = False
|
|
607
|
+
self._progressive_not_before = 0.0
|
|
608
|
+
self._progressive_notifier.cancel()
|
|
609
|
+
self._foreground_claims_since_background_metadata = 0
|
|
610
|
+
self._age_progressive_background_next = True
|
|
611
|
+
self._ordinary_claims_since_progressive_page = 0
|
|
612
|
+
self._memory_payload = None
|
|
613
|
+
self._deferred_invalidations.clear()
|
|
614
|
+
retry_slot_count = len(updated) * _RETRY_KIND_COUNT
|
|
615
|
+
self._retry_attempts = bytearray(retry_slot_count)
|
|
616
|
+
self._retry_deadlines = array("d", [0.0]) * retry_slot_count
|
|
617
|
+
self._retry_notifier.cancel()
|
|
618
|
+
self._configure_cache(database_instance)
|
|
619
|
+
self._scheduler.switch_database(
|
|
620
|
+
database_instance,
|
|
621
|
+
tuple(run.scheduler_source() for run in updated),
|
|
622
|
+
)
|
|
623
|
+
if priority is not None:
|
|
624
|
+
selected_index, visible_indices = priority
|
|
625
|
+
self._scheduler.set_priority(selected_index, visible_indices)
|
|
626
|
+
self._selection_changed(selected_index)
|
|
627
|
+
if prior_owned and prior_service is not service:
|
|
628
|
+
prior_service.close_async()
|
|
629
|
+
if not defer_start:
|
|
630
|
+
self._pump()
|
|
631
|
+
|
|
632
|
+
def helper_restarted(self) -> None:
|
|
633
|
+
"""Invalidate every result after a helper-incarnation boundary."""
|
|
634
|
+
|
|
635
|
+
self._require_owner()
|
|
636
|
+
self._require_open()
|
|
637
|
+
self._cancel_selected_detail_attempt()
|
|
638
|
+
self._selection_generation += 1
|
|
639
|
+
self._helper_incarnation += 1
|
|
640
|
+
self._accepted_selected_detail = None
|
|
641
|
+
selected_index = self._selected_index
|
|
642
|
+
visible_indices = self._scheduler.snapshot().visible_indices
|
|
643
|
+
replacements = tuple(
|
|
644
|
+
TrustedDerivedRun(
|
|
645
|
+
run.run_id,
|
|
646
|
+
run.run_guid,
|
|
647
|
+
self._invalidation_revision(index),
|
|
648
|
+
)
|
|
649
|
+
for index, run in enumerate(self._runs)
|
|
650
|
+
)
|
|
651
|
+
self._runs = replacements
|
|
652
|
+
self._clear_reused_observations()
|
|
653
|
+
self._progressive_state = bytearray(len(replacements))
|
|
654
|
+
self._progressive_round_epoch = False
|
|
655
|
+
self._progressive_active_count = 0
|
|
656
|
+
self._progressive_remaining_cursor = 0
|
|
657
|
+
self._progressive_restart_deferred = False
|
|
658
|
+
self._progressive_not_before = 0.0
|
|
659
|
+
self._progressive_notifier.cancel()
|
|
660
|
+
self._foreground_claims_since_background_metadata = 0
|
|
661
|
+
self._age_progressive_background_next = True
|
|
662
|
+
self._ordinary_claims_since_progressive_page = 0
|
|
663
|
+
self._memory_payload = None
|
|
664
|
+
self._deferred_invalidations.clear()
|
|
665
|
+
retry_slot_count = len(replacements) * _RETRY_KIND_COUNT
|
|
666
|
+
self._retry_attempts = bytearray(retry_slot_count)
|
|
667
|
+
self._retry_deadlines = array("d", [0.0]) * retry_slot_count
|
|
668
|
+
self._retry_notifier.cancel()
|
|
669
|
+
self._scheduler.switch_database(
|
|
670
|
+
self._scheduler.database_instance,
|
|
671
|
+
tuple(run.scheduler_source() for run in replacements),
|
|
672
|
+
)
|
|
673
|
+
self._scheduler.set_priority(selected_index, visible_indices)
|
|
674
|
+
self._pump()
|
|
675
|
+
|
|
676
|
+
def poll(self) -> int:
|
|
677
|
+
"""Marshal at most one worker completion onto the scheduler owner thread.
|
|
678
|
+
|
|
679
|
+
A bounded owner turn is important for progressive layout inspection. A
|
|
680
|
+
worker can complete quickly enough for its callback to refill the queue
|
|
681
|
+
while this method is running; draining until empty would then let one Qt
|
|
682
|
+
event consume arbitrarily many pages and prevent a priority change from
|
|
683
|
+
intervening. Every completion schedules (or has already scheduled) a
|
|
684
|
+
fresh wakeup, so one-at-a-time draining preserves throughput without
|
|
685
|
+
relying on worker timing for GUI fairness.
|
|
686
|
+
"""
|
|
687
|
+
|
|
688
|
+
self._require_owner()
|
|
689
|
+
try:
|
|
690
|
+
completion = self._completions.get_nowait()
|
|
691
|
+
except queue.Empty:
|
|
692
|
+
# A deferred progressive restart is admitted only from a fresh
|
|
693
|
+
# owner event (or an explicit owner action that calls ``_pump``).
|
|
694
|
+
self._pump(allow_progressive_restart=True)
|
|
695
|
+
return 0
|
|
696
|
+
|
|
697
|
+
active = self._active
|
|
698
|
+
if active is not None and active.work is completion.work:
|
|
699
|
+
self._active = None
|
|
700
|
+
if self._scheduler.is_current_claim(completion.work):
|
|
701
|
+
if isinstance(completion, _ExecutionFailure):
|
|
702
|
+
self._handle_failure(completion)
|
|
703
|
+
elif isinstance(completion, _ProgressiveExecutionResult):
|
|
704
|
+
self._handle_progressive(completion)
|
|
705
|
+
else:
|
|
706
|
+
self._handle_success(completion)
|
|
707
|
+
# Ordinary work may continue immediately, but reaching the end of a
|
|
708
|
+
# progressive pass schedules a separate owner turn before another page.
|
|
709
|
+
self._pump(allow_progressive_restart=False)
|
|
710
|
+
return 1
|
|
711
|
+
|
|
712
|
+
def close(self, *, timeout: float = 30.0) -> None:
|
|
713
|
+
"""Cancel work, optionally retire the service, and join the sole worker."""
|
|
714
|
+
|
|
715
|
+
self._require_owner()
|
|
716
|
+
if not 0 <= timeout <= 300:
|
|
717
|
+
raise ValueError("timeout must be from zero through 300 seconds.")
|
|
718
|
+
self.close_async()
|
|
719
|
+
if not self.wait_closed(timeout):
|
|
720
|
+
raise TimeoutError(
|
|
721
|
+
"The trusted derived worker did not stop within its deadline."
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
def close_async(self) -> None:
|
|
725
|
+
"""Promptly cancel scheduling without waiting on the owner/GUI thread."""
|
|
726
|
+
|
|
727
|
+
self._require_owner()
|
|
728
|
+
if self._closed:
|
|
729
|
+
return
|
|
730
|
+
self._closed = True
|
|
731
|
+
self._cancel_selected_detail_attempt()
|
|
732
|
+
self._selection_generation += 1
|
|
733
|
+
self._selected_index = None
|
|
734
|
+
self._accepted_selected_detail = None
|
|
735
|
+
self._retry_attempts = bytearray(len(self._retry_attempts))
|
|
736
|
+
self._retry_deadlines = array("d", [0.0]) * len(self._retry_deadlines)
|
|
737
|
+
self._retry_notifier.cancel()
|
|
738
|
+
self._progressive_notifier.cancel()
|
|
739
|
+
self._scheduler.close()
|
|
740
|
+
if self._own_service:
|
|
741
|
+
self._service.close_async()
|
|
742
|
+
active = self._active
|
|
743
|
+
if active is not None:
|
|
744
|
+
active.work.cancellation.cancel()
|
|
745
|
+
self._executor.shutdown(wait=False, cancel_futures=True)
|
|
746
|
+
self._clear_reused_observations()
|
|
747
|
+
self._progressive_state = bytearray(len(self._runs))
|
|
748
|
+
self._progressive_round_epoch = False
|
|
749
|
+
self._progressive_active_count = 0
|
|
750
|
+
self._progressive_remaining_cursor = 0
|
|
751
|
+
self._progressive_restart_deferred = False
|
|
752
|
+
self._progressive_not_before = 0.0
|
|
753
|
+
self._foreground_claims_since_background_metadata = 0
|
|
754
|
+
self._ordinary_claims_since_progressive_page = 0
|
|
755
|
+
self._memory_payload = None
|
|
756
|
+
|
|
757
|
+
def wait_closed(self, timeout: float = 0.0) -> bool:
|
|
758
|
+
"""Wait for the already-cancelled worker under one explicit bound."""
|
|
759
|
+
|
|
760
|
+
self._require_owner()
|
|
761
|
+
if not 0 <= timeout <= 300:
|
|
762
|
+
raise ValueError("timeout must be from zero through 300 seconds.")
|
|
763
|
+
self.close_async()
|
|
764
|
+
deadline = time.monotonic() + timeout
|
|
765
|
+
active = self._active
|
|
766
|
+
if active is not None:
|
|
767
|
+
try:
|
|
768
|
+
active.future.result(timeout=max(0.0, deadline - time.monotonic()))
|
|
769
|
+
except FutureTimeout:
|
|
770
|
+
return False
|
|
771
|
+
except BaseException:
|
|
772
|
+
pass
|
|
773
|
+
self._active = None
|
|
774
|
+
while True:
|
|
775
|
+
try:
|
|
776
|
+
self._completions.get_nowait()
|
|
777
|
+
except queue.Empty:
|
|
778
|
+
break
|
|
779
|
+
if self._own_service and not self._service.wait_closed(
|
|
780
|
+
max(0.0, deadline - time.monotonic())
|
|
781
|
+
):
|
|
782
|
+
return False
|
|
783
|
+
if not self._executor_joined:
|
|
784
|
+
self._executor.shutdown(wait=True, cancel_futures=True)
|
|
785
|
+
self._executor_joined = True
|
|
786
|
+
return True
|
|
787
|
+
|
|
788
|
+
def _pump(self, *, allow_progressive_restart: bool = True) -> None:
|
|
789
|
+
if self._closed or self._active is not None:
|
|
790
|
+
return
|
|
791
|
+
|
|
792
|
+
snapshot = self._scheduler.snapshot()
|
|
793
|
+
now = time.monotonic()
|
|
794
|
+
retry_deadline = self._reschedule_retry_notifier(now)
|
|
795
|
+
retry_filter_active = retry_deadline is not None
|
|
796
|
+
next_priority = (
|
|
797
|
+
self._scheduler.next_pending_priority(
|
|
798
|
+
admissible=lambda run_index, kind: self._retry_work_is_admissible(
|
|
799
|
+
run_index,
|
|
800
|
+
kind,
|
|
801
|
+
now,
|
|
802
|
+
)
|
|
803
|
+
)
|
|
804
|
+
if retry_filter_active
|
|
805
|
+
else snapshot.next_priority_key
|
|
806
|
+
)
|
|
807
|
+
selected_index = snapshot.selected_index
|
|
808
|
+
selected_image_ready = bool(
|
|
809
|
+
next_priority is not None
|
|
810
|
+
and TrustedRunTier(next_priority[0]) is TrustedRunTier.SELECTED
|
|
811
|
+
and TrustedWorkKind(next_priority[1]) is not TrustedWorkKind.METADATA
|
|
812
|
+
) or bool(
|
|
813
|
+
selected_index is not None
|
|
814
|
+
and self._scheduler.metadata_is_conclusive(selected_index)
|
|
815
|
+
and any(
|
|
816
|
+
self._scheduler.state_for(selected_index, kind)
|
|
817
|
+
is TrustedWorkState.PENDING
|
|
818
|
+
and self._retry_slot_is_ready(selected_index, kind, now)
|
|
819
|
+
for kind in (TrustedWorkKind.THUMBNAIL, TrustedWorkKind.PREVIEW)
|
|
820
|
+
)
|
|
821
|
+
)
|
|
822
|
+
age_background_due = (
|
|
823
|
+
self._foreground_claims_since_background_metadata
|
|
824
|
+
>= TRUSTED_DERIVED_BACKGROUND_AGING_CLAIM_BOUND
|
|
825
|
+
and not selected_image_ready
|
|
826
|
+
)
|
|
827
|
+
progressive_index = self._peek_progressive_run_index(
|
|
828
|
+
snapshot,
|
|
829
|
+
admissible=lambda run_index: self._retry_slot_is_ready(
|
|
830
|
+
run_index,
|
|
831
|
+
TrustedWorkKind.METADATA,
|
|
832
|
+
now,
|
|
833
|
+
),
|
|
834
|
+
)
|
|
835
|
+
progressive_precedes = False
|
|
836
|
+
if progressive_index is not None:
|
|
837
|
+
progressive_tier = self._tier_for_snapshot(snapshot, progressive_index)
|
|
838
|
+
base_is_progressive = bool(
|
|
839
|
+
next_priority is not None
|
|
840
|
+
and TrustedWorkKind(next_priority[1]) is TrustedWorkKind.METADATA
|
|
841
|
+
and self._progressive_is_active(next_priority[2])
|
|
842
|
+
)
|
|
843
|
+
if next_priority is None:
|
|
844
|
+
progressive_precedes = True
|
|
845
|
+
else:
|
|
846
|
+
# Equal-tier ordinary work wins, but an abandoned progressive
|
|
847
|
+
# slot receives one page after at most one sibling's M/T/P
|
|
848
|
+
# sequence. This keeps ready images prompt while preventing
|
|
849
|
+
# a stream of newly appended same-tier runs from starving an
|
|
850
|
+
# older continuation forever.
|
|
851
|
+
progressive_precedes = (
|
|
852
|
+
base_is_progressive
|
|
853
|
+
or (progressive_tier < TrustedRunTier(next_priority[0]))
|
|
854
|
+
or (
|
|
855
|
+
progressive_tier is TrustedRunTier(next_priority[0])
|
|
856
|
+
and self._ordinary_claims_since_progressive_page
|
|
857
|
+
>= TRUSTED_DERIVED_SAME_TIER_PROGRESSIVE_CLAIM_BOUND
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
age_ordinary_metadata = False
|
|
862
|
+
ordinary_remaining_index: int | None = None
|
|
863
|
+
if age_background_due:
|
|
864
|
+
ordinary_remaining_index = (
|
|
865
|
+
self._scheduler.next_pending_remaining_metadata_index(
|
|
866
|
+
admissible=lambda run_index: (
|
|
867
|
+
not self._progressive_is_active(run_index)
|
|
868
|
+
and self._retry_slot_is_ready(
|
|
869
|
+
run_index,
|
|
870
|
+
TrustedWorkKind.METADATA,
|
|
871
|
+
now,
|
|
872
|
+
)
|
|
873
|
+
),
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
remaining_progressive_index = self._peek_progressive_run_index(
|
|
877
|
+
snapshot,
|
|
878
|
+
required_tier=TrustedRunTier.REMAINING,
|
|
879
|
+
admissible=lambda run_index: self._retry_slot_is_ready(
|
|
880
|
+
run_index,
|
|
881
|
+
TrustedWorkKind.METADATA,
|
|
882
|
+
now,
|
|
883
|
+
),
|
|
884
|
+
)
|
|
885
|
+
choose_progressive = remaining_progressive_index is not None and (
|
|
886
|
+
ordinary_remaining_index is None
|
|
887
|
+
or self._age_progressive_background_next
|
|
888
|
+
)
|
|
889
|
+
if choose_progressive:
|
|
890
|
+
progressive_index = remaining_progressive_index
|
|
891
|
+
progressive_precedes = True
|
|
892
|
+
elif ordinary_remaining_index is not None:
|
|
893
|
+
progressive_precedes = False
|
|
894
|
+
age_ordinary_metadata = True
|
|
895
|
+
|
|
896
|
+
work: ScheduledWork | None = None
|
|
897
|
+
if progressive_precedes:
|
|
898
|
+
if not allow_progressive_restart or now < self._progressive_not_before:
|
|
899
|
+
self._defer_progressive_restart(now)
|
|
900
|
+
return
|
|
901
|
+
assert progressive_index is not None
|
|
902
|
+
run = self._runs[progressive_index]
|
|
903
|
+
self._scheduler.request_completed_work(
|
|
904
|
+
progressive_index,
|
|
905
|
+
TrustedWorkKind.METADATA,
|
|
906
|
+
database_instance=self._scheduler.database_instance,
|
|
907
|
+
generation=self._scheduler.generation,
|
|
908
|
+
run_guid=run.run_guid,
|
|
909
|
+
)
|
|
910
|
+
work = self._scheduler.claim_pending(
|
|
911
|
+
progressive_index,
|
|
912
|
+
TrustedWorkKind.METADATA,
|
|
913
|
+
)
|
|
914
|
+
if work is None:
|
|
915
|
+
if (
|
|
916
|
+
self._scheduler.state_for(
|
|
917
|
+
progressive_index,
|
|
918
|
+
TrustedWorkKind.METADATA,
|
|
919
|
+
)
|
|
920
|
+
is TrustedWorkState.COMPLETED
|
|
921
|
+
):
|
|
922
|
+
self._discard_progressive_guid(run.run_guid)
|
|
923
|
+
return
|
|
924
|
+
self._progressive_restart_deferred = False
|
|
925
|
+
self._progressive_not_before = 0.0
|
|
926
|
+
self._progressive_notifier.cancel()
|
|
927
|
+
if work is None and age_ordinary_metadata:
|
|
928
|
+
assert ordinary_remaining_index is not None
|
|
929
|
+
work = self._scheduler.claim_pending(
|
|
930
|
+
ordinary_remaining_index,
|
|
931
|
+
TrustedWorkKind.METADATA,
|
|
932
|
+
)
|
|
933
|
+
if work is None and not age_ordinary_metadata:
|
|
934
|
+
if retry_filter_active:
|
|
935
|
+
if next_priority is not None:
|
|
936
|
+
work = self._scheduler.claim_pending(
|
|
937
|
+
next_priority[2],
|
|
938
|
+
TrustedWorkKind(next_priority[1]),
|
|
939
|
+
)
|
|
940
|
+
else:
|
|
941
|
+
work = self._scheduler.claim_next(
|
|
942
|
+
age_remaining_metadata=False,
|
|
943
|
+
)
|
|
944
|
+
if work is None:
|
|
945
|
+
if self._progressive_active_count == 0:
|
|
946
|
+
self._clear_progressive_pacing()
|
|
947
|
+
return
|
|
948
|
+
run = self._runs[work.run_index]
|
|
949
|
+
progressive_refresh = (
|
|
950
|
+
work.key.kind is TrustedWorkKind.METADATA
|
|
951
|
+
and self._progressive_is_active(work.run_index)
|
|
952
|
+
)
|
|
953
|
+
if progressive_refresh:
|
|
954
|
+
self._mark_progressive_served(work.run_index, snapshot)
|
|
955
|
+
self._record_claim_admission(work)
|
|
956
|
+
memory_payload = None
|
|
957
|
+
observation = None
|
|
958
|
+
if not progressive_refresh:
|
|
959
|
+
memory_payload = self._memory_payload
|
|
960
|
+
if memory_payload is not None and memory_payload[0] == work.key:
|
|
961
|
+
self._memory_payload = None
|
|
962
|
+
else:
|
|
963
|
+
memory_payload = None
|
|
964
|
+
observation = self._reused_observation_for(work)
|
|
965
|
+
selected_detail_attempt = self._selected_detail_attempt_for(work, run)
|
|
966
|
+
deadline = time.monotonic() + self._deadline_seconds
|
|
967
|
+
future = self._executor.submit(
|
|
968
|
+
self._execute,
|
|
969
|
+
work,
|
|
970
|
+
run,
|
|
971
|
+
self._service,
|
|
972
|
+
observation,
|
|
973
|
+
memory_payload,
|
|
974
|
+
selected_detail_attempt,
|
|
975
|
+
deadline,
|
|
976
|
+
progressive_refresh,
|
|
977
|
+
)
|
|
978
|
+
self._active = _ActiveClaim(
|
|
979
|
+
work,
|
|
980
|
+
future,
|
|
981
|
+
bytes(self._service.source_revision_namespace.nonce),
|
|
982
|
+
deadline,
|
|
983
|
+
selected_detail_attempt,
|
|
984
|
+
)
|
|
985
|
+
future.add_done_callback(self._worker_done)
|
|
986
|
+
|
|
987
|
+
def _defer_progressive_restart(self, now: float) -> None:
|
|
988
|
+
self._progressive_restart_deferred = True
|
|
989
|
+
if self._progressive_not_before <= now:
|
|
990
|
+
self._progressive_not_before = (
|
|
991
|
+
now + TRUSTED_DERIVED_PROGRESSIVE_MIN_INTERVAL_SECONDS
|
|
992
|
+
)
|
|
993
|
+
self._progressive_notifier.schedule(self._progressive_not_before)
|
|
994
|
+
|
|
995
|
+
def _clear_progressive_pacing(self) -> None:
|
|
996
|
+
self._progressive_restart_deferred = False
|
|
997
|
+
self._progressive_not_before = 0.0
|
|
998
|
+
self._progressive_notifier.cancel()
|
|
999
|
+
|
|
1000
|
+
def _record_claim_admission(self, work: ScheduledWork) -> None:
|
|
1001
|
+
tier = TrustedRunTier(work.priority_key[0])
|
|
1002
|
+
if work.key.kind is TrustedWorkKind.METADATA and self._progressive_is_active(
|
|
1003
|
+
work.run_index
|
|
1004
|
+
):
|
|
1005
|
+
self._ordinary_claims_since_progressive_page = 0
|
|
1006
|
+
else:
|
|
1007
|
+
self._ordinary_claims_since_progressive_page = min(
|
|
1008
|
+
TRUSTED_DERIVED_SAME_TIER_PROGRESSIVE_CLAIM_BOUND,
|
|
1009
|
+
self._ordinary_claims_since_progressive_page + 1,
|
|
1010
|
+
)
|
|
1011
|
+
if tier is TrustedRunTier.REMAINING and (
|
|
1012
|
+
work.key.kind is TrustedWorkKind.METADATA
|
|
1013
|
+
):
|
|
1014
|
+
self._foreground_claims_since_background_metadata = 0
|
|
1015
|
+
self._age_progressive_background_next = not self._progressive_is_active(
|
|
1016
|
+
work.run_index
|
|
1017
|
+
)
|
|
1018
|
+
elif tier is not TrustedRunTier.REMAINING:
|
|
1019
|
+
self._foreground_claims_since_background_metadata = min(
|
|
1020
|
+
TRUSTED_DERIVED_BACKGROUND_AGING_CLAIM_BOUND,
|
|
1021
|
+
self._foreground_claims_since_background_metadata + 1,
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
def _retry_offset(self, run_index: int, kind: TrustedWorkKind) -> int:
|
|
1025
|
+
return run_index * _RETRY_KIND_COUNT + int(kind)
|
|
1026
|
+
|
|
1027
|
+
def _retry_offset_for_slot(
|
|
1028
|
+
self,
|
|
1029
|
+
slot: tuple[str, TrustedWorkKind],
|
|
1030
|
+
) -> int | None:
|
|
1031
|
+
run_index = self._run_indices_by_guid.get(slot[0])
|
|
1032
|
+
if run_index is None:
|
|
1033
|
+
return None
|
|
1034
|
+
return self._retry_offset(run_index, slot[1])
|
|
1035
|
+
|
|
1036
|
+
def _retry_slot_for_offset(
|
|
1037
|
+
self,
|
|
1038
|
+
offset: int,
|
|
1039
|
+
) -> tuple[str, TrustedWorkKind] | None:
|
|
1040
|
+
run_index, kind_value = divmod(offset, _RETRY_KIND_COUNT)
|
|
1041
|
+
if not 0 <= run_index < len(self._runs):
|
|
1042
|
+
return None
|
|
1043
|
+
return self._runs[run_index].run_guid, TrustedWorkKind(kind_value)
|
|
1044
|
+
|
|
1045
|
+
@property
|
|
1046
|
+
def _retry_not_before(self) -> float:
|
|
1047
|
+
"""Earliest retained slot deadline, exposed for bounded diagnostics."""
|
|
1048
|
+
|
|
1049
|
+
return min(
|
|
1050
|
+
(deadline for deadline in self._retry_deadlines if deadline), default=0.0
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
@property
|
|
1054
|
+
def _retry_blocked_slot(self) -> tuple[str, TrustedWorkKind] | None:
|
|
1055
|
+
"""Earliest retained retry slot, exposed for deterministic diagnostics."""
|
|
1056
|
+
|
|
1057
|
+
candidate = min(
|
|
1058
|
+
(
|
|
1059
|
+
(deadline, offset)
|
|
1060
|
+
for offset, deadline in enumerate(self._retry_deadlines)
|
|
1061
|
+
if deadline
|
|
1062
|
+
),
|
|
1063
|
+
default=None,
|
|
1064
|
+
)
|
|
1065
|
+
if candidate is None:
|
|
1066
|
+
return None
|
|
1067
|
+
return self._retry_slot_for_offset(candidate[1])
|
|
1068
|
+
|
|
1069
|
+
def _retry_slot_is_ready(
|
|
1070
|
+
self,
|
|
1071
|
+
run_index: int,
|
|
1072
|
+
kind: TrustedWorkKind,
|
|
1073
|
+
now: float,
|
|
1074
|
+
) -> bool:
|
|
1075
|
+
return self._retry_deadlines[self._retry_offset(run_index, kind)] <= now
|
|
1076
|
+
|
|
1077
|
+
def _retry_work_is_admissible(
|
|
1078
|
+
self,
|
|
1079
|
+
run_index: int,
|
|
1080
|
+
kind: TrustedWorkKind,
|
|
1081
|
+
now: float,
|
|
1082
|
+
) -> bool:
|
|
1083
|
+
if not self._retry_slot_is_ready(run_index, kind, now):
|
|
1084
|
+
return False
|
|
1085
|
+
return kind is TrustedWorkKind.METADATA or (
|
|
1086
|
+
self._scheduler.metadata_is_conclusive(run_index)
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
def _reschedule_retry_notifier(self, now: float) -> float | None:
|
|
1090
|
+
deadline = min(
|
|
1091
|
+
(candidate for candidate in self._retry_deadlines if candidate > now),
|
|
1092
|
+
default=None,
|
|
1093
|
+
)
|
|
1094
|
+
if deadline is None:
|
|
1095
|
+
self._retry_notifier.cancel()
|
|
1096
|
+
else:
|
|
1097
|
+
self._retry_notifier.schedule(deadline)
|
|
1098
|
+
return deadline
|
|
1099
|
+
|
|
1100
|
+
@staticmethod
|
|
1101
|
+
def _tier_for_snapshot(
|
|
1102
|
+
snapshot: SchedulerSnapshot,
|
|
1103
|
+
run_index: int,
|
|
1104
|
+
) -> TrustedRunTier:
|
|
1105
|
+
if run_index == snapshot.selected_index:
|
|
1106
|
+
return TrustedRunTier.SELECTED
|
|
1107
|
+
if run_index in snapshot.visible_indices:
|
|
1108
|
+
return TrustedRunTier.VISIBLE
|
|
1109
|
+
return TrustedRunTier.REMAINING
|
|
1110
|
+
|
|
1111
|
+
def _peek_progressive_run_index(
|
|
1112
|
+
self,
|
|
1113
|
+
snapshot: SchedulerSnapshot,
|
|
1114
|
+
*,
|
|
1115
|
+
required_tier: TrustedRunTier | None = None,
|
|
1116
|
+
exclude_index: int | None = None,
|
|
1117
|
+
admissible: Callable[[int], bool] | None = None,
|
|
1118
|
+
) -> int | None:
|
|
1119
|
+
candidate = self._peek_progressive_current_round(
|
|
1120
|
+
snapshot,
|
|
1121
|
+
required_tier,
|
|
1122
|
+
exclude_index,
|
|
1123
|
+
admissible,
|
|
1124
|
+
)
|
|
1125
|
+
if (
|
|
1126
|
+
candidate is None
|
|
1127
|
+
and required_tier is None
|
|
1128
|
+
and self._progressive_active_count
|
|
1129
|
+
):
|
|
1130
|
+
self._progressive_round_epoch = not self._progressive_round_epoch
|
|
1131
|
+
self._progressive_remaining_cursor = 0
|
|
1132
|
+
candidate = self._peek_progressive_current_round(
|
|
1133
|
+
snapshot,
|
|
1134
|
+
None,
|
|
1135
|
+
exclude_index,
|
|
1136
|
+
admissible,
|
|
1137
|
+
)
|
|
1138
|
+
return candidate
|
|
1139
|
+
|
|
1140
|
+
def _peek_progressive_current_round(
|
|
1141
|
+
self,
|
|
1142
|
+
snapshot: SchedulerSnapshot,
|
|
1143
|
+
required_tier: TrustedRunTier | None,
|
|
1144
|
+
exclude_index: int | None,
|
|
1145
|
+
admissible: Callable[[int], bool] | None,
|
|
1146
|
+
) -> int | None:
|
|
1147
|
+
selected = snapshot.selected_index
|
|
1148
|
+
if (
|
|
1149
|
+
required_tier in {None, TrustedRunTier.SELECTED}
|
|
1150
|
+
and selected is not None
|
|
1151
|
+
and selected != exclude_index
|
|
1152
|
+
and (admissible is None or admissible(selected))
|
|
1153
|
+
and self._progressive_is_unserved(selected)
|
|
1154
|
+
):
|
|
1155
|
+
return selected
|
|
1156
|
+
if required_tier in {None, TrustedRunTier.VISIBLE}:
|
|
1157
|
+
for run_index in snapshot.visible_indices:
|
|
1158
|
+
if (
|
|
1159
|
+
run_index != selected
|
|
1160
|
+
and run_index != exclude_index
|
|
1161
|
+
and (admissible is None or admissible(run_index))
|
|
1162
|
+
and self._progressive_is_unserved(run_index)
|
|
1163
|
+
):
|
|
1164
|
+
return run_index
|
|
1165
|
+
if required_tier not in {None, TrustedRunTier.REMAINING}:
|
|
1166
|
+
return None
|
|
1167
|
+
target = _PROGRESSIVE_ACTIVE_BIT | (
|
|
1168
|
+
0 if self._progressive_round_epoch else _PROGRESSIVE_SERVED_BIT
|
|
1169
|
+
)
|
|
1170
|
+
needle = bytes((target,))
|
|
1171
|
+
visible = frozenset(snapshot.visible_indices)
|
|
1172
|
+
for start, stop in (
|
|
1173
|
+
(self._progressive_remaining_cursor, len(self._progressive_state)),
|
|
1174
|
+
(0, self._progressive_remaining_cursor),
|
|
1175
|
+
):
|
|
1176
|
+
run_index = self._progressive_state.find(needle, start, stop)
|
|
1177
|
+
while run_index >= 0:
|
|
1178
|
+
if (
|
|
1179
|
+
run_index != selected
|
|
1180
|
+
and run_index != exclude_index
|
|
1181
|
+
and run_index not in visible
|
|
1182
|
+
and (admissible is None or admissible(run_index))
|
|
1183
|
+
):
|
|
1184
|
+
return run_index
|
|
1185
|
+
run_index = self._progressive_state.find(
|
|
1186
|
+
needle,
|
|
1187
|
+
run_index + 1,
|
|
1188
|
+
stop,
|
|
1189
|
+
)
|
|
1190
|
+
return None
|
|
1191
|
+
|
|
1192
|
+
def _next_progressive_run_index(
|
|
1193
|
+
self,
|
|
1194
|
+
snapshot: SchedulerSnapshot,
|
|
1195
|
+
) -> int | None:
|
|
1196
|
+
"""Choose one page from a selected/visible/remaining fair round."""
|
|
1197
|
+
|
|
1198
|
+
run_index = self._peek_progressive_run_index(snapshot)
|
|
1199
|
+
if run_index is None:
|
|
1200
|
+
return None
|
|
1201
|
+
self._mark_progressive_served(run_index, snapshot)
|
|
1202
|
+
return run_index
|
|
1203
|
+
|
|
1204
|
+
def _execute(
|
|
1205
|
+
self,
|
|
1206
|
+
work: ScheduledWork,
|
|
1207
|
+
run: TrustedDerivedRun,
|
|
1208
|
+
service: TrustedLiveReadService,
|
|
1209
|
+
reused: _RetainedObservation | None,
|
|
1210
|
+
memory_payload: tuple[TrustedCacheWorkKey, DerivedPayload] | None,
|
|
1211
|
+
selected_detail_attempt: _SelectedDetailAttempt | None,
|
|
1212
|
+
deadline: float,
|
|
1213
|
+
progressive_refresh: bool,
|
|
1214
|
+
) -> _WorkerResult:
|
|
1215
|
+
request = None
|
|
1216
|
+
try:
|
|
1217
|
+
|
|
1218
|
+
def cancel_check() -> None:
|
|
1219
|
+
work.cancellation.raise_if_cancelled()
|
|
1220
|
+
if time.monotonic() >= deadline:
|
|
1221
|
+
raise _JobDeadlineExceeded(
|
|
1222
|
+
"The trusted derived job exceeded its absolute deadline."
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
def completed(
|
|
1226
|
+
key: TrustedCacheWorkKey,
|
|
1227
|
+
payload: DerivedPayload,
|
|
1228
|
+
observation: TrustedDerivedSourceObservation | None,
|
|
1229
|
+
observation_revision: TrustedSourceRevision | None,
|
|
1230
|
+
observation_size: int,
|
|
1231
|
+
*,
|
|
1232
|
+
cache_hit: bool,
|
|
1233
|
+
) -> _ExecutionResult:
|
|
1234
|
+
effective_attempt, selected_detail = selected_detail_for(key)
|
|
1235
|
+
return _ExecutionResult(
|
|
1236
|
+
work,
|
|
1237
|
+
key,
|
|
1238
|
+
payload,
|
|
1239
|
+
observation,
|
|
1240
|
+
observation_revision,
|
|
1241
|
+
observation_size,
|
|
1242
|
+
cache_hit,
|
|
1243
|
+
effective_attempt,
|
|
1244
|
+
selected_detail,
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
def selected_detail_for(
|
|
1248
|
+
key: TrustedCacheWorkKey,
|
|
1249
|
+
) -> tuple[
|
|
1250
|
+
_SelectedDetailAttempt | None,
|
|
1251
|
+
TrustedSelectedRunDetail | None,
|
|
1252
|
+
]:
|
|
1253
|
+
selected_detail = None
|
|
1254
|
+
effective_attempt = selected_detail_attempt
|
|
1255
|
+
if effective_attempt is not None:
|
|
1256
|
+
if effective_attempt.source_revision != key.source_revision:
|
|
1257
|
+
# The first bounded source read commonly refines a
|
|
1258
|
+
# provisional or progressive revision. Fence the
|
|
1259
|
+
# attached ephemeral detail to that accepted revision,
|
|
1260
|
+
# while sharing the original cancellation event held by
|
|
1261
|
+
# the active claim.
|
|
1262
|
+
effective_attempt = _SelectedDetailAttempt(
|
|
1263
|
+
effective_attempt.database_instance,
|
|
1264
|
+
effective_attempt.generation,
|
|
1265
|
+
key.source_revision,
|
|
1266
|
+
effective_attempt.helper_incarnation,
|
|
1267
|
+
effective_attempt.selection_generation,
|
|
1268
|
+
effective_attempt.run_index,
|
|
1269
|
+
effective_attempt.run_id,
|
|
1270
|
+
effective_attempt.run_guid,
|
|
1271
|
+
effective_attempt.cancelled,
|
|
1272
|
+
)
|
|
1273
|
+
selected_detail = self._read_selected_detail(
|
|
1274
|
+
effective_attempt, service, deadline, cancel_check
|
|
1275
|
+
)
|
|
1276
|
+
return effective_attempt, selected_detail
|
|
1277
|
+
|
|
1278
|
+
def progressive(
|
|
1279
|
+
key: TrustedCacheWorkKey,
|
|
1280
|
+
observation: TrustedDerivedSourceObservation,
|
|
1281
|
+
observation_revision: TrustedSourceRevision,
|
|
1282
|
+
observation_size: int,
|
|
1283
|
+
) -> _ProgressiveExecutionResult:
|
|
1284
|
+
effective_attempt, selected_detail = selected_detail_for(key)
|
|
1285
|
+
return _ProgressiveExecutionResult(
|
|
1286
|
+
work,
|
|
1287
|
+
key,
|
|
1288
|
+
observation,
|
|
1289
|
+
observation_revision,
|
|
1290
|
+
observation_size,
|
|
1291
|
+
effective_attempt,
|
|
1292
|
+
selected_detail,
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
cancel_check()
|
|
1296
|
+
observation: TrustedDerivedSourceObservation | None = None
|
|
1297
|
+
observation_revision: TrustedSourceRevision | None = None
|
|
1298
|
+
observation_size = 0
|
|
1299
|
+
if progressive_refresh and work.key.kind is not TrustedWorkKind.METADATA:
|
|
1300
|
+
raise RuntimeError("Only metadata work may refresh a progressive page.")
|
|
1301
|
+
if not progressive_refresh and reused is not None:
|
|
1302
|
+
observation, observation_revision, observation_size = reused
|
|
1303
|
+
if not progressive_refresh and memory_payload is not None:
|
|
1304
|
+
cancel_check()
|
|
1305
|
+
return completed(
|
|
1306
|
+
work.key,
|
|
1307
|
+
memory_payload[1],
|
|
1308
|
+
observation,
|
|
1309
|
+
observation_revision,
|
|
1310
|
+
observation_size,
|
|
1311
|
+
cache_hit=True,
|
|
1312
|
+
)
|
|
1313
|
+
if not progressive_refresh:
|
|
1314
|
+
cached = self._cache.get(work.key, cancel_check=cancel_check)
|
|
1315
|
+
if cached is not None:
|
|
1316
|
+
cancel_check()
|
|
1317
|
+
return completed(
|
|
1318
|
+
work.key,
|
|
1319
|
+
cached,
|
|
1320
|
+
observation,
|
|
1321
|
+
observation_revision,
|
|
1322
|
+
observation_size,
|
|
1323
|
+
cache_hit=True,
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
if (
|
|
1327
|
+
observation is None
|
|
1328
|
+
or observation.database_instance != work.key.database_instance
|
|
1329
|
+
or observation.run_guid != run.run_guid
|
|
1330
|
+
or observation_revision != work.key.source_revision
|
|
1331
|
+
):
|
|
1332
|
+
request = service.submit_derived_source(
|
|
1333
|
+
run.run_id,
|
|
1334
|
+
deadline=deadline,
|
|
1335
|
+
)
|
|
1336
|
+
while not request.done:
|
|
1337
|
+
try:
|
|
1338
|
+
cancel_check()
|
|
1339
|
+
except BaseException:
|
|
1340
|
+
request.cancel()
|
|
1341
|
+
raise
|
|
1342
|
+
time.sleep(min(0.01, max(0.0, deadline - time.monotonic())))
|
|
1343
|
+
observation = request.wait(0)
|
|
1344
|
+
observation_revision = None
|
|
1345
|
+
observation_size = 0
|
|
1346
|
+
cancel_check()
|
|
1347
|
+
if (
|
|
1348
|
+
observation.database_instance != work.key.database_instance
|
|
1349
|
+
or observation.run_id != run.run_id
|
|
1350
|
+
or observation.run_guid != run.run_guid
|
|
1351
|
+
or observation.service_namespace
|
|
1352
|
+
!= service.source_revision_namespace.nonce
|
|
1353
|
+
):
|
|
1354
|
+
raise RuntimeError("A stale trusted source observation was rejected.")
|
|
1355
|
+
if observation_revision is None:
|
|
1356
|
+
observation_revision = trusted_derived_source_revision(observation)
|
|
1357
|
+
observation_size = self._observation_size(observation)
|
|
1358
|
+
revision = observation_revision
|
|
1359
|
+
actual_key = TrustedCacheWorkKey(
|
|
1360
|
+
work.key.database_instance,
|
|
1361
|
+
work.key.run_guid,
|
|
1362
|
+
work.key.kind,
|
|
1363
|
+
revision,
|
|
1364
|
+
work.key.renderer_version,
|
|
1365
|
+
work.key.rendering_options,
|
|
1366
|
+
)
|
|
1367
|
+
if bool(observation.progressive_layout_pending):
|
|
1368
|
+
if work.key.kind is not TrustedWorkKind.METADATA:
|
|
1369
|
+
raise RuntimeError(
|
|
1370
|
+
"An inconclusive layout reached non-metadata derived work."
|
|
1371
|
+
)
|
|
1372
|
+
cancel_check()
|
|
1373
|
+
return progressive(
|
|
1374
|
+
actual_key,
|
|
1375
|
+
observation,
|
|
1376
|
+
observation_revision,
|
|
1377
|
+
observation_size,
|
|
1378
|
+
)
|
|
1379
|
+
cached = self._cache.get(actual_key, cancel_check=cancel_check)
|
|
1380
|
+
if cached is not None:
|
|
1381
|
+
cancel_check()
|
|
1382
|
+
return completed(
|
|
1383
|
+
actual_key,
|
|
1384
|
+
cached,
|
|
1385
|
+
observation,
|
|
1386
|
+
observation_revision,
|
|
1387
|
+
observation_size,
|
|
1388
|
+
cache_hit=True,
|
|
1389
|
+
)
|
|
1390
|
+
payload = render_trusted_derived_payload(
|
|
1391
|
+
observation,
|
|
1392
|
+
work.key.kind,
|
|
1393
|
+
work.key.rendering_options,
|
|
1394
|
+
cancel_check=cancel_check,
|
|
1395
|
+
)
|
|
1396
|
+
validate_trusted_derived_payload(payload)
|
|
1397
|
+
cancel_check()
|
|
1398
|
+
self._cache.put(actual_key, payload, cancel_check=cancel_check)
|
|
1399
|
+
cancel_check()
|
|
1400
|
+
return completed(
|
|
1401
|
+
actual_key,
|
|
1402
|
+
payload,
|
|
1403
|
+
observation,
|
|
1404
|
+
observation_revision,
|
|
1405
|
+
observation_size,
|
|
1406
|
+
cache_hit=False,
|
|
1407
|
+
)
|
|
1408
|
+
except BaseException as error:
|
|
1409
|
+
if request is not None and not request.done:
|
|
1410
|
+
request.cancel()
|
|
1411
|
+
return _ExecutionFailure(work, self._error_record(error))
|
|
1412
|
+
|
|
1413
|
+
def _selected_detail_attempt_for(
|
|
1414
|
+
self,
|
|
1415
|
+
work: ScheduledWork,
|
|
1416
|
+
run: TrustedDerivedRun,
|
|
1417
|
+
) -> _SelectedDetailAttempt | None:
|
|
1418
|
+
if (
|
|
1419
|
+
work.key.kind is not TrustedWorkKind.METADATA
|
|
1420
|
+
or work.run_index != self._selected_index
|
|
1421
|
+
):
|
|
1422
|
+
return None
|
|
1423
|
+
marker = (
|
|
1424
|
+
self._selection_generation,
|
|
1425
|
+
self._helper_incarnation,
|
|
1426
|
+
work.run_index,
|
|
1427
|
+
run.run_id,
|
|
1428
|
+
run.run_guid,
|
|
1429
|
+
)
|
|
1430
|
+
accepted = self._accepted_selected_detail
|
|
1431
|
+
if accepted is not None and marker == accepted[:5]:
|
|
1432
|
+
return None
|
|
1433
|
+
return _SelectedDetailAttempt(
|
|
1434
|
+
work.key.database_instance,
|
|
1435
|
+
work.generation,
|
|
1436
|
+
work.key.source_revision,
|
|
1437
|
+
self._helper_incarnation,
|
|
1438
|
+
self._selection_generation,
|
|
1439
|
+
work.run_index,
|
|
1440
|
+
run.run_id,
|
|
1441
|
+
run.run_guid,
|
|
1442
|
+
threading.Event(),
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
@staticmethod
|
|
1446
|
+
def _read_selected_detail(
|
|
1447
|
+
attempt: _SelectedDetailAttempt,
|
|
1448
|
+
service: TrustedLiveReadService,
|
|
1449
|
+
deadline: float,
|
|
1450
|
+
cancel_check: Callable[[], None],
|
|
1451
|
+
) -> TrustedSelectedRunDetail | None:
|
|
1452
|
+
request = None
|
|
1453
|
+
|
|
1454
|
+
def selected_cancel_check() -> None:
|
|
1455
|
+
cancel_check()
|
|
1456
|
+
if attempt.cancelled.is_set():
|
|
1457
|
+
raise InterruptedError("The selected-detail request was cancelled.")
|
|
1458
|
+
|
|
1459
|
+
try:
|
|
1460
|
+
selected_cancel_check()
|
|
1461
|
+
request = service.submit_selected_run(
|
|
1462
|
+
attempt.run_id,
|
|
1463
|
+
deadline=deadline,
|
|
1464
|
+
)
|
|
1465
|
+
identity = getattr(request, "identity", None)
|
|
1466
|
+
if (
|
|
1467
|
+
identity is not None
|
|
1468
|
+
and getattr(identity, "database_instance", None)
|
|
1469
|
+
!= attempt.database_instance
|
|
1470
|
+
):
|
|
1471
|
+
request.cancel()
|
|
1472
|
+
return None
|
|
1473
|
+
while not request.done:
|
|
1474
|
+
try:
|
|
1475
|
+
selected_cancel_check()
|
|
1476
|
+
except Exception:
|
|
1477
|
+
request.cancel()
|
|
1478
|
+
return None
|
|
1479
|
+
time.sleep(min(0.01, max(0.0, deadline - time.monotonic())))
|
|
1480
|
+
detail = request.wait(0)
|
|
1481
|
+
selected_cancel_check()
|
|
1482
|
+
if not isinstance(detail, TrustedSelectedRunDetail):
|
|
1483
|
+
return None
|
|
1484
|
+
if detail.run.run_id != attempt.run_id:
|
|
1485
|
+
return None
|
|
1486
|
+
fields = detail.run.as_dict()
|
|
1487
|
+
if str(fields.get("guid") or "") != attempt.run_guid:
|
|
1488
|
+
return None
|
|
1489
|
+
return detail
|
|
1490
|
+
except Exception:
|
|
1491
|
+
if request is not None and not request.done:
|
|
1492
|
+
request.cancel()
|
|
1493
|
+
return None
|
|
1494
|
+
|
|
1495
|
+
def _cancel_selected_detail_attempt(self) -> None:
|
|
1496
|
+
active = self._active
|
|
1497
|
+
if active is not None and active.selected_detail_attempt is not None:
|
|
1498
|
+
active.selected_detail_attempt.cancelled.set()
|
|
1499
|
+
|
|
1500
|
+
def _invalidate_selected_detail(self) -> None:
|
|
1501
|
+
self._cancel_selected_detail_attempt()
|
|
1502
|
+
self._selection_generation += 1
|
|
1503
|
+
self._accepted_selected_detail = None
|
|
1504
|
+
|
|
1505
|
+
def _worker_done(self, future: Future[_WorkerResult]) -> None:
|
|
1506
|
+
try:
|
|
1507
|
+
result = future.result()
|
|
1508
|
+
except BaseException as error:
|
|
1509
|
+
active = self._active
|
|
1510
|
+
if active is None:
|
|
1511
|
+
return
|
|
1512
|
+
result = _ExecutionFailure(active.work, self._error_record(error))
|
|
1513
|
+
try:
|
|
1514
|
+
self._completions.put_nowait(result)
|
|
1515
|
+
except queue.Full:
|
|
1516
|
+
return
|
|
1517
|
+
if self._wakeup is not None:
|
|
1518
|
+
self._wakeup()
|
|
1519
|
+
|
|
1520
|
+
def _handle_progressive(self, result: _ProgressiveExecutionResult) -> None:
|
|
1521
|
+
"""Park one inconclusive metadata page without rendering or caching it."""
|
|
1522
|
+
|
|
1523
|
+
slot = (result.work.key.run_guid, result.work.key.kind)
|
|
1524
|
+
self._clear_retry_backoff(slot)
|
|
1525
|
+
disposition = self._scheduler.park_progressive_metadata(
|
|
1526
|
+
result.work,
|
|
1527
|
+
result.key.source_revision,
|
|
1528
|
+
)
|
|
1529
|
+
if disposition is CompletionDisposition.STALE:
|
|
1530
|
+
return
|
|
1531
|
+
self._adopt_observation(
|
|
1532
|
+
result.observation,
|
|
1533
|
+
result.observation_revision,
|
|
1534
|
+
result.observation_size,
|
|
1535
|
+
)
|
|
1536
|
+
self._ordinary_claims_since_progressive_page = 0
|
|
1537
|
+
self._runs = tuple(
|
|
1538
|
+
TrustedDerivedRun(
|
|
1539
|
+
run.run_id,
|
|
1540
|
+
run.run_guid,
|
|
1541
|
+
(
|
|
1542
|
+
result.key.source_revision
|
|
1543
|
+
if index == result.work.run_index
|
|
1544
|
+
else run.source_revision
|
|
1545
|
+
),
|
|
1546
|
+
)
|
|
1547
|
+
for index, run in enumerate(self._runs)
|
|
1548
|
+
)
|
|
1549
|
+
self._publish_selected_detail(result)
|
|
1550
|
+
self._process_deferred(result.work, disposition, terminal=False)
|
|
1551
|
+
self._replay_selected_metadata_for_detail(result)
|
|
1552
|
+
|
|
1553
|
+
def _handle_success(self, result: _ExecutionResult) -> None:
|
|
1554
|
+
slot = (result.work.key.run_guid, result.work.key.kind)
|
|
1555
|
+
self._clear_retry_backoff(slot)
|
|
1556
|
+
if (
|
|
1557
|
+
result.work.key.kind is TrustedWorkKind.METADATA
|
|
1558
|
+
and self._scheduler.unpark_progressive_metadata(result.work)
|
|
1559
|
+
is CompletionDisposition.STALE
|
|
1560
|
+
):
|
|
1561
|
+
return
|
|
1562
|
+
if result.key != result.work.key:
|
|
1563
|
+
disposition = self._scheduler.refine_claim_source_revision(
|
|
1564
|
+
result.work,
|
|
1565
|
+
result.key.source_revision,
|
|
1566
|
+
)
|
|
1567
|
+
if disposition is CompletionDisposition.STALE:
|
|
1568
|
+
return
|
|
1569
|
+
self._adopt_observation(
|
|
1570
|
+
result.observation,
|
|
1571
|
+
result.observation_revision,
|
|
1572
|
+
result.observation_size,
|
|
1573
|
+
)
|
|
1574
|
+
self._memory_payload = (result.key, result.payload)
|
|
1575
|
+
self._runs = tuple(
|
|
1576
|
+
TrustedDerivedRun(
|
|
1577
|
+
run.run_id,
|
|
1578
|
+
run.run_guid,
|
|
1579
|
+
(
|
|
1580
|
+
result.key.source_revision
|
|
1581
|
+
if index == result.work.run_index
|
|
1582
|
+
else run.source_revision
|
|
1583
|
+
),
|
|
1584
|
+
)
|
|
1585
|
+
for index, run in enumerate(self._runs)
|
|
1586
|
+
)
|
|
1587
|
+
self._process_deferred(result.work, disposition, terminal=False)
|
|
1588
|
+
return
|
|
1589
|
+
self._adopt_observation(
|
|
1590
|
+
result.observation,
|
|
1591
|
+
result.observation_revision,
|
|
1592
|
+
result.observation_size,
|
|
1593
|
+
)
|
|
1594
|
+
disposition = self._scheduler.complete(result.work, result.payload)
|
|
1595
|
+
if disposition is CompletionDisposition.ACCEPTED:
|
|
1596
|
+
self._publish_selected_detail(result)
|
|
1597
|
+
self._process_deferred(result.work, disposition, terminal=True)
|
|
1598
|
+
if disposition is CompletionDisposition.ACCEPTED:
|
|
1599
|
+
self._replay_selected_metadata_for_detail(result)
|
|
1600
|
+
|
|
1601
|
+
def _replay_selected_metadata_for_detail(
|
|
1602
|
+
self,
|
|
1603
|
+
result: _SuccessfulResult,
|
|
1604
|
+
) -> None:
|
|
1605
|
+
"""Replay metadata for a missing or terminally stale selected detail."""
|
|
1606
|
+
|
|
1607
|
+
run_index = result.work.run_index
|
|
1608
|
+
if (
|
|
1609
|
+
result.work.key.kind is not TrustedWorkKind.METADATA
|
|
1610
|
+
or run_index != self._selected_index
|
|
1611
|
+
or not 0 <= run_index < len(self._runs)
|
|
1612
|
+
):
|
|
1613
|
+
return
|
|
1614
|
+
run = self._runs[run_index]
|
|
1615
|
+
marker = (
|
|
1616
|
+
self._selection_generation,
|
|
1617
|
+
self._helper_incarnation,
|
|
1618
|
+
run_index,
|
|
1619
|
+
run.run_id,
|
|
1620
|
+
run.run_guid,
|
|
1621
|
+
)
|
|
1622
|
+
accepted = self._accepted_selected_detail
|
|
1623
|
+
if accepted is not None and marker == accepted[:5]:
|
|
1624
|
+
if accepted[5] == run.source_revision:
|
|
1625
|
+
return
|
|
1626
|
+
# Progressive metadata pages refine their revision independently,
|
|
1627
|
+
# but must not amplify selected-run Snapshot reads. Once metadata
|
|
1628
|
+
# is terminal, release the older accepted identity and replay the
|
|
1629
|
+
# completed payload exactly once to obtain detail fenced to the
|
|
1630
|
+
# final revision.
|
|
1631
|
+
if (
|
|
1632
|
+
self._scheduler.state_for(run_index, TrustedWorkKind.METADATA)
|
|
1633
|
+
is not TrustedWorkState.COMPLETED
|
|
1634
|
+
):
|
|
1635
|
+
return
|
|
1636
|
+
self._accepted_selected_detail = None
|
|
1637
|
+
attempt = result.selected_detail_attempt
|
|
1638
|
+
if (
|
|
1639
|
+
attempt is not None
|
|
1640
|
+
and not attempt.cancelled.is_set()
|
|
1641
|
+
and marker
|
|
1642
|
+
== (
|
|
1643
|
+
attempt.selection_generation,
|
|
1644
|
+
attempt.helper_incarnation,
|
|
1645
|
+
attempt.run_index,
|
|
1646
|
+
attempt.run_id,
|
|
1647
|
+
attempt.run_guid,
|
|
1648
|
+
)
|
|
1649
|
+
):
|
|
1650
|
+
return
|
|
1651
|
+
if (
|
|
1652
|
+
self._scheduler.state_for(run_index, TrustedWorkKind.METADATA)
|
|
1653
|
+
is not TrustedWorkState.COMPLETED
|
|
1654
|
+
):
|
|
1655
|
+
return
|
|
1656
|
+
self._scheduler.request_completed_work(
|
|
1657
|
+
run_index,
|
|
1658
|
+
TrustedWorkKind.METADATA,
|
|
1659
|
+
database_instance=self._scheduler.database_instance,
|
|
1660
|
+
generation=self._scheduler.generation,
|
|
1661
|
+
run_guid=run.run_guid,
|
|
1662
|
+
)
|
|
1663
|
+
|
|
1664
|
+
def _publish_selected_detail(self, result: _SuccessfulResult) -> None:
|
|
1665
|
+
attempt = result.selected_detail_attempt
|
|
1666
|
+
detail = result.selected_detail
|
|
1667
|
+
if attempt is None or detail is None or attempt.cancelled.is_set():
|
|
1668
|
+
return
|
|
1669
|
+
marker = (
|
|
1670
|
+
attempt.selection_generation,
|
|
1671
|
+
attempt.helper_incarnation,
|
|
1672
|
+
attempt.run_index,
|
|
1673
|
+
attempt.run_id,
|
|
1674
|
+
attempt.run_guid,
|
|
1675
|
+
)
|
|
1676
|
+
accepted = (*marker, result.key.source_revision)
|
|
1677
|
+
if (
|
|
1678
|
+
self._closed
|
|
1679
|
+
or accepted == self._accepted_selected_detail
|
|
1680
|
+
or attempt.database_instance != self._scheduler.database_instance
|
|
1681
|
+
or attempt.generation != self._scheduler.generation
|
|
1682
|
+
or attempt.source_revision != result.key.source_revision
|
|
1683
|
+
or attempt.helper_incarnation != self._helper_incarnation
|
|
1684
|
+
or attempt.selection_generation != self._selection_generation
|
|
1685
|
+
or attempt.run_index != self._selected_index
|
|
1686
|
+
or not 0 <= attempt.run_index < len(self._runs)
|
|
1687
|
+
):
|
|
1688
|
+
return
|
|
1689
|
+
run = self._runs[attempt.run_index]
|
|
1690
|
+
if (
|
|
1691
|
+
run.run_id != attempt.run_id
|
|
1692
|
+
or run.run_guid != attempt.run_guid
|
|
1693
|
+
or run.source_revision != result.key.source_revision
|
|
1694
|
+
or result.work.run_index != attempt.run_index
|
|
1695
|
+
or result.work.key.kind is not TrustedWorkKind.METADATA
|
|
1696
|
+
or result.key.database_instance != attempt.database_instance
|
|
1697
|
+
or result.key.run_guid != attempt.run_guid
|
|
1698
|
+
or detail.run.run_id != attempt.run_id
|
|
1699
|
+
or str(detail.run.as_dict().get("guid") or "") != attempt.run_guid
|
|
1700
|
+
):
|
|
1701
|
+
return
|
|
1702
|
+
self._accepted_selected_detail = accepted
|
|
1703
|
+
if self._on_selected_detail is not None:
|
|
1704
|
+
self._on_selected_detail(
|
|
1705
|
+
TrustedSelectedDetailPublication(
|
|
1706
|
+
attempt.generation,
|
|
1707
|
+
result.key,
|
|
1708
|
+
attempt.run_index,
|
|
1709
|
+
attempt.run_id,
|
|
1710
|
+
attempt.run_guid,
|
|
1711
|
+
attempt.helper_incarnation,
|
|
1712
|
+
attempt.selection_generation,
|
|
1713
|
+
detail,
|
|
1714
|
+
)
|
|
1715
|
+
)
|
|
1716
|
+
|
|
1717
|
+
def _handle_failure(self, failure: _ExecutionFailure) -> None:
|
|
1718
|
+
slot = (failure.work.key.run_guid, failure.work.key.kind)
|
|
1719
|
+
if failure.error.category is TrustedDerivedErrorCategory.STALE:
|
|
1720
|
+
self._clear_retry_backoff(slot)
|
|
1721
|
+
disposition = self._scheduler.abandon(failure.work)
|
|
1722
|
+
self._process_deferred(failure.work, disposition, terminal=False)
|
|
1723
|
+
return
|
|
1724
|
+
if failure.error.category is TrustedDerivedErrorCategory.TRANSIENT:
|
|
1725
|
+
disposition = self._scheduler.abandon(failure.work)
|
|
1726
|
+
if disposition is CompletionDisposition.ACCEPTED:
|
|
1727
|
+
self._set_retry_backoff(slot, time.monotonic())
|
|
1728
|
+
self._process_deferred(
|
|
1729
|
+
failure.work,
|
|
1730
|
+
disposition,
|
|
1731
|
+
terminal=False,
|
|
1732
|
+
preserve_retry=True,
|
|
1733
|
+
)
|
|
1734
|
+
return
|
|
1735
|
+
self._clear_retry_backoff(slot)
|
|
1736
|
+
if self._on_error is not None:
|
|
1737
|
+
self._on_error(failure.work, failure.error)
|
|
1738
|
+
if failure.work.key.kind is TrustedWorkKind.METADATA:
|
|
1739
|
+
was_progressive = self._progressive_is_active(failure.work.run_index)
|
|
1740
|
+
self._discard_progressive_guid(failure.work.key.run_guid)
|
|
1741
|
+
if was_progressive:
|
|
1742
|
+
# The retained object is the last known-inconclusive page. A
|
|
1743
|
+
# later explicit replay must ask the live service for the next
|
|
1744
|
+
# page, never re-park the stale retained page without progress.
|
|
1745
|
+
self._discard_reused_observation(failure.work.key.run_guid)
|
|
1746
|
+
# A bounded error description is a terminal uncached result for this
|
|
1747
|
+
# finite claim. It prevents a permanent malformed run from spinning.
|
|
1748
|
+
payload: DerivedPayload = {
|
|
1749
|
+
"format": "qplot-trusted-derived-payload-v1",
|
|
1750
|
+
"kind": failure.work.key.kind.name.lower(),
|
|
1751
|
+
"status": "error",
|
|
1752
|
+
"description": failure.error.message,
|
|
1753
|
+
"source": (),
|
|
1754
|
+
"images": (),
|
|
1755
|
+
}
|
|
1756
|
+
disposition = self._scheduler.complete(failure.work, payload)
|
|
1757
|
+
self._process_deferred(failure.work, disposition, terminal=True)
|
|
1758
|
+
|
|
1759
|
+
def _invalidate_now(
|
|
1760
|
+
self,
|
|
1761
|
+
run_index: int,
|
|
1762
|
+
*,
|
|
1763
|
+
preserve_retry_kind: TrustedWorkKind | None = None,
|
|
1764
|
+
preserve_all_retries: bool = False,
|
|
1765
|
+
) -> None:
|
|
1766
|
+
revision = self._invalidation_revision(run_index)
|
|
1767
|
+
run = self._runs[run_index]
|
|
1768
|
+
updated = list(self._runs)
|
|
1769
|
+
updated[run_index] = TrustedDerivedRun(run.run_id, run.run_guid, revision)
|
|
1770
|
+
self._runs = tuple(updated)
|
|
1771
|
+
if not preserve_all_retries:
|
|
1772
|
+
self._clear_retry_for_run(
|
|
1773
|
+
run_index,
|
|
1774
|
+
preserve_kind=preserve_retry_kind,
|
|
1775
|
+
)
|
|
1776
|
+
self._discard_reused_observation(run.run_guid)
|
|
1777
|
+
# Preserve the one-bit continuation classification and its current
|
|
1778
|
+
# pacing deadline while discarding the stale page/revision. Otherwise
|
|
1779
|
+
# repeated writer notifications turn each page back into zero-delay
|
|
1780
|
+
# ordinary metadata.
|
|
1781
|
+
if not self._progressive_is_active(run_index):
|
|
1782
|
+
self._discard_progressive_guid(run.run_guid)
|
|
1783
|
+
self._memory_payload = None
|
|
1784
|
+
self._scheduler.update_source_revision(run_index, revision)
|
|
1785
|
+
|
|
1786
|
+
def _set_retry_backoff(
|
|
1787
|
+
self,
|
|
1788
|
+
slot: tuple[str, TrustedWorkKind],
|
|
1789
|
+
now: float,
|
|
1790
|
+
) -> None:
|
|
1791
|
+
offset = self._retry_offset_for_slot(slot)
|
|
1792
|
+
if offset is None:
|
|
1793
|
+
return
|
|
1794
|
+
attempt = min(self._retry_attempts[offset] + 1, 8)
|
|
1795
|
+
self._retry_attempts[offset] = attempt
|
|
1796
|
+
self._retry_deadlines[offset] = now + min(
|
|
1797
|
+
1.0,
|
|
1798
|
+
0.025 * (2 ** (attempt - 1)),
|
|
1799
|
+
)
|
|
1800
|
+
self._reschedule_retry_notifier(now)
|
|
1801
|
+
|
|
1802
|
+
def _clear_retry_backoff(
|
|
1803
|
+
self,
|
|
1804
|
+
slot: tuple[str, TrustedWorkKind],
|
|
1805
|
+
) -> None:
|
|
1806
|
+
offset = self._retry_offset_for_slot(slot)
|
|
1807
|
+
if offset is None:
|
|
1808
|
+
return
|
|
1809
|
+
self._retry_attempts[offset] = 0
|
|
1810
|
+
self._retry_deadlines[offset] = 0.0
|
|
1811
|
+
self._reschedule_retry_notifier(time.monotonic())
|
|
1812
|
+
|
|
1813
|
+
def _clear_retry_for_run(
|
|
1814
|
+
self,
|
|
1815
|
+
run_index: int,
|
|
1816
|
+
*,
|
|
1817
|
+
preserve_kind: TrustedWorkKind | None = None,
|
|
1818
|
+
) -> None:
|
|
1819
|
+
for kind in TrustedWorkKind:
|
|
1820
|
+
if kind is preserve_kind:
|
|
1821
|
+
continue
|
|
1822
|
+
offset = self._retry_offset(run_index, kind)
|
|
1823
|
+
self._retry_attempts[offset] = 0
|
|
1824
|
+
self._retry_deadlines[offset] = 0.0
|
|
1825
|
+
self._reschedule_retry_notifier(time.monotonic())
|
|
1826
|
+
|
|
1827
|
+
def _clear_retry_kind(self, kind: TrustedWorkKind) -> None:
|
|
1828
|
+
for run_index in range(len(self._runs)):
|
|
1829
|
+
offset = self._retry_offset(run_index, kind)
|
|
1830
|
+
self._retry_attempts[offset] = 0
|
|
1831
|
+
self._retry_deadlines[offset] = 0.0
|
|
1832
|
+
self._reschedule_retry_notifier(time.monotonic())
|
|
1833
|
+
|
|
1834
|
+
def _invalidation_revision(self, run_index: int) -> TrustedSourceRevision:
|
|
1835
|
+
self._invalidation_serial += 1
|
|
1836
|
+
payload = repr(
|
|
1837
|
+
(
|
|
1838
|
+
"qplot-derived-invalidation-v1",
|
|
1839
|
+
self._service.source_revision_namespace.nonce,
|
|
1840
|
+
self._scheduler.generation,
|
|
1841
|
+
run_index,
|
|
1842
|
+
self._invalidation_serial,
|
|
1843
|
+
)
|
|
1844
|
+
).encode("utf-8")
|
|
1845
|
+
return TrustedSourceRevision(hashlib.sha256(payload).digest())
|
|
1846
|
+
|
|
1847
|
+
def _publish(self, publication: WorkPublication) -> None:
|
|
1848
|
+
if self._on_publish is not None:
|
|
1849
|
+
self._on_publish(publication)
|
|
1850
|
+
|
|
1851
|
+
def _adopt_observation(
|
|
1852
|
+
self,
|
|
1853
|
+
observation: TrustedDerivedSourceObservation | None,
|
|
1854
|
+
revision: TrustedSourceRevision | None,
|
|
1855
|
+
size: int,
|
|
1856
|
+
) -> None:
|
|
1857
|
+
if observation is None:
|
|
1858
|
+
return
|
|
1859
|
+
if not isinstance(revision, TrustedSourceRevision):
|
|
1860
|
+
raise TypeError("A retained observation requires its source revision.")
|
|
1861
|
+
if type(size) is not int or size < 0:
|
|
1862
|
+
raise ValueError("A retained observation requires a bounded size.")
|
|
1863
|
+
if bool(getattr(observation, "progressive_layout_pending", False)):
|
|
1864
|
+
run_index = self._run_indices_by_guid.get(observation.run_guid)
|
|
1865
|
+
if run_index is not None:
|
|
1866
|
+
self._mark_progressive_active(run_index)
|
|
1867
|
+
else:
|
|
1868
|
+
self._discard_progressive_guid(observation.run_guid)
|
|
1869
|
+
self._discard_reused_observation(observation.run_guid)
|
|
1870
|
+
if size > TRUSTED_DERIVED_MAX_REUSED_SOURCE_BYTES:
|
|
1871
|
+
return
|
|
1872
|
+
self._reused_observations[observation.run_guid] = (
|
|
1873
|
+
observation,
|
|
1874
|
+
revision,
|
|
1875
|
+
size,
|
|
1876
|
+
)
|
|
1877
|
+
self._reused_observation_bytes += size
|
|
1878
|
+
while (
|
|
1879
|
+
len(self._reused_observations) > TRUSTED_DERIVED_MAX_REUSED_SOURCES
|
|
1880
|
+
or self._reused_observation_bytes > TRUSTED_DERIVED_MAX_REUSED_SOURCE_BYTES
|
|
1881
|
+
):
|
|
1882
|
+
(
|
|
1883
|
+
_guid,
|
|
1884
|
+
(
|
|
1885
|
+
_discarded,
|
|
1886
|
+
_discarded_revision,
|
|
1887
|
+
discarded_size,
|
|
1888
|
+
),
|
|
1889
|
+
) = self._reused_observations.popitem(last=False)
|
|
1890
|
+
self._reused_observation_bytes -= discarded_size
|
|
1891
|
+
|
|
1892
|
+
def _reused_observation_for(
|
|
1893
|
+
self,
|
|
1894
|
+
work: ScheduledWork,
|
|
1895
|
+
) -> _RetainedObservation | None:
|
|
1896
|
+
retained = self._reused_observations.get(work.key.run_guid)
|
|
1897
|
+
if retained is None:
|
|
1898
|
+
return None
|
|
1899
|
+
observation, revision, _size = retained
|
|
1900
|
+
if (
|
|
1901
|
+
observation.database_instance != work.key.database_instance
|
|
1902
|
+
or revision != work.key.source_revision
|
|
1903
|
+
):
|
|
1904
|
+
self._discard_reused_observation(work.key.run_guid)
|
|
1905
|
+
return None
|
|
1906
|
+
self._reused_observations.move_to_end(work.key.run_guid)
|
|
1907
|
+
return retained
|
|
1908
|
+
|
|
1909
|
+
def _discard_reused_observation(self, run_guid: str) -> None:
|
|
1910
|
+
retained = self._reused_observations.pop(run_guid, None)
|
|
1911
|
+
if retained is not None:
|
|
1912
|
+
self._reused_observation_bytes -= retained[2]
|
|
1913
|
+
|
|
1914
|
+
def _discard_progressive_guid(self, run_guid: str) -> None:
|
|
1915
|
+
run_index = self._run_indices_by_guid.get(run_guid)
|
|
1916
|
+
if run_index is None or not self._progressive_is_active(run_index):
|
|
1917
|
+
return
|
|
1918
|
+
self._progressive_state[run_index] = 0
|
|
1919
|
+
self._progressive_active_count -= 1
|
|
1920
|
+
self._progressive_remaining_cursor = min(
|
|
1921
|
+
self._progressive_remaining_cursor,
|
|
1922
|
+
run_index,
|
|
1923
|
+
)
|
|
1924
|
+
if self._progressive_active_count == 0:
|
|
1925
|
+
self._clear_progressive_pacing()
|
|
1926
|
+
|
|
1927
|
+
def _progressive_is_active(self, run_index: int) -> bool:
|
|
1928
|
+
return bool(self._progressive_state[run_index] & _PROGRESSIVE_ACTIVE_BIT)
|
|
1929
|
+
|
|
1930
|
+
def _progressive_is_unserved(self, run_index: int) -> bool:
|
|
1931
|
+
state = self._progressive_state[run_index]
|
|
1932
|
+
return bool(state & _PROGRESSIVE_ACTIVE_BIT) and (
|
|
1933
|
+
bool(state & _PROGRESSIVE_SERVED_BIT) != self._progressive_round_epoch
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1936
|
+
def _mark_progressive_active(self, run_index: int) -> None:
|
|
1937
|
+
if self._progressive_is_active(run_index):
|
|
1938
|
+
return
|
|
1939
|
+
self._progressive_active_count += 1
|
|
1940
|
+
self._mark_progressive_unserved(run_index)
|
|
1941
|
+
|
|
1942
|
+
def _mark_progressive_unserved(self, run_index: int) -> None:
|
|
1943
|
+
served_marker = 0 if self._progressive_round_epoch else _PROGRESSIVE_SERVED_BIT
|
|
1944
|
+
self._progressive_state[run_index] = _PROGRESSIVE_ACTIVE_BIT | served_marker
|
|
1945
|
+
self._progressive_remaining_cursor = min(
|
|
1946
|
+
self._progressive_remaining_cursor,
|
|
1947
|
+
run_index,
|
|
1948
|
+
)
|
|
1949
|
+
|
|
1950
|
+
def _mark_progressive_served(
|
|
1951
|
+
self,
|
|
1952
|
+
run_index: int,
|
|
1953
|
+
snapshot: SchedulerSnapshot,
|
|
1954
|
+
) -> None:
|
|
1955
|
+
served_marker = _PROGRESSIVE_SERVED_BIT if self._progressive_round_epoch else 0
|
|
1956
|
+
self._progressive_state[run_index] = _PROGRESSIVE_ACTIVE_BIT | served_marker
|
|
1957
|
+
if self._tier_for_snapshot(snapshot, run_index) is TrustedRunTier.REMAINING:
|
|
1958
|
+
self._progressive_remaining_cursor = max(
|
|
1959
|
+
self._progressive_remaining_cursor,
|
|
1960
|
+
run_index + 1,
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
def _clear_reused_observations(self) -> None:
|
|
1964
|
+
self._reused_observations.clear()
|
|
1965
|
+
self._reused_observation_bytes = 0
|
|
1966
|
+
|
|
1967
|
+
def _process_deferred(
|
|
1968
|
+
self,
|
|
1969
|
+
work: ScheduledWork,
|
|
1970
|
+
disposition: CompletionDisposition,
|
|
1971
|
+
*,
|
|
1972
|
+
terminal: bool,
|
|
1973
|
+
preserve_retry: bool = False,
|
|
1974
|
+
) -> None:
|
|
1975
|
+
run_index = work.run_index
|
|
1976
|
+
if (
|
|
1977
|
+
disposition is CompletionDisposition.STALE
|
|
1978
|
+
or run_index not in self._deferred_invalidations
|
|
1979
|
+
or work.generation != self._scheduler.generation
|
|
1980
|
+
or work.key.database_instance != self._scheduler.database_instance
|
|
1981
|
+
or not 0 <= run_index < len(self._runs)
|
|
1982
|
+
or self._runs[run_index].run_guid != work.key.run_guid
|
|
1983
|
+
):
|
|
1984
|
+
return
|
|
1985
|
+
completed_prefix = terminal and all(
|
|
1986
|
+
self._scheduler.state_for(run_index, kind) is TrustedWorkState.COMPLETED
|
|
1987
|
+
for kind in TrustedWorkKind
|
|
1988
|
+
)
|
|
1989
|
+
if not terminal or completed_prefix:
|
|
1990
|
+
self._deferred_invalidations.discard(run_index)
|
|
1991
|
+
self._invalidate_now(
|
|
1992
|
+
run_index,
|
|
1993
|
+
preserve_retry_kind=(work.key.kind if preserve_retry else None),
|
|
1994
|
+
)
|
|
1995
|
+
|
|
1996
|
+
def _configure_cache(self, database_instance: DatabaseInstance) -> None:
|
|
1997
|
+
configure = getattr(self._cache, "configure_for_database", None)
|
|
1998
|
+
if callable(configure):
|
|
1999
|
+
configure(database_instance)
|
|
2000
|
+
|
|
2001
|
+
@staticmethod
|
|
2002
|
+
def _error_record(error: BaseException) -> TrustedDerivedErrorRecord:
|
|
2003
|
+
stale_types = (
|
|
2004
|
+
InterruptedError,
|
|
2005
|
+
TrustedReadRequestCancelledError,
|
|
2006
|
+
TrustedLiveCancelledError,
|
|
2007
|
+
)
|
|
2008
|
+
transient_types = (
|
|
2009
|
+
_JobDeadlineExceeded,
|
|
2010
|
+
TimeoutError,
|
|
2011
|
+
TrustedReadQueueFullError,
|
|
2012
|
+
TrustedReadRequestDeadlineError,
|
|
2013
|
+
TrustedReadSessionFailedError,
|
|
2014
|
+
TrustedLiveBusyTimeoutError,
|
|
2015
|
+
TrustedLiveDeadlineExceededError,
|
|
2016
|
+
TrustedLiveSourceChangedError,
|
|
2017
|
+
)
|
|
2018
|
+
if isinstance(error, stale_types):
|
|
2019
|
+
category = TrustedDerivedErrorCategory.STALE
|
|
2020
|
+
elif isinstance(error, transient_types):
|
|
2021
|
+
category = TrustedDerivedErrorCategory.TRANSIENT
|
|
2022
|
+
else:
|
|
2023
|
+
category = TrustedDerivedErrorCategory.PERMANENT
|
|
2024
|
+
code = (
|
|
2025
|
+
type(error).__name__.encode("ascii", errors="replace")[:96].decode("ascii")
|
|
2026
|
+
)
|
|
2027
|
+
message = (
|
|
2028
|
+
str(error)
|
|
2029
|
+
.encode("utf-8", errors="replace")[:1024]
|
|
2030
|
+
.decode("utf-8", errors="ignore")
|
|
2031
|
+
)
|
|
2032
|
+
return TrustedDerivedErrorRecord(category, code, message)
|
|
2033
|
+
|
|
2034
|
+
@staticmethod
|
|
2035
|
+
def _observation_size(observation: TrustedDerivedSourceObservation) -> int:
|
|
2036
|
+
"""Conservatively bound every retained part of an observation.
|
|
2037
|
+
|
|
2038
|
+
This is an admission bound, not an estimate of CPython's allocator.
|
|
2039
|
+
Count scalar payloads plus fixed per-node/container overhead and stop as
|
|
2040
|
+
soon as the shared reuse budget is exceeded. In particular, retained
|
|
2041
|
+
run fields, parameter text, layout indexes, and byte-valued cells must
|
|
2042
|
+
not sit outside the byte-accounted LRU merely because sampled numeric
|
|
2043
|
+
cells are usually its largest component.
|
|
2044
|
+
"""
|
|
2045
|
+
|
|
2046
|
+
maximum = TRUSTED_DERIVED_MAX_REUSED_SOURCE_BYTES
|
|
2047
|
+
total = 2_048
|
|
2048
|
+
|
|
2049
|
+
def add_scalar(value: object) -> bool:
|
|
2050
|
+
nonlocal total
|
|
2051
|
+
if value is None or isinstance(value, bool):
|
|
2052
|
+
total += 16
|
|
2053
|
+
elif isinstance(value, (int, float)):
|
|
2054
|
+
total += 32
|
|
2055
|
+
elif isinstance(value, str):
|
|
2056
|
+
total += 49 + len(value.encode("utf-8", errors="surrogatepass"))
|
|
2057
|
+
elif isinstance(value, bytes):
|
|
2058
|
+
total += 33 + len(value)
|
|
2059
|
+
else:
|
|
2060
|
+
return False
|
|
2061
|
+
return total <= maximum
|
|
2062
|
+
|
|
2063
|
+
primitive_roots: list[object] = [
|
|
2064
|
+
observation.database_instance.logical_path,
|
|
2065
|
+
observation.database_instance.resolved_path,
|
|
2066
|
+
observation.database_instance.identity,
|
|
2067
|
+
tuple(observation.database_instance.sidecar_identities),
|
|
2068
|
+
observation.run_id,
|
|
2069
|
+
observation.run_guid,
|
|
2070
|
+
observation.service_namespace,
|
|
2071
|
+
observation.helper_incarnation,
|
|
2072
|
+
observation.data_version,
|
|
2073
|
+
observation.result_table_name,
|
|
2074
|
+
observation.result_columns,
|
|
2075
|
+
observation.result_schema_sha256,
|
|
2076
|
+
observation.result_watermark,
|
|
2077
|
+
observation.dependent_parameters,
|
|
2078
|
+
observation.planned_shape,
|
|
2079
|
+
observation.sample_columns,
|
|
2080
|
+
observation.sample_rows,
|
|
2081
|
+
observation.unsupported_reason,
|
|
2082
|
+
observation.run_fields,
|
|
2083
|
+
observation.progressive_layout_pending,
|
|
2084
|
+
observation.progressive_layout_cursor,
|
|
2085
|
+
]
|
|
2086
|
+
for parameter in observation.parameters:
|
|
2087
|
+
primitive_roots.append(
|
|
2088
|
+
(
|
|
2089
|
+
parameter.name,
|
|
2090
|
+
parameter.label,
|
|
2091
|
+
parameter.unit,
|
|
2092
|
+
parameter.depends_on,
|
|
2093
|
+
parameter.paramtype,
|
|
2094
|
+
)
|
|
2095
|
+
)
|
|
2096
|
+
for summary in observation.setpoint_summaries:
|
|
2097
|
+
primitive_roots.append(
|
|
2098
|
+
(summary.name, summary.first, summary.last, summary.steps)
|
|
2099
|
+
)
|
|
2100
|
+
for layout in observation.validated_2d_layouts:
|
|
2101
|
+
primitive_roots.append(
|
|
2102
|
+
(
|
|
2103
|
+
layout.dependent,
|
|
2104
|
+
layout.dependencies,
|
|
2105
|
+
layout.shape,
|
|
2106
|
+
layout.fast_axis_index,
|
|
2107
|
+
layout.first_row_id,
|
|
2108
|
+
layout.fast_id_stride,
|
|
2109
|
+
layout.slow_id_stride,
|
|
2110
|
+
layout.sample_slow_indexes,
|
|
2111
|
+
layout.sample_fast_indexes,
|
|
2112
|
+
layout.slow_reversed,
|
|
2113
|
+
layout.fast_reversed,
|
|
2114
|
+
layout.serpentine,
|
|
2115
|
+
layout.complete,
|
|
2116
|
+
layout.source,
|
|
2117
|
+
)
|
|
2118
|
+
)
|
|
2119
|
+
|
|
2120
|
+
stack = list(primitive_roots)
|
|
2121
|
+
nodes = 0
|
|
2122
|
+
while stack:
|
|
2123
|
+
value = stack.pop()
|
|
2124
|
+
nodes += 1
|
|
2125
|
+
if nodes > 262_144:
|
|
2126
|
+
return maximum + 1
|
|
2127
|
+
if isinstance(value, tuple):
|
|
2128
|
+
total += 40 + len(value) * 8
|
|
2129
|
+
if total > maximum:
|
|
2130
|
+
return total
|
|
2131
|
+
stack.extend(value)
|
|
2132
|
+
elif not add_scalar(value):
|
|
2133
|
+
return maximum + 1
|
|
2134
|
+
return total
|
|
2135
|
+
|
|
2136
|
+
@staticmethod
|
|
2137
|
+
def _validated_runs(
|
|
2138
|
+
runs: Sequence[TrustedDerivedRun],
|
|
2139
|
+
) -> tuple[TrustedDerivedRun, ...]:
|
|
2140
|
+
output = tuple(runs)
|
|
2141
|
+
if any(not isinstance(run, TrustedDerivedRun) for run in output):
|
|
2142
|
+
raise TypeError("runs must contain TrustedDerivedRun values.")
|
|
2143
|
+
if len({run.run_id for run in output}) != len(output):
|
|
2144
|
+
raise ValueError("run ids must be unique.")
|
|
2145
|
+
if len({run.run_guid for run in output}) != len(output):
|
|
2146
|
+
raise ValueError("run GUIDs must be unique.")
|
|
2147
|
+
return output
|
|
2148
|
+
|
|
2149
|
+
def _require_owner(self) -> None:
|
|
2150
|
+
if threading.get_ident() != self._owner_thread_id:
|
|
2151
|
+
raise RuntimeError(
|
|
2152
|
+
"TrustedWorkCoordinator must be used on its owner thread."
|
|
2153
|
+
)
|
|
2154
|
+
|
|
2155
|
+
def _require_open(self) -> None:
|
|
2156
|
+
if self._closed or self._scheduler.lifecycle is SchedulerLifecycle.CLOSED:
|
|
2157
|
+
raise RuntimeError("The trusted work coordinator is closed.")
|