seqevi 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {seqevi-0.2.0 → seqevi-0.3.0}/PKG-INFO +6 -3
- {seqevi-0.2.0 → seqevi-0.3.0}/README.md +5 -2
- {seqevi-0.2.0 → seqevi-0.3.0}/pyproject.toml +1 -1
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/__init__.py +1 -1
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/annotate.py +252 -31
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/cli.py +204 -1
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/distribution/oci.py +5 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/errors.py +12 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/evidence.py +99 -1
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/kits/dbcan-cazyme.toml +1 -1
- seqevi-0.3.0/src/seqevi/runner.py +571 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/service/__init__.py +2 -1
- seqevi-0.3.0/src/seqevi/service/app.py +695 -0
- seqevi-0.3.0/src/seqevi/service/config.py +82 -0
- seqevi-0.3.0/src/seqevi/service/persistence.py +1508 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/__init__.py +9 -1
- seqevi-0.3.0/src/seqevi/store/_artifact_reader.py +47 -0
- seqevi-0.3.0/src/seqevi/store/_resolver.py +35 -0
- seqevi-0.3.0/src/seqevi/store/client.py +1426 -0
- seqevi-0.3.0/src/seqevi/store/contract.py +77 -0
- seqevi-0.3.0/src/seqevi/store/local.py +1118 -0
- seqevi-0.3.0/src/seqevi/store/migration.py +1471 -0
- seqevi-0.3.0/src/seqevi/store/migrations/versions/0003_evidence_claim_leases.py +59 -0
- seqevi-0.3.0/src/seqevi/store/migrations/versions/0004_claim_sessions.py +89 -0
- seqevi-0.3.0/src/seqevi/store/schema.py +212 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/transport.py +167 -0
- seqevi-0.3.0/tests/test_acceptance_harnesses.py +1083 -0
- seqevi-0.3.0/tests/test_annotate.py +880 -0
- seqevi-0.3.0/tests/test_architecture_contracts.py +11 -0
- seqevi-0.3.0/tests/test_claim_leases.py +131 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_cli.py +65 -1
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_dbcan_image_assets.py +2 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_deployment_assets.py +19 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_local_store.py +222 -2
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_managed_setup.py +2 -2
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_oci.py +3 -1
- seqevi-0.3.0/tests/test_runner.py +830 -0
- seqevi-0.3.0/tests/test_shared_store.py +6593 -0
- seqevi-0.2.0/src/seqevi/runner.py +0 -163
- seqevi-0.2.0/src/seqevi/service/app.py +0 -213
- seqevi-0.2.0/src/seqevi/service/config.py +0 -38
- seqevi-0.2.0/src/seqevi/service/persistence.py +0 -360
- seqevi-0.2.0/src/seqevi/store/client.py +0 -311
- seqevi-0.2.0/src/seqevi/store/contract.py +0 -33
- seqevi-0.2.0/src/seqevi/store/local.py +0 -479
- seqevi-0.2.0/src/seqevi/store/migration.py +0 -62
- seqevi-0.2.0/src/seqevi/store/schema.py +0 -86
- seqevi-0.2.0/tests/test_annotate.py +0 -393
- seqevi-0.2.0/tests/test_runner.py +0 -81
- seqevi-0.2.0/tests/test_shared_store.py +0 -649
- {seqevi-0.2.0 → seqevi-0.3.0}/LICENSE +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/__main__.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/adapters/__init__.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/adapters/base.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/adapters/dbcan_cazyme.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/adapters/eggnog.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/adapters/interpro_pfam.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/adapters/registry.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/api.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/distribution/__init__.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/distribution/manifest.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/distribution/setup.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/execution_profile.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/hashing.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/kits/__init__.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/resource_lock.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/result.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/runtime_identity.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/sequence.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/artifact.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/factory.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/migrations/__init__.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/migrations/env.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/migrations/versions/0001_initial_store.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/migrations/versions/0002_artifact_byte_size_bigint.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/src/seqevi/store/migrations/versions/__init__.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/__init__.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/support.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_api.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_artifact_store.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_dbcan_cazyme.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_eggnog.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_evidence.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_execution_profile.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_incremental_reuse.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_interpro_pfam.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_resource_lock.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_result_prototype.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_runtime_identity.py +0 -0
- {seqevi-0.2.0 → seqevi-0.3.0}/tests/test_sequence.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seqevi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Content-addressed, reusable protein sequence annotation evidence.
|
|
5
5
|
Author-Email: Fuqing Zhang <fu.qing.zhang.work@gmail.com>, FuqingZhang <103730099+FuqingZh@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -45,11 +45,11 @@ single-file DuckDB result for the current FASTA.
|
|
|
45
45
|
## Status
|
|
46
46
|
|
|
47
47
|
The target architecture and v1.1 result contracts are approved. The SeqEvi
|
|
48
|
-
0.
|
|
48
|
+
0.3.0 source tree uses strict protein sequence identity, the single-host
|
|
49
49
|
SQLite/POSIX Store, the external tool runner, exact cache-miss orchestration,
|
|
50
50
|
and atomic DuckDB result materialization.
|
|
51
51
|
|
|
52
|
-
SeqEvi 0.
|
|
52
|
+
SeqEvi 0.3.0 provides managed setup for dbCAN only. The `eggnog` and
|
|
53
53
|
`interpro-pfam` adapters remain supported through explicit runtimes and named
|
|
54
54
|
host profiles; managed setup for them is later feature work. The
|
|
55
55
|
[Slice D gate record](docs/benchmarks/20260806-v1.4-dbcan-public-release-gate.md)
|
|
@@ -208,6 +208,9 @@ seqevi annotate \
|
|
|
208
208
|
|
|
209
209
|
Shared deployments expose the same Store contract:
|
|
210
210
|
|
|
211
|
+
The shared Store requires PostgreSQL 17 or newer so every mutation can enforce
|
|
212
|
+
one cumulative transaction deadline inside the claim lease runway.
|
|
213
|
+
|
|
211
214
|
```bash
|
|
212
215
|
seqevi serve \
|
|
213
216
|
--database-url postgresql+psycopg://seqevi@postgres/seqevi \
|
|
@@ -11,11 +11,11 @@ single-file DuckDB result for the current FASTA.
|
|
|
11
11
|
## Status
|
|
12
12
|
|
|
13
13
|
The target architecture and v1.1 result contracts are approved. The SeqEvi
|
|
14
|
-
0.
|
|
14
|
+
0.3.0 source tree uses strict protein sequence identity, the single-host
|
|
15
15
|
SQLite/POSIX Store, the external tool runner, exact cache-miss orchestration,
|
|
16
16
|
and atomic DuckDB result materialization.
|
|
17
17
|
|
|
18
|
-
SeqEvi 0.
|
|
18
|
+
SeqEvi 0.3.0 provides managed setup for dbCAN only. The `eggnog` and
|
|
19
19
|
`interpro-pfam` adapters remain supported through explicit runtimes and named
|
|
20
20
|
host profiles; managed setup for them is later feature work. The
|
|
21
21
|
[Slice D gate record](docs/benchmarks/20260806-v1.4-dbcan-public-release-gate.md)
|
|
@@ -174,6 +174,9 @@ seqevi annotate \
|
|
|
174
174
|
|
|
175
175
|
Shared deployments expose the same Store contract:
|
|
176
176
|
|
|
177
|
+
The shared Store requires PostgreSQL 17 or newer so every mutation can enforce
|
|
178
|
+
one cumulative transaction deadline inside the claim lease runway.
|
|
179
|
+
|
|
177
180
|
```bash
|
|
178
181
|
seqevi serve \
|
|
179
182
|
--database-url postgresql+psycopg://seqevi@postgres/seqevi \
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import sys
|
|
7
8
|
import tempfile
|
|
9
|
+
import threading
|
|
8
10
|
import time
|
|
9
|
-
from dataclasses import dataclass
|
|
10
11
|
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
11
13
|
from datetime import UTC, datetime
|
|
12
14
|
from itertools import batched
|
|
13
15
|
from pathlib import Path
|
|
@@ -17,13 +19,15 @@ from . import __version__
|
|
|
17
19
|
from .adapters.base import AdapterBatchResult, AnnotationAdapter
|
|
18
20
|
from .errors import AnnotationError, OutputPackageError
|
|
19
21
|
from .evidence import (
|
|
22
|
+
ClaimDisposition,
|
|
23
|
+
CommitOutcome,
|
|
20
24
|
EvidenceCommit,
|
|
21
25
|
EvidenceQuery,
|
|
22
26
|
EvidenceSource,
|
|
23
27
|
EvidenceStatus,
|
|
24
28
|
)
|
|
25
29
|
from .result import RESULT_FORMAT_VERSION, materialize_result_database
|
|
26
|
-
from .runner import ToolCommand, ToolRunResult, ToolRunner
|
|
30
|
+
from .runner import ToolCancelledError, ToolCommand, ToolRunResult, ToolRunner
|
|
27
31
|
from .sequence import (
|
|
28
32
|
SequenceIdentity,
|
|
29
33
|
iter_staged_identities,
|
|
@@ -31,15 +35,25 @@ from .sequence import (
|
|
|
31
35
|
iter_fasta_lines,
|
|
32
36
|
stage_fasta,
|
|
33
37
|
)
|
|
34
|
-
from .store.contract import
|
|
38
|
+
from .store.contract import (
|
|
39
|
+
ClaimSession,
|
|
40
|
+
EvidenceStore,
|
|
41
|
+
is_claim_session_capable_store,
|
|
42
|
+
)
|
|
35
43
|
|
|
36
44
|
_STORE_BATCH_SIZE = 1_000
|
|
37
45
|
_ANNOTATION_BATCH_SIZE = 10_000
|
|
46
|
+
_CLAIM_RUNWAY_SECONDS = 5.0
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
@dataclass(frozen=True, slots=True)
|
|
41
50
|
class AnnotationMetrics:
|
|
42
|
-
"""Operational measurements for one successful annotation invocation.
|
|
51
|
+
"""Operational measurements for one successful annotation invocation.
|
|
52
|
+
|
|
53
|
+
``existing_finalizations`` counts proposed terminal records that a
|
|
54
|
+
ClaimSession resolved to immutable evidence already finalized by a peer.
|
|
55
|
+
It is observational and does not change computed/cache provenance.
|
|
56
|
+
"""
|
|
43
57
|
|
|
44
58
|
elapsed_seconds: float
|
|
45
59
|
fasta_staging_seconds: float
|
|
@@ -56,6 +70,7 @@ class AnnotationMetrics:
|
|
|
56
70
|
tool_batches: int
|
|
57
71
|
unique_artifact_reads: int
|
|
58
72
|
configured_threads: int
|
|
73
|
+
existing_finalizations: int = 0
|
|
59
74
|
|
|
60
75
|
|
|
61
76
|
@dataclass(frozen=True, slots=True)
|
|
@@ -77,11 +92,19 @@ class _BatchMetrics:
|
|
|
77
92
|
commit_batches: int
|
|
78
93
|
adapter_seconds: float
|
|
79
94
|
store_commit_seconds: float
|
|
95
|
+
peer_completed_sequence_ids: frozenset[str] = frozenset()
|
|
96
|
+
existing_finalizations: int = 0
|
|
80
97
|
|
|
81
98
|
|
|
82
99
|
class _MeasuringToolRunner(ToolRunner):
|
|
83
|
-
def __init__(
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
delegate: ToolRunner,
|
|
103
|
+
*,
|
|
104
|
+
cancellation_signal: threading.Event | None = None,
|
|
105
|
+
) -> None:
|
|
84
106
|
self.delegate = delegate
|
|
107
|
+
self.cancellation_signal = cancellation_signal
|
|
85
108
|
self.duration_seconds = 0.0
|
|
86
109
|
|
|
87
110
|
def run(
|
|
@@ -89,8 +112,13 @@ class _MeasuringToolRunner(ToolRunner):
|
|
|
89
112
|
command: ToolCommand,
|
|
90
113
|
*,
|
|
91
114
|
timeout_seconds: float | None = None,
|
|
115
|
+
cancellation_signal: threading.Event | None = None,
|
|
92
116
|
) -> ToolRunResult:
|
|
93
|
-
result = self.delegate.run(
|
|
117
|
+
result = self.delegate.run(
|
|
118
|
+
command,
|
|
119
|
+
timeout_seconds=timeout_seconds,
|
|
120
|
+
cancellation_signal=cancellation_signal or self.cancellation_signal,
|
|
121
|
+
)
|
|
94
122
|
self.duration_seconds += result.duration_seconds
|
|
95
123
|
return result
|
|
96
124
|
|
|
@@ -138,11 +166,41 @@ def run_annotation(
|
|
|
138
166
|
store_lookup_seconds = 0.0
|
|
139
167
|
adapter_seconds = 0.0
|
|
140
168
|
store_commit_seconds = 0.0
|
|
169
|
+
existing_finalizations = 0
|
|
170
|
+
claim_session: ClaimSession | None = None
|
|
171
|
+
primary_failure: BaseException | None = None
|
|
172
|
+
close_failure: BaseException | None = None
|
|
173
|
+
published_output_identity: tuple[int, int] | None = None
|
|
174
|
+
published_output_marker: Path | None = None
|
|
175
|
+
|
|
176
|
+
def unlink_owned_output() -> None:
|
|
177
|
+
if published_output_identity is None or published_output_marker is None:
|
|
178
|
+
return
|
|
179
|
+
try:
|
|
180
|
+
current = output_dir.stat(follow_symlinks=False)
|
|
181
|
+
marker = published_output_marker.stat(follow_symlinks=False)
|
|
182
|
+
except FileNotFoundError:
|
|
183
|
+
return
|
|
184
|
+
if (current.st_dev, current.st_ino) == published_output_identity and (
|
|
185
|
+
marker.st_dev,
|
|
186
|
+
marker.st_ino,
|
|
187
|
+
) == published_output_identity:
|
|
188
|
+
output_dir.unlink()
|
|
189
|
+
|
|
141
190
|
try:
|
|
142
191
|
keys_by_sequence_id = {}
|
|
143
192
|
computed_ids: set[str] = set()
|
|
144
193
|
pending_misses: list[SequenceIdentity] = []
|
|
145
|
-
|
|
194
|
+
busy_queries: list[EvidenceQuery] = []
|
|
195
|
+
busy_retry_after: float | None = None
|
|
196
|
+
if is_claim_session_capable_store(store):
|
|
197
|
+
claim_session = store.claim_session().__enter__()
|
|
198
|
+
tool_runner = _MeasuringToolRunner(
|
|
199
|
+
runner or ToolRunner(),
|
|
200
|
+
cancellation_signal=(
|
|
201
|
+
claim_session.cancellation_signal if claim_session is not None else None
|
|
202
|
+
),
|
|
203
|
+
)
|
|
146
204
|
|
|
147
205
|
for identity_batch in batched(iter_staged_identities(stage), _STORE_BATCH_SIZE):
|
|
148
206
|
queries = tuple(
|
|
@@ -153,16 +211,43 @@ def run_annotation(
|
|
|
153
211
|
(query.identity.sequence_id, query.key) for query in queries
|
|
154
212
|
)
|
|
155
213
|
lookup_started = time.perf_counter()
|
|
156
|
-
|
|
214
|
+
if claim_session is not None:
|
|
215
|
+
claim_session.raise_if_lost()
|
|
216
|
+
decisions = claim_session.acquire_many(queries)
|
|
217
|
+
cached = {
|
|
218
|
+
query.key: decision.record
|
|
219
|
+
for query, decision in zip(queries, decisions, strict=True)
|
|
220
|
+
if decision.disposition is ClaimDisposition.CACHED
|
|
221
|
+
}
|
|
222
|
+
for query, decision in zip(queries, decisions, strict=True):
|
|
223
|
+
if decision.disposition is ClaimDisposition.ACQUIRED:
|
|
224
|
+
assert decision.claim is not None
|
|
225
|
+
pending_misses.append(query.identity)
|
|
226
|
+
elif decision.disposition is ClaimDisposition.BUSY:
|
|
227
|
+
busy_queries.append(query)
|
|
228
|
+
assert decision.busy is not None
|
|
229
|
+
busy_retry_after = (
|
|
230
|
+
decision.busy.retry_after_seconds
|
|
231
|
+
if busy_retry_after is None
|
|
232
|
+
else min(
|
|
233
|
+
busy_retry_after,
|
|
234
|
+
decision.busy.retry_after_seconds,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
cached = store.lookup_many(queries)
|
|
157
239
|
store_lookup_seconds += time.perf_counter() - lookup_started
|
|
158
240
|
lookup_batches += 1
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
241
|
+
if claim_session is None:
|
|
242
|
+
for query in queries:
|
|
243
|
+
if query.key not in cached:
|
|
244
|
+
pending_misses.append(query.identity)
|
|
163
245
|
while len(pending_misses) >= _ANNOTATION_BATCH_SIZE:
|
|
164
246
|
annotation_identities = tuple(pending_misses[:_ANNOTATION_BATCH_SIZE])
|
|
165
247
|
del pending_misses[:_ANNOTATION_BATCH_SIZE]
|
|
248
|
+
computed_ids.update(
|
|
249
|
+
identity.sequence_id for identity in annotation_identities
|
|
250
|
+
)
|
|
166
251
|
tool_batches += 1
|
|
167
252
|
batch_metrics = _run_annotation_batch(
|
|
168
253
|
identities=annotation_identities,
|
|
@@ -173,12 +258,18 @@ def run_annotation(
|
|
|
173
258
|
runner=tool_runner,
|
|
174
259
|
timeout_seconds=timeout_seconds,
|
|
175
260
|
threads=threads,
|
|
261
|
+
claim_session=claim_session,
|
|
176
262
|
)
|
|
177
263
|
commit_batches += batch_metrics.commit_batches
|
|
178
264
|
adapter_seconds += batch_metrics.adapter_seconds
|
|
179
265
|
store_commit_seconds += batch_metrics.store_commit_seconds
|
|
266
|
+
existing_finalizations += batch_metrics.existing_finalizations
|
|
267
|
+
computed_ids.difference_update(
|
|
268
|
+
batch_metrics.peer_completed_sequence_ids
|
|
269
|
+
)
|
|
180
270
|
|
|
181
271
|
if pending_misses:
|
|
272
|
+
computed_ids.update(identity.sequence_id for identity in pending_misses)
|
|
182
273
|
tool_batches += 1
|
|
183
274
|
batch_metrics = _run_annotation_batch(
|
|
184
275
|
identities=tuple(pending_misses),
|
|
@@ -189,14 +280,72 @@ def run_annotation(
|
|
|
189
280
|
runner=tool_runner,
|
|
190
281
|
timeout_seconds=timeout_seconds,
|
|
191
282
|
threads=threads,
|
|
283
|
+
claim_session=claim_session,
|
|
192
284
|
)
|
|
193
285
|
commit_batches += batch_metrics.commit_batches
|
|
194
286
|
adapter_seconds += batch_metrics.adapter_seconds
|
|
195
287
|
store_commit_seconds += batch_metrics.store_commit_seconds
|
|
288
|
+
existing_finalizations += batch_metrics.existing_finalizations
|
|
289
|
+
computed_ids.difference_update(batch_metrics.peer_completed_sequence_ids)
|
|
290
|
+
|
|
291
|
+
while busy_queries:
|
|
292
|
+
time.sleep(busy_retry_after or 1.0)
|
|
293
|
+
next_retry_after: float | None = None
|
|
294
|
+
next_busy = []
|
|
295
|
+
acquired = []
|
|
296
|
+
assert claim_session is not None
|
|
297
|
+
for busy_batch in batched(busy_queries, _STORE_BATCH_SIZE):
|
|
298
|
+
reacquire_started = time.perf_counter()
|
|
299
|
+
claim_session.raise_if_lost()
|
|
300
|
+
decisions = claim_session.acquire_many(busy_batch)
|
|
301
|
+
store_lookup_seconds += time.perf_counter() - reacquire_started
|
|
302
|
+
lookup_batches += 1
|
|
303
|
+
for query, decision in zip(busy_batch, decisions, strict=True):
|
|
304
|
+
if decision.disposition is ClaimDisposition.ACQUIRED:
|
|
305
|
+
assert decision.claim is not None
|
|
306
|
+
acquired.append(query.identity)
|
|
307
|
+
elif decision.disposition is ClaimDisposition.BUSY:
|
|
308
|
+
assert decision.busy is not None
|
|
309
|
+
next_retry_after = (
|
|
310
|
+
decision.busy.retry_after_seconds
|
|
311
|
+
if next_retry_after is None
|
|
312
|
+
else min(
|
|
313
|
+
next_retry_after,
|
|
314
|
+
decision.busy.retry_after_seconds,
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
next_busy.append(query)
|
|
318
|
+
for acquired_batch in batched(acquired, _ANNOTATION_BATCH_SIZE):
|
|
319
|
+
computed_ids.update(identity.sequence_id for identity in acquired_batch)
|
|
320
|
+
tool_batches += 1
|
|
321
|
+
batch_metrics = _run_annotation_batch(
|
|
322
|
+
identities=acquired_batch,
|
|
323
|
+
batch_number=tool_batches,
|
|
324
|
+
work_dir=work_dir,
|
|
325
|
+
adapter=adapter,
|
|
326
|
+
store=store,
|
|
327
|
+
runner=tool_runner,
|
|
328
|
+
timeout_seconds=timeout_seconds,
|
|
329
|
+
threads=threads,
|
|
330
|
+
claim_session=claim_session,
|
|
331
|
+
)
|
|
332
|
+
commit_batches += batch_metrics.commit_batches
|
|
333
|
+
adapter_seconds += batch_metrics.adapter_seconds
|
|
334
|
+
store_commit_seconds += batch_metrics.store_commit_seconds
|
|
335
|
+
existing_finalizations += batch_metrics.existing_finalizations
|
|
336
|
+
computed_ids.difference_update(
|
|
337
|
+
batch_metrics.peer_completed_sequence_ids
|
|
338
|
+
)
|
|
339
|
+
busy_queries = next_busy
|
|
340
|
+
busy_retry_after = next_retry_after
|
|
196
341
|
|
|
342
|
+
if claim_session is not None:
|
|
343
|
+
claim_session.raise_if_lost()
|
|
197
344
|
fetch_started = time.perf_counter()
|
|
198
345
|
fetched_by_key = store.fetch_many(keys_by_sequence_id.values())
|
|
199
346
|
store_fetch_seconds = time.perf_counter() - fetch_started
|
|
347
|
+
if claim_session is not None:
|
|
348
|
+
claim_session.raise_if_lost()
|
|
200
349
|
missing_keys = set(keys_by_sequence_id.values()) - fetched_by_key.keys()
|
|
201
350
|
if missing_keys:
|
|
202
351
|
raise AnnotationError(
|
|
@@ -223,8 +372,17 @@ def run_annotation(
|
|
|
223
372
|
metadata = dict(result_metadata or _default_result_metadata(adapter))
|
|
224
373
|
metadata["InputDigest"] = stage.input_digest
|
|
225
374
|
metadata.setdefault("CreatedAt", datetime.now(UTC).isoformat())
|
|
375
|
+
package_output = (
|
|
376
|
+
work_dir / "candidate-result.duckdb"
|
|
377
|
+
if claim_session is not None
|
|
378
|
+
else output_dir
|
|
379
|
+
)
|
|
380
|
+
if claim_session is not None:
|
|
381
|
+
claim_session.raise_if_lost()
|
|
382
|
+
if output_dir.exists():
|
|
383
|
+
raise AnnotationError(f"output path already exists: {output_dir}")
|
|
226
384
|
materialize_result_database(
|
|
227
|
-
output_path=
|
|
385
|
+
output_path=package_output,
|
|
228
386
|
records=iter_staged_records(stage),
|
|
229
387
|
input_record_count=stage.input_records,
|
|
230
388
|
fetched_by_sequence_id=fetched_by_sequence_id,
|
|
@@ -240,18 +398,56 @@ def run_annotation(
|
|
|
240
398
|
"computed": len(computed_ids),
|
|
241
399
|
"hits": statuses.count(EvidenceStatus.HIT),
|
|
242
400
|
"no_hits": statuses.count(EvidenceStatus.NO_HIT),
|
|
401
|
+
"existing_finalizations": existing_finalizations,
|
|
243
402
|
},
|
|
244
403
|
)
|
|
404
|
+
if claim_session is not None:
|
|
405
|
+
try:
|
|
406
|
+
claim_session.raise_if_lost()
|
|
407
|
+
candidate = package_output.stat(follow_symlinks=False)
|
|
408
|
+
published_output_identity = (candidate.st_dev, candidate.st_ino)
|
|
409
|
+
published_output_marker = package_output
|
|
410
|
+
os.link(package_output, output_dir)
|
|
411
|
+
claim_session.raise_if_lost()
|
|
412
|
+
except BaseException:
|
|
413
|
+
unlink_owned_output()
|
|
414
|
+
raise
|
|
245
415
|
package_seconds = time.perf_counter() - package_started
|
|
246
|
-
except
|
|
416
|
+
except BaseException as error:
|
|
417
|
+
primary_failure = error
|
|
418
|
+
if not isinstance(error, Exception):
|
|
419
|
+
raise
|
|
247
420
|
raise AnnotationError(
|
|
248
421
|
f"annotation failed; diagnostics retained at {work_dir}: {error}"
|
|
249
422
|
) from error
|
|
250
|
-
else:
|
|
251
|
-
shutil.rmtree(work_dir, ignore_errors=True)
|
|
252
423
|
finally:
|
|
424
|
+
if claim_session is not None:
|
|
425
|
+
try:
|
|
426
|
+
claim_session.__exit__(None, None, None)
|
|
427
|
+
except BaseException as close_error:
|
|
428
|
+
if primary_failure is None:
|
|
429
|
+
close_failure = close_error
|
|
430
|
+
else:
|
|
431
|
+
primary_failure.add_note(
|
|
432
|
+
f"ClaimSession cleanup also failed: {close_error!r}"
|
|
433
|
+
)
|
|
253
434
|
shutil.rmtree(stage.root, ignore_errors=True)
|
|
254
435
|
|
|
436
|
+
if close_failure is not None:
|
|
437
|
+
unlink_owned_output()
|
|
438
|
+
raise close_failure
|
|
439
|
+
if claim_session is not None:
|
|
440
|
+
try:
|
|
441
|
+
claim_session.raise_if_lost()
|
|
442
|
+
except BaseException as error:
|
|
443
|
+
unlink_owned_output()
|
|
444
|
+
if not isinstance(error, Exception):
|
|
445
|
+
raise
|
|
446
|
+
raise AnnotationError(
|
|
447
|
+
f"annotation failed; diagnostics retained at {work_dir}: {error}"
|
|
448
|
+
) from error
|
|
449
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
|
450
|
+
|
|
255
451
|
statuses = [fetched.record.status for fetched in fetched_by_sequence_id.values()]
|
|
256
452
|
artifact_digests = {
|
|
257
453
|
digest
|
|
@@ -286,6 +482,7 @@ def run_annotation(
|
|
|
286
482
|
tool_batches=tool_batches,
|
|
287
483
|
unique_artifact_reads=len(artifact_digests),
|
|
288
484
|
configured_threads=threads,
|
|
485
|
+
existing_finalizations=existing_finalizations,
|
|
289
486
|
),
|
|
290
487
|
)
|
|
291
488
|
|
|
@@ -340,36 +537,60 @@ def _run_annotation_batch(
|
|
|
340
537
|
runner: ToolRunner,
|
|
341
538
|
timeout_seconds: float | None,
|
|
342
539
|
threads: int,
|
|
540
|
+
claim_session: ClaimSession | None = None,
|
|
343
541
|
) -> _BatchMetrics:
|
|
344
542
|
batch_dir = work_dir / f"batch-{batch_number:06d}"
|
|
345
543
|
batch_dir.mkdir()
|
|
346
544
|
misses_fasta = batch_dir / "cache-misses.fasta"
|
|
545
|
+
if claim_session is not None:
|
|
546
|
+
claim_session.raise_if_lost()
|
|
347
547
|
with misses_fasta.open("w", encoding="ascii", newline="\n") as handle:
|
|
348
548
|
handle.writelines(iter_fasta_lines(identities))
|
|
349
549
|
adapter_started = time.perf_counter()
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
550
|
+
try:
|
|
551
|
+
batch = adapter.run_batch(
|
|
552
|
+
identities=identities,
|
|
553
|
+
input_fasta=misses_fasta,
|
|
554
|
+
work_dir=batch_dir,
|
|
555
|
+
runner=runner,
|
|
556
|
+
timeout_seconds=timeout_seconds,
|
|
557
|
+
threads=threads,
|
|
558
|
+
)
|
|
559
|
+
except ToolCancelledError:
|
|
560
|
+
if claim_session is not None:
|
|
561
|
+
claim_session.raise_if_lost()
|
|
562
|
+
raise
|
|
358
563
|
adapter_seconds = time.perf_counter() - adapter_started
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
adapter=adapter,
|
|
363
|
-
)
|
|
564
|
+
if claim_session is not None:
|
|
565
|
+
claim_session.raise_if_lost()
|
|
566
|
+
commits = _build_commits(batch=batch, identities=identities, adapter=adapter)
|
|
364
567
|
batches = 0
|
|
568
|
+
peer_completed_sequence_ids: set[str] = set()
|
|
569
|
+
existing_finalizations = 0
|
|
365
570
|
commit_started = time.perf_counter()
|
|
366
571
|
for commit_batch in batched(commits, _STORE_BATCH_SIZE):
|
|
367
|
-
|
|
368
|
-
|
|
572
|
+
if claim_session is None:
|
|
573
|
+
store.commit_many(commit_batch)
|
|
574
|
+
batches += 1
|
|
575
|
+
else:
|
|
576
|
+
claim_session.raise_if_lost()
|
|
577
|
+
outcomes = claim_session.finalize_many(commit_batch)
|
|
578
|
+
claim_session.raise_if_lost()
|
|
579
|
+
peer_completed_sequence_ids.update(
|
|
580
|
+
commit.identity.sequence_id
|
|
581
|
+
for commit, outcome in zip(commit_batch, outcomes, strict=True)
|
|
582
|
+
if outcome is CommitOutcome.EXISTING
|
|
583
|
+
)
|
|
584
|
+
existing_finalizations += sum(
|
|
585
|
+
outcome is CommitOutcome.EXISTING for outcome in outcomes
|
|
586
|
+
)
|
|
587
|
+
batches += 1
|
|
369
588
|
return _BatchMetrics(
|
|
370
589
|
commit_batches=batches,
|
|
371
590
|
adapter_seconds=adapter_seconds,
|
|
372
591
|
store_commit_seconds=time.perf_counter() - commit_started,
|
|
592
|
+
peer_completed_sequence_ids=frozenset(peer_completed_sequence_ids),
|
|
593
|
+
existing_finalizations=existing_finalizations,
|
|
373
594
|
)
|
|
374
595
|
|
|
375
596
|
|