shareddata 6.83.16__tar.gz → 6.83.17__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.16 → shareddata-6.83.17}/MANIFEST.in +0 -1
- {shareddata-6.83.16/src/shareddata.egg-info → shareddata-6.83.17}/PKG-INFO +3 -2
- {shareddata-6.83.16 → shareddata-6.83.17}/setup.py +2 -2
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/MongoDBClient.py +2 -2
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Logger.py +11 -7
- shareddata-6.83.17/src/SharedData/Routines/UpdateActiveJobs.py +75 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/Worker.py +3 -3
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/WorkerPool.py +76 -81
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17/src/shareddata.egg-info}/PKG-INFO +3 -2
- {shareddata-6.83.16 → shareddata-6.83.17}/src/shareddata.egg-info/SOURCES.txt +2 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_bson_last_pos_reuse.py +5 -5
- shareddata-6.83.17/tests/test_logger_thread_safety.py +47 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_mount_unmount_detection.py +3 -3
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_schema_acquire_timeout.py +3 -3
- shareddata-6.83.17/tests/test_workerpool_jobs_status.py +283 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_workerpool_post_commands.py +16 -11
- shareddata-6.83.16/src/SharedData/Routines/UpdateActiveJobs.py +0 -53
- {shareddata-6.83.16 → shareddata-6.83.17}/LICENSE +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/README.md +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/pyproject.toml +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/setup.cfg +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/__init__.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/auth.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/cache.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/collections.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/tables.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/timeseries.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Database.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Defaults.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ClientAPI.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/SaveTables.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/Schedule.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/WorkerLib.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/SharedData.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/SharedNumpy.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Table.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableDisk.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndex.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitFunctions.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitGenerate.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Users.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Utils.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/__init__.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/src/shareddata.egg-info/top_level.txt +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_collection.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_collection_loopback.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_table.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_table_production_flows.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_table_schemaless.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_table_schemaless_extend.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_cache_redis.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_extend_rt.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_listcache.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_loc.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_metadata.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_read_write_tail.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_stream_loopback_async.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_table_auto_grow_all_pkeys.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_timeseries.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_timeseries_api.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_d1.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_m1.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_m15.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_upsert_unordered_positions_m1.py +0 -0
- {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_workerpool_mutex_release.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shareddata
|
|
3
|
-
Version: 6.83.
|
|
3
|
+
Version: 6.83.17
|
|
4
4
|
Summary: Memory Mapped / Shared Memory Database with S3 repository
|
|
5
|
+
License: MIT
|
|
5
6
|
Classifier: Programming Language :: Python :: 3
|
|
6
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
7
7
|
Classifier: Operating System :: OS Independent
|
|
8
8
|
Requires-Python: >=3.9, <3.13
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
@@ -39,6 +39,7 @@ Requires-Dist: redis==6.2.0
|
|
|
39
39
|
Dynamic: classifier
|
|
40
40
|
Dynamic: description
|
|
41
41
|
Dynamic: description-content-type
|
|
42
|
+
Dynamic: license
|
|
42
43
|
Dynamic: license-file
|
|
43
44
|
Dynamic: requires-dist
|
|
44
45
|
Dynamic: requires-python
|
|
@@ -34,13 +34,13 @@ install_requires = [
|
|
|
34
34
|
|
|
35
35
|
setup(
|
|
36
36
|
name='shareddata',
|
|
37
|
-
version='6.83.
|
|
37
|
+
version='6.83.17',
|
|
38
38
|
description='Memory Mapped / Shared Memory Database with S3 repository',
|
|
39
|
+
license='MIT',
|
|
39
40
|
long_description=open('README.md').read(),
|
|
40
41
|
long_description_content_type='text/markdown',
|
|
41
42
|
classifiers=[
|
|
42
43
|
'Programming Language :: Python :: 3',
|
|
43
|
-
'License :: OSI Approved :: MIT License',
|
|
44
44
|
'Operating System :: OS Independent',
|
|
45
45
|
],
|
|
46
46
|
packages=find_packages(where='src'),
|
|
@@ -62,7 +62,7 @@ class MongoDBClient:
|
|
|
62
62
|
f'&serverSelectionTimeoutMS=10000'
|
|
63
63
|
)
|
|
64
64
|
elif 'MONGODB_REPLICA_SET' not in os.environ:
|
|
65
|
-
# Single server connection (MONGODB_HOST includes port: '
|
|
65
|
+
# Single server connection (MONGODB_HOST includes port: '192.0.2.10:27017')
|
|
66
66
|
self.mongodb_conn_str = (
|
|
67
67
|
f'mongodb://{os.environ["MONGODB_USER"]}:'
|
|
68
68
|
f'{os.environ["MONGODB_PWD"]}@'
|
|
@@ -260,4 +260,4 @@ class MongoDBClient:
|
|
|
260
260
|
if pymongo.helpers._index_list(index_data['key']) == target_index:
|
|
261
261
|
return # Index already exists
|
|
262
262
|
|
|
263
|
-
coll.create_index(index_fields, **kwargs)
|
|
263
|
+
coll.create_index(index_fields, **kwargs)
|
|
@@ -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()
|
|
@@ -226,8 +226,8 @@ while True:
|
|
|
226
226
|
|
|
227
227
|
# Scheduler lifecycle — watchdog works in both election and non-election
|
|
228
228
|
# modes. In election mode, only the current master owns the scheduler and
|
|
229
|
-
# runs 'MASTER'. In non-election mode (static deploy, e.g.
|
|
230
|
-
# --schedules MASTER,
|
|
229
|
+
# runs 'MASTER'. In non-election mode (static deploy, e.g. WORKER01 with
|
|
230
|
+
# --schedules MASTER,WORKER02), this worker always owns the scheduler
|
|
231
231
|
# and runs its configured SCHEDULE_NAMES.
|
|
232
232
|
election_mode = str(os.environ.get('RUN_MASTER_ELECTION', '')).upper() == 'TRUE'
|
|
233
233
|
if election_mode:
|
|
@@ -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):
|
|
Binary file
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shareddata
|
|
3
|
-
Version: 6.83.
|
|
3
|
+
Version: 6.83.17
|
|
4
4
|
Summary: Memory Mapped / Shared Memory Database with S3 repository
|
|
5
|
+
License: MIT
|
|
5
6
|
Classifier: Programming Language :: Python :: 3
|
|
6
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
7
7
|
Classifier: Operating System :: OS Independent
|
|
8
8
|
Requires-Python: >=3.9, <3.13
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
@@ -39,6 +39,7 @@ Requires-Dist: redis==6.2.0
|
|
|
39
39
|
Dynamic: classifier
|
|
40
40
|
Dynamic: description
|
|
41
41
|
Dynamic: description-content-type
|
|
42
|
+
Dynamic: license
|
|
42
43
|
Dynamic: license-file
|
|
43
44
|
Dynamic: requires-dist
|
|
44
45
|
Dynamic: requires-python
|
|
@@ -86,6 +86,7 @@ tests/test_cache_redis.py
|
|
|
86
86
|
tests/test_extend_rt.py
|
|
87
87
|
tests/test_listcache.py
|
|
88
88
|
tests/test_loc.py
|
|
89
|
+
tests/test_logger_thread_safety.py
|
|
89
90
|
tests/test_metadata.py
|
|
90
91
|
tests/test_mount_unmount_detection.py
|
|
91
92
|
tests/test_read_write_tail.py
|
|
@@ -98,5 +99,6 @@ tests/test_upsert_unordered_get_date_loc_d1.py
|
|
|
98
99
|
tests/test_upsert_unordered_get_date_loc_m1.py
|
|
99
100
|
tests/test_upsert_unordered_get_date_loc_m15.py
|
|
100
101
|
tests/test_upsert_unordered_positions_m1.py
|
|
102
|
+
tests/test_workerpool_jobs_status.py
|
|
101
103
|
tests/test_workerpool_mutex_release.py
|
|
102
104
|
tests/test_workerpool_post_commands.py
|
|
@@ -117,7 +117,7 @@ def test_bson_last_pos_reuse():
|
|
|
117
117
|
'routine': f'Scripts/Routine_{j}.py',
|
|
118
118
|
'status': np.random.choice(['running', 'pending', 'completed', 'error']),
|
|
119
119
|
'started': time.time() - np.random.randint(0, 3600),
|
|
120
|
-
'worker': f'
|
|
120
|
+
'worker': f'WORKER{j % 10:02d}',
|
|
121
121
|
'iteration': i,
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -363,11 +363,11 @@ def test_bson_backwards_compatibility():
|
|
|
363
363
|
# --- Case 1: Insert multiple records to simulate old-format data ---
|
|
364
364
|
print(f"\n[1] Inserting records to simulate pre-existing BSON file...")
|
|
365
365
|
test_data = [
|
|
366
|
-
{'hash': 'WORKER_01', 'status': 'ONLINE', 'info': {'host': '
|
|
367
|
-
{'hash': 'WORKER_02', 'status': 'ONLINE', 'info': {'host': '
|
|
368
|
-
{'hash': 'WORKER_03', 'status': 'OFFLINE', 'info': {'host': '
|
|
366
|
+
{'hash': 'WORKER_01', 'status': 'ONLINE', 'info': {'host': 'HOST01', 'cpu': 12}},
|
|
367
|
+
{'hash': 'WORKER_02', 'status': 'ONLINE', 'info': {'host': 'HOST02', 'cpu': 28}},
|
|
368
|
+
{'hash': 'WORKER_03', 'status': 'OFFLINE', 'info': {'host': 'HOST03', 'cpu': 16}},
|
|
369
369
|
{'hash': 'ACTIVE_JOBS', 'status': 'ACTIVE', 'jobs': {'j1': {'repo': 'R1'}}},
|
|
370
|
-
{'hash': 'WORKER_04', 'status': 'ONLINE', 'info': {'host': '
|
|
370
|
+
{'hash': 'WORKER_04', 'status': 'ONLINE', 'info': {'host': 'HOST04', 'cpu': 16}},
|
|
371
371
|
]
|
|
372
372
|
for d in test_data:
|
|
373
373
|
records = records.upsert(d)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from SharedData.Logger import Logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_getlogs_serializes_the_full_mutation_pipeline(monkeypatch):
|
|
11
|
+
call_count = 8
|
|
12
|
+
start = threading.Barrier(call_count)
|
|
13
|
+
state_guard = threading.Lock()
|
|
14
|
+
state = {"active": 0, "max_active": 0}
|
|
15
|
+
new_lines = pd.DataFrame({"message": ["new log line"]})
|
|
16
|
+
|
|
17
|
+
def read_logs():
|
|
18
|
+
with state_guard:
|
|
19
|
+
state["active"] += 1
|
|
20
|
+
state["max_active"] = max(state["max_active"], state["active"])
|
|
21
|
+
time.sleep(0.005)
|
|
22
|
+
return new_lines
|
|
23
|
+
|
|
24
|
+
def process_logs(_):
|
|
25
|
+
time.sleep(0.005)
|
|
26
|
+
|
|
27
|
+
def handle_heartbeats(_):
|
|
28
|
+
time.sleep(0.005)
|
|
29
|
+
with state_guard:
|
|
30
|
+
state["active"] -= 1
|
|
31
|
+
|
|
32
|
+
monkeypatch.setattr(Logger, "lock", threading.Lock())
|
|
33
|
+
monkeypatch.setattr(Logger, "dflogs", pd.DataFrame())
|
|
34
|
+
monkeypatch.setattr(Logger, "readLogs", staticmethod(read_logs))
|
|
35
|
+
monkeypatch.setattr(Logger, "getLastLog", staticmethod(process_logs))
|
|
36
|
+
monkeypatch.setattr(Logger, "getStatus", staticmethod(process_logs))
|
|
37
|
+
monkeypatch.setattr(Logger, "handleHeartbeats", staticmethod(handle_heartbeats))
|
|
38
|
+
|
|
39
|
+
def poll_logs():
|
|
40
|
+
start.wait()
|
|
41
|
+
return Logger.getLogs()
|
|
42
|
+
|
|
43
|
+
with ThreadPoolExecutor(max_workers=call_count) as executor:
|
|
44
|
+
results = list(executor.map(lambda _: poll_logs(), range(call_count)))
|
|
45
|
+
|
|
46
|
+
assert state == {"active": 0, "max_active": 1}
|
|
47
|
+
assert all(result is Logger.dflogs for result in results)
|
|
@@ -13,14 +13,14 @@ ancestor is a stub -> not accessible.
|
|
|
13
13
|
"""
|
|
14
14
|
from SharedData.Defaults import _classify_on_mounted_device
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# mount targets. DATABASE_FOLDERS='/home/
|
|
16
|
+
# Example host: /home is a real mount; /nvme1../nvme6 are declared fstab
|
|
17
|
+
# mount targets. DATABASE_FOLDERS='/home/example/db,/nvme4/db,/nvme6/db'.
|
|
18
18
|
DECLARED = {'/', '/home', '/nvme1', '/nvme2', '/nvme3', '/nvme4', '/nvme5', '/nvme6'}
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_root_fs_dbfolder_on_home_mount_is_accessible():
|
|
22
22
|
current = {'/', '/home', '/nvme4', '/nvme6'}
|
|
23
|
-
assert _classify_on_mounted_device('/home/
|
|
23
|
+
assert _classify_on_mounted_device('/home/example/db', current, DECLARED) is True
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def test_mounted_nvme_dbfolder_is_accessible():
|
|
@@ -14,13 +14,13 @@ from SharedData.SharedData import SharedData
|
|
|
14
14
|
|
|
15
15
|
def test_schema_relpath_forward_slashes_matches():
|
|
16
16
|
assert SharedData._is_schema_relpath(
|
|
17
|
-
'master/Symbols/D1/SCHEMA/table/
|
|
17
|
+
'master/Symbols/D1/SCHEMA/table/WORKER01') is True
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def test_schema_relpath_backslashes_matches():
|
|
21
21
|
# POSIX Table converts shm_name to backslashes before acquire().
|
|
22
22
|
assert SharedData._is_schema_relpath(
|
|
23
|
-
'master\\Symbols\\D1\\SCHEMA\\table\\
|
|
23
|
+
'master\\Symbols\\D1\\SCHEMA\\table\\WORKER01') is True
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def test_non_schema_relpath_does_not_match_either_separator():
|
|
@@ -42,6 +42,6 @@ def test_schema_predicate_rejects_non_singleton_schema_source():
|
|
|
42
42
|
|
|
43
43
|
def test_schema_predicate_accepts_singleton_for_any_host():
|
|
44
44
|
assert SharedData._is_schema_relpath(
|
|
45
|
-
'master/Symbols/D1/SCHEMA/table/
|
|
45
|
+
'master/Symbols/D1/SCHEMA/table/WORKER01') is True
|
|
46
46
|
assert SharedData._is_schema_relpath(
|
|
47
47
|
'master/Symbols/D1/SCHEMA/table/OTHERHOST') is True
|