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.
Files changed (130) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +67 -0
  4. qplot/_build_info.json +1 -0
  5. qplot/_metadata.py +75 -0
  6. qplot/_repair.py +14 -0
  7. qplot/_shutdown_supervisor.py +4049 -0
  8. qplot/_version.py +12 -0
  9. qplot/_windows_shutdown_job.py +1166 -0
  10. qplot/configuration/__init__.py +3 -0
  11. qplot/configuration/config.py +578 -0
  12. qplot/configuration/config_schema.json +357 -0
  13. qplot/configuration/fonts.py +3 -0
  14. qplot/configuration/scripts.py +255 -0
  15. qplot/configuration/themes/__init__.py +13 -0
  16. qplot/configuration/themes/_base.py +592 -0
  17. qplot/configuration/themes/blank.py +17 -0
  18. qplot/configuration/themes/dark.py +54 -0
  19. qplot/configuration/themes/light.py +54 -0
  20. qplot/datahandling/LoadFromDB.py +224 -0
  21. qplot/datahandling/__init__.py +47 -0
  22. qplot/datahandling/_trusted_live_helper.py +812 -0
  23. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  24. qplot/datahandling/database.py +2340 -0
  25. qplot/datahandling/dimensions.py +43 -0
  26. qplot/datahandling/file_identity.py +461 -0
  27. qplot/datahandling/parameter_data.py +149 -0
  28. qplot/datahandling/plot_progress.py +17 -0
  29. qplot/datahandling/qcodes_cache.py +277 -0
  30. qplot/datahandling/qcodes_compat.py +14 -0
  31. qplot/datahandling/readSQL.py +2442 -0
  32. qplot/datahandling/readonly.py +2566 -0
  33. qplot/datahandling/source_activity.py +21 -0
  34. qplot/datahandling/trusted_array_preview.py +148 -0
  35. qplot/datahandling/trusted_derived_cache.py +1605 -0
  36. qplot/datahandling/trusted_derived_rendering.py +876 -0
  37. qplot/datahandling/trusted_heatmap.py +246 -0
  38. qplot/datahandling/trusted_live.py +2808 -0
  39. qplot/datahandling/trusted_live_queries.py +5907 -0
  40. qplot/datahandling/trusted_live_service.py +1919 -0
  41. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  42. qplot/datahandling/trusted_plot.py +283 -0
  43. qplot/datahandling/trusted_presentation.py +1065 -0
  44. qplot/datahandling/trusted_snapshot.py +1132 -0
  45. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  46. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  47. qplot/diagnostics.py +241 -0
  48. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  49. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  50. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  51. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  52. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  53. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  54. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  55. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  56. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  57. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  58. qplot/testdata.py +1804 -0
  59. qplot/tools/__init__.py +13 -0
  60. qplot/tools/finite_means.py +123 -0
  61. qplot/tools/general.py +139 -0
  62. qplot/tools/heatmap_geometry.py +391 -0
  63. qplot/tools/operation_registry.py +179 -0
  64. qplot/tools/plot_tools.py +650 -0
  65. qplot/tools/sample_statistics.py +111 -0
  66. qplot/tools/worker.py +2838 -0
  67. qplot/windows/__init__.py +9 -0
  68. qplot/windows/_colorbar.py +422 -0
  69. qplot/windows/_commands.py +523 -0
  70. qplot/windows/_config_persistence.py +123 -0
  71. qplot/windows/_database_actions.py +4933 -0
  72. qplot/windows/_dataset_handle.py +221 -0
  73. qplot/windows/_display_precision.py +48 -0
  74. qplot/windows/_dragdrop.py +70 -0
  75. qplot/windows/_export_paths.py +1167 -0
  76. qplot/windows/_help.py +210 -0
  77. qplot/windows/_mixed_appearance.py +303 -0
  78. qplot/windows/_mixed_plots.py +461 -0
  79. qplot/windows/_native_averaging.py +323 -0
  80. qplot/windows/_native_transforms.py +551 -0
  81. qplot/windows/_plot1d_snap.py +612 -0
  82. qplot/windows/_plot1d_traces.py +2236 -0
  83. qplot/windows/_plot2d_colorbar.py +1127 -0
  84. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  85. qplot/windows/_plot2d_layers.py +2291 -0
  86. qplot/windows/_plot2d_sweeps.py +712 -0
  87. qplot/windows/_plotWin.py +2082 -0
  88. qplot/windows/_plot_actions.py +2703 -0
  89. qplot/windows/_plot_annotation.py +186 -0
  90. qplot/windows/_plot_appearance.py +283 -0
  91. qplot/windows/_plot_axis_scaling.py +1521 -0
  92. qplot/windows/_plot_data_viewbox.py +94 -0
  93. qplot/windows/_plot_drop_axes.py +224 -0
  94. qplot/windows/_plot_export.py +1445 -0
  95. qplot/windows/_plot_feedback.py +112 -0
  96. qplot/windows/_plot_marquee.py +659 -0
  97. qplot/windows/_plot_refresh.py +950 -0
  98. qplot/windows/_plot_source_promotion.py +178 -0
  99. qplot/windows/_plot_state.py +237 -0
  100. qplot/windows/_plot_status.py +115 -0
  101. qplot/windows/_plot_tiling.py +35 -0
  102. qplot/windows/_plot_transform_labels.py +60 -0
  103. qplot/windows/_preferences.py +724 -0
  104. qplot/windows/_refresh_interval.py +172 -0
  105. qplot/windows/_run_controls.py +789 -0
  106. qplot/windows/_shortcuts.py +47 -0
  107. qplot/windows/_subplots/__init__.py +8 -0
  108. qplot/windows/_subplots/subplot1d.py +532 -0
  109. qplot/windows/_subplots/subplot2d.py +919 -0
  110. qplot/windows/_svg_export.py +209 -0
  111. qplot/windows/_trusted_derived_qt.py +1759 -0
  112. qplot/windows/_widgets/__init__.py +23 -0
  113. qplot/windows/_widgets/_run_formatting.py +387 -0
  114. qplot/windows/_widgets/details_tables.py +1119 -0
  115. qplot/windows/_widgets/dropbox.py +290 -0
  116. qplot/windows/_widgets/operations.py +460 -0
  117. qplot/windows/_widgets/preview.py +2997 -0
  118. qplot/windows/_widgets/run_list_items.py +488 -0
  119. qplot/windows/_widgets/toolbar.py +111 -0
  120. qplot/windows/_widgets/treeWidgets.py +2572 -0
  121. qplot/windows/_window_controls.py +414 -0
  122. qplot/windows/main.py +1632 -0
  123. qplot/windows/plot1d.py +313 -0
  124. qplot/windows/plot2d.py +1935 -0
  125. qplotter-1.6.0.dist-info/METADATA +322 -0
  126. qplotter-1.6.0.dist-info/RECORD +130 -0
  127. qplotter-1.6.0.dist-info/WHEEL +5 -0
  128. qplotter-1.6.0.dist-info/entry_points.txt +4 -0
  129. qplotter-1.6.0.dist-info/licenses/LICENSE +21 -0
  130. 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.")