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.
Files changed (107) hide show
  1. {shareddata-6.83.16 → shareddata-6.83.17}/MANIFEST.in +0 -1
  2. {shareddata-6.83.16/src/shareddata.egg-info → shareddata-6.83.17}/PKG-INFO +3 -2
  3. {shareddata-6.83.16 → shareddata-6.83.17}/setup.py +2 -2
  4. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/MongoDBClient.py +2 -2
  5. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Logger.py +11 -7
  6. shareddata-6.83.17/src/SharedData/Routines/UpdateActiveJobs.py +75 -0
  7. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/Worker.py +3 -3
  8. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/WorkerPool.py +76 -81
  9. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/sharedmutexwin.pyd +0 -0
  10. {shareddata-6.83.16 → shareddata-6.83.17/src/shareddata.egg-info}/PKG-INFO +3 -2
  11. {shareddata-6.83.16 → shareddata-6.83.17}/src/shareddata.egg-info/SOURCES.txt +2 -0
  12. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_bson_last_pos_reuse.py +5 -5
  13. shareddata-6.83.17/tests/test_logger_thread_safety.py +47 -0
  14. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_mount_unmount_detection.py +3 -3
  15. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_schema_acquire_timeout.py +3 -3
  16. shareddata-6.83.17/tests/test_workerpool_jobs_status.py +283 -0
  17. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_workerpool_post_commands.py +16 -11
  18. shareddata-6.83.16/src/SharedData/Routines/UpdateActiveJobs.py +0 -53
  19. {shareddata-6.83.16 → shareddata-6.83.17}/LICENSE +0 -0
  20. {shareddata-6.83.16 → shareddata-6.83.17}/README.md +0 -0
  21. {shareddata-6.83.16 → shareddata-6.83.17}/pyproject.toml +0 -0
  22. {shareddata-6.83.16 → shareddata-6.83.17}/setup.cfg +0 -0
  23. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/ServerGunicorn.py +0 -0
  24. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/ServerWaitress.py +0 -0
  25. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/__init__.py +0 -0
  26. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/auth.py +0 -0
  27. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/constants.py +0 -0
  28. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/listcache.py +0 -0
  29. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/__init__.py +0 -0
  30. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/cache.py +0 -0
  31. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/collections.py +0 -0
  32. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/metadata.py +0 -0
  33. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/system.py +0 -0
  34. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/tables.py +0 -0
  35. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/timeseries.py +0 -0
  36. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/routes/workers.py +0 -0
  37. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/API/utils.py +0 -0
  38. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/CacheRedis.py +0 -0
  39. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/CollectionMongoDB.py +0 -0
  40. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Database.py +0 -0
  41. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Defaults.py +0 -0
  42. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/AWSEC2.py +0 -0
  43. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/AWSS3.py +0 -0
  44. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/AutoDocstrings.py +0 -0
  45. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ClientAPI.py +0 -0
  46. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ClientSocket.py +0 -0
  47. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ClientWebSocket.py +0 -0
  48. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/LogHandlerAPI.py +0 -0
  49. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/SaveTables.py +0 -0
  50. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ServerSocket.py +0 -0
  51. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/ServerWebSocket.py +0 -0
  52. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/StreamsCache.py +0 -0
  53. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/StreamsPersist.py +0 -0
  54. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/SyncTable.py +0 -0
  55. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/TunnelWebSocket.py +0 -0
  56. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/IO/__init__.py +0 -0
  57. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Metadata.py +0 -0
  58. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/MultiProc.py +0 -0
  59. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/OpenFIGI.py +0 -0
  60. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
  61. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/BatchJob.py +0 -0
  62. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/Schedule.py +0 -0
  63. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
  64. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/Scheduler.py +0 -0
  65. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/WorkerLib.py +0 -0
  66. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Routines/__init__.py +0 -0
  67. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/SharedData.py +0 -0
  68. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/SharedNumpy.py +0 -0
  69. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/StreamKafka.py +0 -0
  70. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Symbol.py +0 -0
  71. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Table.py +0 -0
  72. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableDisk.py +0 -0
  73. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndex.py +0 -0
  74. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitFunctions.py +0 -0
  75. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
  76. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitGenerate.py +0 -0
  77. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitHash.py +0 -0
  78. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TableIndexJitLoc.py +0 -0
  79. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TimeSeriesDisk.py +0 -0
  80. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/TimeseriesContainer.py +0 -0
  81. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Users.py +0 -0
  82. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/Utils.py +0 -0
  83. {shareddata-6.83.16 → shareddata-6.83.17}/src/SharedData/__init__.py +0 -0
  84. {shareddata-6.83.16 → shareddata-6.83.17}/src/shareddata.egg-info/dependency_links.txt +0 -0
  85. {shareddata-6.83.16 → shareddata-6.83.17}/src/shareddata.egg-info/requires.txt +0 -0
  86. {shareddata-6.83.16 → shareddata-6.83.17}/src/shareddata.egg-info/top_level.txt +0 -0
  87. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_collection.py +0 -0
  88. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_collection_loopback.py +0 -0
  89. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_table.py +0 -0
  90. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_table_production_flows.py +0 -0
  91. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_table_schemaless.py +0 -0
  92. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_api_table_schemaless_extend.py +0 -0
  93. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_cache_redis.py +0 -0
  94. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_extend_rt.py +0 -0
  95. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_listcache.py +0 -0
  96. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_loc.py +0 -0
  97. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_metadata.py +0 -0
  98. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_read_write_tail.py +0 -0
  99. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_stream_loopback_async.py +0 -0
  100. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_table_auto_grow_all_pkeys.py +0 -0
  101. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_timeseries.py +0 -0
  102. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_timeseries_api.py +0 -0
  103. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_d1.py +0 -0
  104. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_m1.py +0 -0
  105. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_upsert_unordered_get_date_loc_m15.py +0 -0
  106. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_upsert_unordered_positions_m1.py +0 -0
  107. {shareddata-6.83.16 → shareddata-6.83.17}/tests/test_workerpool_mutex_release.py +0 -0
