worldarray 0.9.0__tar.gz → 0.9.1__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.
- {worldarray-0.9.0 → worldarray-0.9.1}/CHANGELOG.md +11 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/PKG-INFO +1 -1
- {worldarray-0.9.0 → worldarray-0.9.1}/docs/spec.md +11 -2
- {worldarray-0.9.0 → worldarray-0.9.1}/pyproject.toml +1 -1
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/__init__.py +1 -1
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/context.py +25 -6
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/test_access.py +29 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/uv.lock +1 -1
- {worldarray-0.9.0 → worldarray-0.9.1}/.github/workflows/ci.yml +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/.github/workflows/docs.yml +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/.github/workflows/publish.yml +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/.gitignore +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/LICENSE +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/README.md +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/docs/changelog.md +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/docs/index.md +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/mkdocs.yml +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/array.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/geometry.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/grid.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/levels.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/placement.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/resample.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/retry.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/store.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/units.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/src/worldarray/write.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/conftest.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/fixtures/metadata.json +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/fixtures/write.json +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/test_geometry.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/test_grid.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/test_lost_wakeup.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/test_metadata.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/test_read.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/test_resample.py +0 -0
- {worldarray-0.9.0 → worldarray-0.9.1}/tests/test_write.py +0 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.1 (2026-10-26)
|
|
4
|
+
|
|
5
|
+
A fix for exit. No placement change, no unit table change, and no default changes.
|
|
6
|
+
|
|
7
|
+
- The watchdog thread stops at exit, before the interpreter finalizes. On Python up to
|
|
8
|
+
3.13 a process could abort once all its work was done ("terminate called without an
|
|
9
|
+
active exception", exit 134) when finalization began while the thread was inside a
|
|
10
|
+
tensorstore call; an LSF job that did this looked failed. Now an `atexit` handler,
|
|
11
|
+
which runs once the program's other threads are joined, ends the thread and waits
|
|
12
|
+
for it, and no new one starts after that.
|
|
13
|
+
|
|
3
14
|
## 0.9.0 (2026-10-23)
|
|
4
15
|
|
|
5
16
|
A job script can size each worker's threads. No placement change, no unit table change, and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: worldarray
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only.
|
|
5
5
|
Project-URL: Homepage, https://janeliascicomp.github.io/worldarray/
|
|
6
6
|
Project-URL: Documentation, https://janeliascicomp.github.io/worldarray/
|
|
@@ -692,7 +692,15 @@ this document and that one differ, fix one of them.
|
|
|
692
692
|
copy and a local read of a key that does not exist; no store is touched, about 0.3 ms
|
|
693
693
|
of CPU). A stranded task then runs within about a second whoever waits on it: the
|
|
694
694
|
package, `vol.ts` directly, or `await`, at any concurrency. The retry stays the outer
|
|
695
|
-
net.
|
|
695
|
+
net. The thread stops at exit (0.9.1): an `atexit` handler, which Python runs once the
|
|
696
|
+
program's other threads are joined and before it finalizes, ends it and waits for it
|
|
697
|
+
(at most the longest one kick takes), and no new one starts after that. Python up to
|
|
698
|
+
3.13 ends a daemon thread that takes the GIL back during finalization with
|
|
699
|
+
`pthread_exit`, and inside a tensorstore call that aborts the process with "terminate
|
|
700
|
+
called without an active exception" and exit 134 after all the work is done (once in
|
|
701
|
+
six of mesh-n-bone's Linux CI runs on Python 3.12, 2026-10-26; every time when a
|
|
702
|
+
process exits while the thread kicks flat out). Open question 14 says when the
|
|
703
|
+
watchdog goes.
|
|
696
704
|
|
|
697
705
|
7. **Contexts.** Without `context`, every open in the process shares `shared_context(0)`:
|
|
698
706
|
tensorstore's cache is off and the levels of one pyramid share one context. Its
|
|
@@ -1089,7 +1097,8 @@ through a pickle, and a create whose first attempt lands after its timeout adopt
|
|
|
1089
1097
|
array under `exists="error"`. 0.9: `WORLDARRAY_THREADS` ahead of every other variable,
|
|
1090
1098
|
blank as unset and a bad value refused; `file_io_floor` sizing file I/O under the budget,
|
|
1091
1099
|
ignored with a thread count, and carried through a pickle; a handle pickled on the driver
|
|
1092
|
-
sized in a spawned worker by that worker's `WORLDARRAY_THREADS`.
|
|
1100
|
+
sized in a spawned worker by that worker's `WORLDARRAY_THREADS`. 0.9.1: a process whose
|
|
1101
|
+
finalization takes a while, exiting while the watchdog kicks flat out, exits cleanly.
|
|
1093
1102
|
Opt-in (`WORLDARRAY_STRESS=1`, about a minute,
|
|
1094
1103
|
`tests/test_lost_wakeup.py`): worker processes aim tasks at the moment a one-thread pool's
|
|
1095
1104
|
worker exits idle, so a plain context strands some for good (13 of about 7,000 on
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "worldarray"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -21,11 +21,11 @@ every open today; pass the same object twice to share pools (spec, Behaviour 7).
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
+
import atexit
|
|
24
25
|
import json
|
|
25
26
|
import os
|
|
26
27
|
import tempfile
|
|
27
28
|
import threading
|
|
28
|
-
import time
|
|
29
29
|
import weakref
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
@@ -50,12 +50,19 @@ _lock = threading.Lock()
|
|
|
50
50
|
# `gh api repos/google/tensorstore/compare/ed51718085...vX.Y.Z` says "ahead" or "identical"
|
|
51
51
|
# for a release that has it, and `WORLDARRAY_STRESS=1 pytest tests/test_lost_wakeup.py -s`
|
|
52
52
|
# then strands nothing on a plain context.
|
|
53
|
+
#
|
|
54
|
+
# The thread stops at exit, before the interpreter finalizes. Python up to 3.13 ends a
|
|
55
|
+
# daemon thread that takes the GIL back during finalization with pthread_exit, and inside
|
|
56
|
+
# a tensorstore call that aborts the process: "terminate called without an active
|
|
57
|
+
# exception", exit 134, once all the work is done (mesh-n-bone's CI, 2026-10-26).
|
|
53
58
|
KICK_EVERY = 1.0
|
|
59
|
+
KICK_TIMEOUT = 10.0
|
|
54
60
|
_KICK_DIR = tempfile.gettempdir()
|
|
55
61
|
_KICK_KEY = "worldarray-kick-no-such-key"
|
|
56
62
|
_watched: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary()
|
|
57
63
|
_watch_lock = threading.Lock()
|
|
58
64
|
_watchdog: threading.Thread | None = None
|
|
65
|
+
_stop = threading.Event()
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
def _kickers(context: ts.Context) -> tuple:
|
|
@@ -63,7 +70,7 @@ def _kickers(context: ts.Context) -> tuple:
|
|
|
63
70
|
decoding pool, reading a missing key from the other runs on its file I/O pool."""
|
|
64
71
|
array = ts.open({"driver": "array", "array": [0], "dtype": "uint8"}, context=context)
|
|
65
72
|
kv = ts.KvStore.open({"driver": "file", "path": _KICK_DIR + "/"}, context=context)
|
|
66
|
-
return array.result(timeout=
|
|
73
|
+
return array.result(timeout=KICK_TIMEOUT), kv.result(timeout=KICK_TIMEOUT)
|
|
67
74
|
|
|
68
75
|
|
|
69
76
|
def kick(context: ts.Context) -> list:
|
|
@@ -86,14 +93,15 @@ def kick(context: ts.Context) -> list:
|
|
|
86
93
|
|
|
87
94
|
def watch(context: ts.Context | None) -> ts.Context | None:
|
|
88
95
|
"""Kick ``context``'s pools every ``KICK_EVERY`` seconds for as long as it lives, starting
|
|
89
|
-
the watchdog thread on first use. Returns
|
|
96
|
+
the watchdog thread on first use (not once it has stopped at exit). Returns
|
|
97
|
+
``context``."""
|
|
90
98
|
global _watchdog
|
|
91
99
|
if context is None:
|
|
92
100
|
return None
|
|
93
101
|
with _watch_lock:
|
|
94
102
|
if context not in _watched:
|
|
95
103
|
_watched[context] = None
|
|
96
|
-
if _watchdog is None or not _watchdog.is_alive():
|
|
104
|
+
if not _stop.is_set() and (_watchdog is None or not _watchdog.is_alive()):
|
|
97
105
|
_watchdog = threading.Thread(target=_watch, name="worldarray-kick", daemon=True)
|
|
98
106
|
_watchdog.start()
|
|
99
107
|
return context
|
|
@@ -101,16 +109,26 @@ def watch(context: ts.Context | None) -> ts.Context | None:
|
|
|
101
109
|
|
|
102
110
|
def _watch() -> None:
|
|
103
111
|
held: list = []
|
|
104
|
-
while
|
|
105
|
-
time.sleep(KICK_EVERY)
|
|
112
|
+
while not _stop.wait(KICK_EVERY):
|
|
106
113
|
with _watch_lock:
|
|
107
114
|
contexts = list(_watched.keys())
|
|
108
115
|
held = [f for f in held if not f.done()]
|
|
109
116
|
for context in contexts:
|
|
117
|
+
if _stop.is_set():
|
|
118
|
+
break
|
|
110
119
|
held += kick(context)
|
|
111
120
|
del contexts
|
|
112
121
|
|
|
113
122
|
|
|
123
|
+
def _stop_watchdog() -> None:
|
|
124
|
+
"""End every watchdog thread and wait for it, at exit: atexit runs once the program's
|
|
125
|
+
other threads are joined, so they are kicked to the end, and before finalization."""
|
|
126
|
+
_stop.set()
|
|
127
|
+
for thread in threading.enumerate():
|
|
128
|
+
if thread.name == "worldarray-kick" and thread is not threading.current_thread():
|
|
129
|
+
thread.join(2 * KICK_TIMEOUT) # the longest one kick takes: two opens
|
|
130
|
+
|
|
131
|
+
|
|
114
132
|
def _forget_watchdog() -> None:
|
|
115
133
|
global _watchdog
|
|
116
134
|
_watchdog = None # a forked child has no such thread; the next watch() starts one
|
|
@@ -118,6 +136,7 @@ def _forget_watchdog() -> None:
|
|
|
118
136
|
|
|
119
137
|
if hasattr(os, "register_at_fork"):
|
|
120
138
|
os.register_at_fork(after_in_child=_forget_watchdog)
|
|
139
|
+
atexit.register(_stop_watchdog)
|
|
121
140
|
|
|
122
141
|
|
|
123
142
|
THREADS_VAR = "WORLDARRAY_THREADS"
|
|
@@ -8,6 +8,9 @@ import http.server
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import socket
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import textwrap
|
|
11
14
|
import threading
|
|
12
15
|
import time
|
|
13
16
|
import weakref
|
|
@@ -205,6 +208,32 @@ def test_every_context_the_package_touches_is_kicked(monkeypatch, tmp_path):
|
|
|
205
208
|
assert len(wa.context._watched) == n - 1 # a context that died is no longer watched
|
|
206
209
|
|
|
207
210
|
|
|
211
|
+
def test_a_process_exits_cleanly_while_the_watchdog_kicks(tmp_path):
|
|
212
|
+
# Python up to 3.13 ends a daemon thread that takes the GIL back during finalization
|
|
213
|
+
# with pthread_exit, which aborts inside a tensorstore call; the watchdog stops at exit
|
|
214
|
+
script = tmp_path / "exit.py"
|
|
215
|
+
script.write_text(
|
|
216
|
+
textwrap.dedent("""
|
|
217
|
+
import time
|
|
218
|
+
import tensorstore as ts
|
|
219
|
+
from worldarray import context
|
|
220
|
+
context.KICK_EVERY = 0.0 # kick flat out
|
|
221
|
+
limits = [{"cache_pool": {"total_bytes_limit": i}} for i in range(20)]
|
|
222
|
+
contexts = [context.watch(ts.Context(spec)) for spec in limits]
|
|
223
|
+
class SlowExit: # finalization takes a while, with the GIL free
|
|
224
|
+
def __del__(self):
|
|
225
|
+
time.sleep(0.5)
|
|
226
|
+
slow = SlowExit()
|
|
227
|
+
time.sleep(0.2)
|
|
228
|
+
""")
|
|
229
|
+
)
|
|
230
|
+
done = subprocess.run(
|
|
231
|
+
[sys.executable, str(script)], capture_output=True, text=True, timeout=120
|
|
232
|
+
)
|
|
233
|
+
assert done.returncode == 0, done.stderr
|
|
234
|
+
assert "terminate" not in done.stderr
|
|
235
|
+
|
|
236
|
+
|
|
208
237
|
class _StalledView:
|
|
209
238
|
"""A view whose first ``stalls`` reads never finish (a future nothing resolves, as a
|
|
210
239
|
read stuck on NFS looks from Python); later reads return ``0, 1, 2, 3``."""
|
|
@@ -1247,7 +1247,7 @@ wheels = [
|
|
|
1247
1247
|
|
|
1248
1248
|
[[package]]
|
|
1249
1249
|
name = "worldarray"
|
|
1250
|
-
version = "0.9.
|
|
1250
|
+
version = "0.9.1"
|
|
1251
1251
|
source = { editable = "." }
|
|
1252
1252
|
dependencies = [
|
|
1253
1253
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|