Make the worker batch timeout configurable via LIDAR_BATCH_TIMEOUT

The pool of tile workers used to cancel every remaining tile after a
hardcoded 2-hour wall clock, silently truncating large batches (a
670-tile completion run lost its last 348 tiles that way). The timeout
now defaults to unlimited and can be capped per deployment with the
LIDAR_BATCH_TIMEOUT environment variable (seconds); the local worker
compose sets it to 6 hours.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-28 00:03:52 +02:00
parent 6e8580138c
commit d0dc8d90e9
4 changed files with 48 additions and 9 deletions

View File

@ -47,6 +47,22 @@ def resolve_workers(value):
return 1
def _batch_timeout_s():
"""Wall-clock budget for one batch, from LIDAR_BATCH_TIMEOUT (seconds).
Unset, empty or 0 means unlimited (default). A negative or non-numeric
value is ignored with a warning.
"""
raw = (os.environ.get("LIDAR_BATCH_TIMEOUT") or "").strip()
if not raw:
return 0.0
try:
return max(0.0, float(raw))
except ValueError:
logger.warning(f"LIDAR_BATCH_TIMEOUT={raw!r} is not a number — ignored (unlimited)")
return 0.0
def _file_basename(path):
"""Extract base name from a LAZ/LAS file, removing all known extensions.
@ -852,15 +868,17 @@ class LidarArchaeoPipeline:
for laz_file in files
}
done = 0
t_deadline = time.time() + 7200
budget_s = _batch_timeout_s()
deadline = time.time() + budget_s if budget_s > 0 else None
try:
# timeout=: without it, as_completed blocks between two
# completions and the 2 h deadline is never evaluated if
# no worker returns (run stuck forever).
# timeout= lets the deadline be checked between two
# completions; without it a stuck worker would block
# as_completed forever. No deadline (LIDAR_BATCH_TIMEOUT
# unset or 0) → wait indefinitely.
as_completed_kwargs = ({"timeout": max(1.0, deadline - time.time())}
if deadline is not None else {})
try:
futures_iter = as_completed(future_to_file,
timeout=max(1.0, t_deadline - time.time()))
for future in futures_iter:
for future in as_completed(future_to_file, **as_completed_kwargs):
laz_file = future_to_file[future]
done += 1
try:
@ -877,7 +895,8 @@ class LidarArchaeoPipeline:
"tile", "fail", detail=str(e))
results[laz_file.name] = False
except FuturesTimeoutError:
logger.error("Timeout exceeded (2 h) — cancelling remaining workers")
logger.error(f"Batch exceeded LIDAR_BATCH_TIMEOUT "
f"({budget_s:g} s) — cancelling remaining workers")
for f in future_to_file:
f.cancel()
except KeyboardInterrupt: