shareddata 6.83.21__tar.gz → 6.83.22__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.
- {shareddata-6.83.21/src/shareddata.egg-info → shareddata-6.83.22}/PKG-INFO +1 -1
- {shareddata-6.83.21 → shareddata-6.83.22}/setup.py +1 -1
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Defaults.py +30 -5
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/MongoDBClient.py +1 -1
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Logger.py +11 -7
- shareddata-6.83.22/src/SharedData/Routines/UpdateActiveJobs.py +75 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/Worker.py +1 -1
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/WorkerPool.py +76 -81
- {shareddata-6.83.21 → shareddata-6.83.22/src/shareddata.egg-info}/PKG-INFO +1 -1
- shareddata-6.83.21/src/SharedData/Routines/UpdateActiveJobs.py +0 -53
- {shareddata-6.83.21 → shareddata-6.83.22}/LICENSE +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/MANIFEST.in +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/README.md +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/pyproject.toml +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/setup.cfg +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/__init__.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/auth.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/download_paths.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/download_slots.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/cache.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/collections.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/downloads.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/tables.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/timeseries.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Database.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ClientAPI.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/SaveTables.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/Schedule.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/WorkerLib.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/SharedData.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/SharedNumpy.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Table.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableDisk.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndex.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitFunctions.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitGenerate.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Users.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Utils.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/__init__.py +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/shareddata.egg-info/SOURCES.txt +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.83.21 → shareddata-6.83.22}/src/shareddata.egg-info/top_level.txt +0 -0
|
@@ -34,7 +34,7 @@ install_requires = [
|
|
|
34
34
|
|
|
35
35
|
setup(
|
|
36
36
|
name='shareddata',
|
|
37
|
-
version='6.83.
|
|
37
|
+
version='6.83.22',
|
|
38
38
|
description='Memory Mapped / Shared Memory Database with S3 repository',
|
|
39
39
|
long_description=open('README.md').read(),
|
|
40
40
|
long_description_content_type='text/markdown',
|
|
@@ -221,6 +221,27 @@ def _classify_on_mounted_device(resolved_path, current_mounts, declared_mounts):
|
|
|
221
221
|
return True
|
|
222
222
|
|
|
223
223
|
|
|
224
|
+
def _windows_folder_is_live(folder: str) -> bool:
|
|
225
|
+
"""Windows has no fstab/mountinfo stub semantics; an existing dir is live."""
|
|
226
|
+
try:
|
|
227
|
+
return bool(folder) and Path(folder).is_dir()
|
|
228
|
+
except (OSError, TypeError):
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_filesystem_root(current) -> bool:
|
|
233
|
+
"""True for POSIX ``/``, Windows drive roots (``C:\\``), and UNC share roots.
|
|
234
|
+
|
|
235
|
+
``Path('C:\\\\').parent`` is itself and ``os.path.ismount('C:\\\\')`` is
|
|
236
|
+
True, so the st_dev==parent_dev stub heuristic must not treat a drive
|
|
237
|
+
letter as an unmounted mountpoint.
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
return current == current.parent or current == type(current)(current.anchor)
|
|
241
|
+
except (OSError, TypeError, ValueError):
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
224
245
|
def _is_on_mounted_device_legacy(folder: str) -> bool:
|
|
225
246
|
"""Original ``os.path.ismount()`` st_dev heuristic — fallback for platforms
|
|
226
247
|
without ``/proc/self/mountinfo``. Cannot detect an unmounted mountpoint
|
|
@@ -232,20 +253,20 @@ def _is_on_mounted_device_legacy(folder: str) -> bool:
|
|
|
232
253
|
while True:
|
|
233
254
|
if os.path.ismount(current):
|
|
234
255
|
try:
|
|
256
|
+
# Drive / UNC / POSIX root is a live filesystem, not a stub.
|
|
257
|
+
if _is_filesystem_root(current):
|
|
258
|
+
return True
|
|
235
259
|
dev = current.stat().st_dev
|
|
236
260
|
parent_dev = current.parent.stat().st_dev
|
|
237
261
|
if dev != parent_dev:
|
|
238
262
|
return True # device is mounted
|
|
239
263
|
else:
|
|
240
264
|
# Mountpoint stub — device not mounted.
|
|
241
|
-
# But if this is /, it's the root fs (always live).
|
|
242
|
-
if current == Path('/'):
|
|
243
|
-
return True
|
|
244
265
|
return False
|
|
245
266
|
except OSError:
|
|
246
267
|
return False
|
|
247
268
|
if current == current.parent:
|
|
248
|
-
# Reached
|
|
269
|
+
# Reached filesystem root without finding a mountpoint — live.
|
|
249
270
|
return True
|
|
250
271
|
current = current.parent
|
|
251
272
|
except (OSError, TypeError):
|
|
@@ -265,11 +286,15 @@ def _is_on_mounted_device(folder: str) -> bool:
|
|
|
265
286
|
detection (writes silently landing on ``/`` instead of the intended disk).
|
|
266
287
|
|
|
267
288
|
Falls back to the legacy ``os.path.ismount()`` heuristic when mountinfo is
|
|
268
|
-
unavailable (non-Linux).
|
|
289
|
+
unavailable (non-Linux). On Windows there is no fstab/mountinfo stub
|
|
290
|
+
semantics — an existing directory is live (drive roots are not unmounted
|
|
291
|
+
mountpoint stubs).
|
|
269
292
|
"""
|
|
270
293
|
try:
|
|
271
294
|
if not folder:
|
|
272
295
|
return False
|
|
296
|
+
if os.name == 'nt':
|
|
297
|
+
return _windows_folder_is_live(folder)
|
|
273
298
|
resolved = str(Path(folder).resolve())
|
|
274
299
|
current = _current_mount_points()
|
|
275
300
|
if current is None:
|
|
@@ -340,14 +340,18 @@ class Logger:
|
|
|
340
340
|
Returns:
|
|
341
341
|
pandas.DataFrame: The updated DataFrame containing processed log entries.
|
|
342
342
|
"""
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
# Serialize cursor + dflogs mutation: gunicorn gthread workers run
|
|
344
|
+
# concurrent request threads that would otherwise race _logfileposition
|
|
345
|
+
# / dflogs and serve inconsistent tails to UI polls.
|
|
346
|
+
with Logger.lock:
|
|
347
|
+
dfnewlines_sorted = Logger.readLogs()
|
|
348
|
+
if dfnewlines_sorted is None or len(dfnewlines_sorted) == 0:
|
|
349
|
+
return Logger.dflogs
|
|
350
|
+
|
|
351
|
+
Logger.getLastLog(dfnewlines_sorted)
|
|
352
|
+
Logger.getStatus(dfnewlines_sorted)
|
|
353
|
+
Logger.handleHeartbeats(keep_latest_heartbeat)
|
|
345
354
|
return Logger.dflogs
|
|
346
|
-
|
|
347
|
-
Logger.getLastLog(dfnewlines_sorted)
|
|
348
|
-
Logger.getStatus(dfnewlines_sorted)
|
|
349
|
-
Logger.handleHeartbeats(keep_latest_heartbeat)
|
|
350
|
-
return Logger.dflogs
|
|
351
355
|
|
|
352
356
|
@staticmethod
|
|
353
357
|
def readLogs():
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Scheduler-supervised realtime WorkerPool job-state reconciler."""
|
|
2
|
+
|
|
3
|
+
import faulthandler
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from SharedData.Logger import Logger
|
|
7
|
+
from SharedData.Routines.WorkerPool import WorkerPool
|
|
8
|
+
|
|
9
|
+
faulthandler.enable()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _wait_with_heartbeats(delay, heartbeat_interval, error_count, sleep):
|
|
13
|
+
"""Wait without letting Scheduler supervision go stale."""
|
|
14
|
+
remaining = delay
|
|
15
|
+
while remaining > 0:
|
|
16
|
+
wait_for = min(heartbeat_interval, remaining)
|
|
17
|
+
sleep(wait_for)
|
|
18
|
+
remaining -= wait_for
|
|
19
|
+
if remaining > 0:
|
|
20
|
+
Logger.log.debug(f'#heartbeat#errors={error_count}')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(
|
|
24
|
+
shdata,
|
|
25
|
+
interval=15,
|
|
26
|
+
retry_interval=60,
|
|
27
|
+
max_errors=10,
|
|
28
|
+
sleep=time.sleep,
|
|
29
|
+
):
|
|
30
|
+
"""Reconcile active jobs forever while publishing scheduler heartbeats."""
|
|
31
|
+
if interval <= 0 or retry_interval <= 0:
|
|
32
|
+
raise ValueError('intervals must be greater than zero')
|
|
33
|
+
if max_errors < 1:
|
|
34
|
+
raise ValueError('max_errors must be at least one')
|
|
35
|
+
|
|
36
|
+
Logger.log.info('ROUTINE STARTED!')
|
|
37
|
+
error_count = 0
|
|
38
|
+
|
|
39
|
+
while True:
|
|
40
|
+
delay = interval
|
|
41
|
+
try:
|
|
42
|
+
counts = WorkerPool.update_active_jobs_once(shdata)
|
|
43
|
+
error_count = 0
|
|
44
|
+
if counts['pending'] or counts['timed_out'] or counts['removed']:
|
|
45
|
+
Logger.log.info(
|
|
46
|
+
'WorkerPool jobs status: '
|
|
47
|
+
f"scanned={counts['scanned']},pending={counts['pending']},"
|
|
48
|
+
f"timed_out={counts['timed_out']},removed={counts['removed']}"
|
|
49
|
+
)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
error_count += 1
|
|
52
|
+
delay = retry_interval
|
|
53
|
+
Logger.log.error(f'Error in update_active_jobs_once: {e}')
|
|
54
|
+
if error_count >= max_errors:
|
|
55
|
+
Logger.log.error(
|
|
56
|
+
f'Max errors ({max_errors}) reached, exiting'
|
|
57
|
+
)
|
|
58
|
+
raise SystemExit(1)
|
|
59
|
+
|
|
60
|
+
Logger.log.debug(f'#heartbeat#errors={error_count}')
|
|
61
|
+
_wait_with_heartbeats(delay, interval, error_count, sleep)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
from SharedData.SharedData import SharedData
|
|
66
|
+
|
|
67
|
+
shdata = SharedData(
|
|
68
|
+
'SharedData.Routines.UpdateActiveJobs',
|
|
69
|
+
user='master',
|
|
70
|
+
)
|
|
71
|
+
run(shdata)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == '__main__':
|
|
75
|
+
main()
|
|
@@ -330,4 +330,4 @@ while True:
|
|
|
330
330
|
)
|
|
331
331
|
# Logger.log.debug(f'#heartbeat# {nroutines}routines,{running_batch_jobs}/{MAX_BATCH_JOBS}jobs,{completed_batch_jobs}completed,{error_batch_jobs}errors,{SHAREDDATA_VERSION}')
|
|
332
332
|
|
|
333
|
-
time.sleep(SLEEP_TIME * np.random.rand())
|
|
333
|
+
time.sleep(SLEEP_TIME * np.random.rand())
|
|
@@ -406,139 +406,134 @@ class WorkerPool:
|
|
|
406
406
|
return fetched_jobs
|
|
407
407
|
|
|
408
408
|
@staticmethod
|
|
409
|
-
def update_active_jobs_once(shdata
|
|
410
|
-
"""
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
error, so a failed pass never blocks other JOBS readers/writers.
|
|
416
|
-
|
|
417
|
-
Extracted from the former update_active_jobs() so it can run as the
|
|
418
|
-
body of a standalone routine (SharedData.Routines.UpdateActiveJobs)
|
|
419
|
-
instead of an in-process Worker thread — see that module for the
|
|
420
|
-
owning loop, heartbeat, and MAX_ERRORS handling.
|
|
409
|
+
def update_active_jobs_once(shdata, now=None):
|
|
410
|
+
"""Run one atomic WorkerPool job-state reconciliation pass.
|
|
411
|
+
|
|
412
|
+
NEW/WAITING jobs become PENDING once due and all dependencies are
|
|
413
|
+
COMPLETED. FETCHED/RUNNING jobs become ERROR after one hour. An absent
|
|
414
|
+
ACTIVE_JOBS cache is a normal empty-queue condition.
|
|
421
415
|
"""
|
|
422
|
-
|
|
416
|
+
tnow = (
|
|
417
|
+
pd.Timestamp.utcnow().tz_localize(None)
|
|
418
|
+
if now is None
|
|
419
|
+
else pd.Timestamp(now)
|
|
420
|
+
)
|
|
421
|
+
one_hour_ago = tnow - pd.Timedelta(hours=1)
|
|
422
|
+
counts = {
|
|
423
|
+
'scanned': 0,
|
|
424
|
+
'pending': 0,
|
|
425
|
+
'timed_out': 0,
|
|
426
|
+
'removed': 0,
|
|
427
|
+
}
|
|
428
|
+
|
|
423
429
|
job_table = WorkerPool.get_job_table(shdata)
|
|
430
|
+
job_table.acquire()
|
|
424
431
|
try:
|
|
425
|
-
tnow = pd.Timestamp.utcnow().tz_localize(None)
|
|
426
|
-
one_hour_ago = tnow - pd.Timedelta(hours=1)
|
|
427
|
-
|
|
428
|
-
job_table.acquire()
|
|
429
|
-
isacquired = True
|
|
430
|
-
|
|
431
|
-
# Get active jobs cache
|
|
432
432
|
buff = np.full((1,), dtype=job_table.dtype, fill_value=np.nan)
|
|
433
433
|
buff['hash'][0] = 'ACTIVE_JOBS'
|
|
434
434
|
loc = job_table.get_loc(buff, acquire=False)
|
|
435
435
|
if loc[0] == -1:
|
|
436
|
-
return
|
|
436
|
+
return counts
|
|
437
437
|
|
|
438
|
-
active_jobs = job_table.get_dict_list(
|
|
438
|
+
active_jobs = job_table.get_dict_list(
|
|
439
|
+
job_table[loc[0]], acquire=False
|
|
440
|
+
)[0]
|
|
439
441
|
modified = False
|
|
440
|
-
|
|
441
|
-
|
|
442
|
+
if isinstance(active_jobs.get('jobs'), list):
|
|
443
|
+
active_jobs['jobs'] = {}
|
|
444
|
+
modified = True
|
|
445
|
+
|
|
446
|
+
started_at = time.monotonic()
|
|
447
|
+
active_statuses = {'NEW', 'WAITING', 'PENDING', 'FETCHED', 'RUNNING'}
|
|
442
448
|
|
|
443
|
-
ti = time.time()
|
|
444
|
-
# Process each active job
|
|
445
449
|
for job_hash, job in list(active_jobs['jobs'].items()):
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
450
|
+
counts['scanned'] += 1
|
|
451
|
+
|
|
452
|
+
if job['status'] not in active_statuses:
|
|
453
|
+
del active_jobs['jobs'][job_hash]
|
|
449
454
|
modified = True
|
|
455
|
+
counts['removed'] += 1
|
|
450
456
|
continue
|
|
451
457
|
|
|
452
|
-
|
|
453
|
-
|
|
458
|
+
if (
|
|
459
|
+
job['status'] in {'FETCHED', 'RUNNING'}
|
|
460
|
+
and job['mtime'] < one_hour_ago
|
|
461
|
+
):
|
|
454
462
|
job['status'] = 'ERROR'
|
|
455
463
|
job['stderr'] = 'timeout'
|
|
456
464
|
job['mtime'] = tnow
|
|
457
465
|
job_table.upsert(job, acquire=False)
|
|
458
|
-
del active_jobs['jobs'][job_hash]
|
|
466
|
+
del active_jobs['jobs'][job_hash]
|
|
459
467
|
modified = True
|
|
460
|
-
|
|
468
|
+
counts['timed_out'] += 1
|
|
469
|
+
counts['removed'] += 1
|
|
461
470
|
continue
|
|
462
471
|
|
|
463
|
-
|
|
464
|
-
if job['status'] not in ['NEW', 'WAITING']:
|
|
472
|
+
if job['status'] not in {'NEW', 'WAITING'} or job['date'] > tnow:
|
|
465
473
|
continue
|
|
466
474
|
|
|
467
|
-
|
|
468
|
-
if job['date'] > tnow:
|
|
469
|
-
continue
|
|
470
|
-
|
|
471
|
-
# Check dependencies
|
|
475
|
+
dependencies = job.get('dependencies') or []
|
|
472
476
|
all_deps_completed = True
|
|
473
|
-
if
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
477
|
+
if dependencies:
|
|
478
|
+
buff = np.full(
|
|
479
|
+
(len(dependencies),),
|
|
480
|
+
dtype=job_table.dtype,
|
|
481
|
+
fill_value=np.nan,
|
|
482
|
+
)
|
|
483
|
+
for i, dep_hash in enumerate(dependencies):
|
|
478
484
|
buff['hash'][i] = dep_hash
|
|
479
485
|
|
|
480
486
|
locs = job_table.get_loc(buff, acquire=False)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
# Dependency not found - cannot proceed
|
|
487
|
+
for dep_loc in locs:
|
|
488
|
+
if dep_loc == -1:
|
|
489
|
+
all_deps_completed = False
|
|
490
|
+
break
|
|
491
|
+
dep_job = job_table.get_dict_list(
|
|
492
|
+
job_table[dep_loc], acquire=False
|
|
493
|
+
)[0]
|
|
494
|
+
if dep_job['status'] != 'COMPLETED':
|
|
490
495
|
all_deps_completed = False
|
|
491
496
|
break
|
|
492
497
|
|
|
493
|
-
# Update to PENDING if all conditions met
|
|
494
498
|
if all_deps_completed:
|
|
495
499
|
job['status'] = 'PENDING'
|
|
496
500
|
job['mtime'] = tnow
|
|
497
501
|
job_table.upsert(job, acquire=False)
|
|
498
502
|
active_jobs['jobs'][job_hash] = job
|
|
499
503
|
modified = True
|
|
500
|
-
|
|
504
|
+
counts['pending'] += 1
|
|
501
505
|
|
|
502
|
-
|
|
503
|
-
if
|
|
504
|
-
Logger.log.warning(
|
|
506
|
+
elapsed = time.monotonic() - started_at
|
|
507
|
+
if elapsed > 5:
|
|
508
|
+
Logger.log.warning(
|
|
509
|
+
f"update_active_jobs processing time high: {elapsed:.2f} seconds"
|
|
510
|
+
)
|
|
505
511
|
|
|
506
|
-
# Save updated active_jobs cache if modified
|
|
507
512
|
if modified:
|
|
508
513
|
job_table.upsert(active_jobs, acquire=False)
|
|
509
514
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
515
|
+
if counts['timed_out']:
|
|
516
|
+
Logger.log.warning(
|
|
517
|
+
f"Marked {counts['timed_out']} jobs as ERROR due to timeout (>1 hour)"
|
|
518
|
+
)
|
|
513
519
|
|
|
520
|
+
return counts
|
|
514
521
|
finally:
|
|
515
|
-
|
|
516
|
-
job_table.release()
|
|
522
|
+
job_table.release()
|
|
517
523
|
|
|
518
524
|
@staticmethod
|
|
519
|
-
def update_active_jobs(shdata) -> None:
|
|
520
|
-
"""
|
|
521
|
-
Legacy in-process loop kept for backward compatibility with any
|
|
522
|
-
external caller still spawning this as a thread. New deployments
|
|
523
|
-
should run SharedData.Routines.UpdateActiveJobs as a standalone
|
|
524
|
-
routine instead — it owns this same retry/backoff policy plus a
|
|
525
|
-
heartbeat and MAX_ERRORS exit, and is independently
|
|
526
|
-
restartable/killable via the normal workerpool commands.
|
|
527
|
-
"""
|
|
525
|
+
def update_active_jobs(shdata, interval=15, retry_interval=60) -> None:
|
|
526
|
+
"""Compatibility loop; scheduled deployments should use UpdateActiveJobs."""
|
|
528
527
|
while True:
|
|
529
|
-
retry_delay = 15
|
|
530
528
|
try:
|
|
531
529
|
WorkerPool.update_active_jobs_once(shdata)
|
|
530
|
+
time.sleep(interval)
|
|
532
531
|
except Exception as e:
|
|
533
532
|
try:
|
|
534
533
|
Logger.log.error(f"Error in update_active_jobs: {e}")
|
|
535
|
-
except:
|
|
534
|
+
except Exception:
|
|
536
535
|
pass
|
|
537
|
-
|
|
538
|
-
# Never hold the JOBS mutex during the retry backoff —
|
|
539
|
-
# update_active_jobs_once() always releases it before raising
|
|
540
|
-
# or returning.
|
|
541
|
-
time.sleep(retry_delay)
|
|
536
|
+
time.sleep(retry_interval)
|
|
542
537
|
|
|
543
538
|
@staticmethod
|
|
544
539
|
def get_active_jobs(shdata, acquire=True):
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import faulthandler
|
|
2
|
-
import time
|
|
3
|
-
|
|
4
|
-
# Capture a Python-level traceback if a C extension (pandas_parser, numpy,
|
|
5
|
-
# pyarrow, ...) crashes the interpreter with SIGSEGV/SIGABRT/SIGFPE/SIGBUS.
|
|
6
|
-
# Without this, a crash leaves only a kernel segfault line with a raw IP.
|
|
7
|
-
faulthandler.enable()
|
|
8
|
-
|
|
9
|
-
# PROPRIETARY LIBS
|
|
10
|
-
from SharedData.Routines.WorkerPool import WorkerPool
|
|
11
|
-
from SharedData.Logger import Logger
|
|
12
|
-
from SharedData.SharedData import SharedData
|
|
13
|
-
shdata = SharedData('SharedData.Routines.UpdateActiveJobs', user='master')
|
|
14
|
-
|
|
15
|
-
Logger.log.info('ROUTINE STARTED!')
|
|
16
|
-
|
|
17
|
-
# Standalone routine form of the promotion sweep formerly run as an
|
|
18
|
-
# in-process Worker thread (WorkerPool.update_active_jobs). Splitting it out
|
|
19
|
-
# lets it be killed/restarted independently via the normal workerpool
|
|
20
|
-
# commands without tearing down the Worker's API server and every other
|
|
21
|
-
# child routine on the host — see docs/INCIDENT_WORKERPOOL_JOBS_INDEX_20260724.md.
|
|
22
|
-
MAX_ERRORS = 10
|
|
23
|
-
error_count = 0
|
|
24
|
-
lastheartbeat = time.time()
|
|
25
|
-
|
|
26
|
-
while True:
|
|
27
|
-
try:
|
|
28
|
-
if time.time() - lastheartbeat > 15:
|
|
29
|
-
lastheartbeat = time.time()
|
|
30
|
-
Logger.log.debug('#heartbeat#errors=%d' % error_count)
|
|
31
|
-
|
|
32
|
-
retry_delay = 15
|
|
33
|
-
try:
|
|
34
|
-
WorkerPool.update_active_jobs_once(shdata)
|
|
35
|
-
error_count = 0
|
|
36
|
-
except Exception as e:
|
|
37
|
-
Logger.log.error('Error in update_active_jobs_once: %s' % (str(e)))
|
|
38
|
-
error_count += 1
|
|
39
|
-
retry_delay = 60
|
|
40
|
-
if error_count >= MAX_ERRORS:
|
|
41
|
-
Logger.log.error(
|
|
42
|
-
'Max errors (%d) reached, exiting' % (MAX_ERRORS))
|
|
43
|
-
raise SystemExit(1)
|
|
44
|
-
|
|
45
|
-
# Never hold the JOBS mutex during the retry backoff —
|
|
46
|
-
# update_active_jobs_once() always releases it before raising or
|
|
47
|
-
# returning.
|
|
48
|
-
time.sleep(retry_delay)
|
|
49
|
-
except SystemExit:
|
|
50
|
-
raise
|
|
51
|
-
except Exception as e:
|
|
52
|
-
Logger.log.error('UpdateActiveJobs Loop Exception: %s' % (str(e)))
|
|
53
|
-
time.sleep(60)
|
|
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
|
|
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
|
|
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
|