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.
Files changed (83) hide show
  1. {shareddata-6.83.21/src/shareddata.egg-info → shareddata-6.83.22}/PKG-INFO +1 -1
  2. {shareddata-6.83.21 → shareddata-6.83.22}/setup.py +1 -1
  3. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Defaults.py +30 -5
  4. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/MongoDBClient.py +1 -1
  5. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Logger.py +11 -7
  6. shareddata-6.83.22/src/SharedData/Routines/UpdateActiveJobs.py +75 -0
  7. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/Worker.py +1 -1
  8. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/WorkerPool.py +76 -81
  9. {shareddata-6.83.21 → shareddata-6.83.22/src/shareddata.egg-info}/PKG-INFO +1 -1
  10. shareddata-6.83.21/src/SharedData/Routines/UpdateActiveJobs.py +0 -53
  11. {shareddata-6.83.21 → shareddata-6.83.22}/LICENSE +0 -0
  12. {shareddata-6.83.21 → shareddata-6.83.22}/MANIFEST.in +0 -0
  13. {shareddata-6.83.21 → shareddata-6.83.22}/README.md +0 -0
  14. {shareddata-6.83.21 → shareddata-6.83.22}/pyproject.toml +0 -0
  15. {shareddata-6.83.21 → shareddata-6.83.22}/setup.cfg +0 -0
  16. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/ServerGunicorn.py +0 -0
  17. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/ServerWaitress.py +0 -0
  18. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/__init__.py +0 -0
  19. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/auth.py +0 -0
  20. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/constants.py +0 -0
  21. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/download_paths.py +0 -0
  22. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/download_slots.py +0 -0
  23. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/listcache.py +0 -0
  24. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/__init__.py +0 -0
  25. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/cache.py +0 -0
  26. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/collections.py +0 -0
  27. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/downloads.py +0 -0
  28. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/metadata.py +0 -0
  29. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/system.py +0 -0
  30. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/tables.py +0 -0
  31. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/timeseries.py +0 -0
  32. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/routes/workers.py +0 -0
  33. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/API/utils.py +0 -0
  34. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/CacheRedis.py +0 -0
  35. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/CollectionMongoDB.py +0 -0
  36. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Database.py +0 -0
  37. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/AWSEC2.py +0 -0
  38. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/AWSS3.py +0 -0
  39. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/AutoDocstrings.py +0 -0
  40. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ClientAPI.py +0 -0
  41. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ClientSocket.py +0 -0
  42. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ClientWebSocket.py +0 -0
  43. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/LogHandlerAPI.py +0 -0
  44. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/SaveTables.py +0 -0
  45. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ServerSocket.py +0 -0
  46. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/ServerWebSocket.py +0 -0
  47. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/StreamsCache.py +0 -0
  48. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/StreamsPersist.py +0 -0
  49. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/SyncTable.py +0 -0
  50. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/TunnelWebSocket.py +0 -0
  51. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/IO/__init__.py +0 -0
  52. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Metadata.py +0 -0
  53. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/MultiProc.py +0 -0
  54. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/OpenFIGI.py +0 -0
  55. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
  56. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/BatchJob.py +0 -0
  57. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/Schedule.py +0 -0
  58. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
  59. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/Scheduler.py +0 -0
  60. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/WorkerLib.py +0 -0
  61. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Routines/__init__.py +0 -0
  62. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/SharedData.py +0 -0
  63. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/SharedNumpy.py +0 -0
  64. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/StreamKafka.py +0 -0
  65. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Symbol.py +0 -0
  66. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Table.py +0 -0
  67. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableDisk.py +0 -0
  68. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndex.py +0 -0
  69. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitFunctions.py +0 -0
  70. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
  71. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitGenerate.py +0 -0
  72. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitHash.py +0 -0
  73. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TableIndexJitLoc.py +0 -0
  74. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TimeSeriesDisk.py +0 -0
  75. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/TimeseriesContainer.py +0 -0
  76. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Users.py +0 -0
  77. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/Utils.py +0 -0
  78. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/__init__.py +0 -0
  79. {shareddata-6.83.21 → shareddata-6.83.22}/src/SharedData/sharedmutexwin.pyd +0 -0
  80. {shareddata-6.83.21 → shareddata-6.83.22}/src/shareddata.egg-info/SOURCES.txt +0 -0
  81. {shareddata-6.83.21 → shareddata-6.83.22}/src/shareddata.egg-info/dependency_links.txt +0 -0
  82. {shareddata-6.83.21 → shareddata-6.83.22}/src/shareddata.egg-info/requires.txt +0 -0
  83. {shareddata-6.83.21 → shareddata-6.83.22}/src/shareddata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.83.21
3
+ Version: 6.83.22
4
4
  Summary: Memory Mapped / Shared Memory Database with S3 repository
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -34,7 +34,7 @@ install_requires = [
34
34
 
35
35
  setup(
36
36
  name='shareddata',
37
- version='6.83.21',
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 / without finding a mountpoint — root fs.
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:
@@ -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()
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.83.21
3
+ Version: 6.83.22
4
4
  Summary: Memory Mapped / Shared Memory Database with S3 repository
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -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