@@ -1,2 +1 @@
1
1
  include src/SharedData/sharedmutexwin.pyd
2
- include src/SharedData/IO/apidocs.yml
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.83.16
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.16',
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: '10.0.0.50:27017')
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
- dfnewlines_sorted = Logger.readLogs()
344
- if dfnewlines_sorted is None or len(dfnewlines_sorted) == 0:
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. TB08-WS with
230
- # --schedules MASTER,TRADEBOT08-VM1), this worker always owns the scheduler
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) -> None:
410
- """
411
- Runs a single pass of the NEW/WAITING -> PENDING promotion and
412
- FETCHED/RUNNING -> ERROR timeout sweep over the WORKERPOOL/JOBS
413
- active-jobs cache. Raises on failure; callers own the retry/backoff
414
- policy. Always releases the JOBS mutex before returning, even on
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
- isacquired = False
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 # No active jobs yet — nothing to do this pass
436
+ return counts
437
437
 
438
- active_jobs = job_table.get_dict_list(job_table[loc[0]], acquire=False)[0]
438
+ active_jobs = job_table.get_dict_list(
439
+ job_table[loc[0]], acquire=False
440
+ )[0]
439
441
  modified = False
440
- timeout_count = 0
441
- pending_count = 0
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
- # Check if job status is active
447
- if job['status'] not in ['NEW', 'WAITING', 'PENDING', 'FETCHED', 'RUNNING']:
448
- del active_jobs['jobs'][job_hash] # remove from active jobs
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
- # 1. Check timeout for FETCHED/RUNNING jobs
453
- if job['status'] in ['FETCHED', 'RUNNING'] and job['mtime'] < one_hour_ago:
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] # remove from active jobs
466
+ del active_jobs['jobs'][job_hash]
459
467
  modified = True
460
- timeout_count += 1
468
+ counts['timed_out'] += 1
469
+ counts['removed'] += 1
461
470
  continue
462
471
 
463
- # 2. Update NEW/WAITING → PENDING if due and dependencies met
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
- # Check if job is due
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 job.get('dependencies'):
474
- ndep = len(job['dependencies'])
475
- buff = np.full((ndep,), dtype=job_table.dtype, fill_value=np.nan)
476
-
477
- for i, dep_hash in enumerate(job['dependencies']):
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
- for i in range(ndep):
483
- if locs[i] != -1:
484
- dep_job = job_table.get_dict_list(job_table[locs[i]], acquire=False)[0]
485
- if dep_job['status'] != 'COMPLETED':
486
- all_deps_completed = False
487
- break
488
- else:
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
- pending_count += 1
504
+ counts['pending'] += 1
501
505
 
502
- te = time.time() - ti
503
- if te > 5:
504
- Logger.log.warning(f"update_active_jobs processing time high: {te:.2f} seconds")
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
- # Log warnings (matches MongoDB logging behavior)
511
- if timeout_count > 0:
512
- Logger.log.warning(f"Marked {timeout_count} jobs as ERROR due to timeout (>1 hour)")
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
- if isacquired:
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
- retry_delay = 60
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,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.83.16
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'TRADEBOT{j % 10:02d}-WS',
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': 'TB01', 'cpu': 12}},
367
- {'hash': 'WORKER_02', 'status': 'ONLINE', 'info': {'host': 'TB02', 'cpu': 28}},
368
- {'hash': 'WORKER_03', 'status': 'OFFLINE', 'info': {'host': 'TB03', 'cpu': 16}},
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': 'TB04', 'cpu': 16}},
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
- # Model of TB08-WS: /home is a real mount; /nvme1../nvme6 are declared fstab
17
- # mount targets. DATABASE_FOLDERS='/home/jcooloj/db,/nvme4/db,/nvme6/db'.
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/jcooloj/db', current, DECLARED) is True
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/TRADEBOT08-WS') is True
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\\TRADEBOT08-WS') is True
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/TRADEBOT08-WS') is True
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