Spaces:
Running
Running
| # fix_stats_lock.py -- fixes BACKEND-2: add threading lock for stats cache | |
| import re | |
| with open('api/main.py', 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| # First: check what actually exists in the file around stats cache | |
| idx = content.find('_stats_cache') | |
| if idx == -1: | |
| print('ERROR: _stats_cache not found in main.py at all') | |
| exit() | |
| # Show context | |
| print('Current stats cache area:') | |
| print(repr(content[idx-10:idx+200])) | |
| print() | |
| # Add threading import + lock after _stats_cache = None line | |
| # Find the module-level _stats_cache declaration | |
| import_added = False | |
| if '_stats_lock' not in content: | |
| # Add threading lock after _STATS_TTL line | |
| old_ttl = '_STATS_TTL = 60.0' | |
| if old_ttl in content: | |
| content = content.replace( | |
| old_ttl, | |
| old_ttl + '\nimport threading as _th\n_stats_lock = _th.Lock()' | |
| ) | |
| import_added = True | |
| print('OK: _stats_lock added after _STATS_TTL') | |
| else: | |
| # Try alternate TTL variable name | |
| for candidate in ['_STATS_TTL = 60.0', '_STATS_TTL=60.0']: | |
| if candidate in content: | |
| content = content.replace( | |
| candidate, | |
| candidate + '\nimport threading as _th\n_stats_lock = _th.Lock()' | |
| ) | |
| import_added = True | |
| print(f'OK: lock added after {candidate}') | |
| break | |
| if not import_added: | |
| print('WARNING: could not find _STATS_TTL -- adding lock near _stats_cache') | |
| content = content.replace( | |
| '_stats_cache = None', | |
| '_stats_cache = None\nimport threading as _th\n_stats_lock = _th.Lock()', | |
| 1 | |
| ) | |
| import_added = True | |
| # Add double-check pattern inside get_stats | |
| # Find the early return and add lock before the Neo4j call | |
| old_early_return = ' if _stats_cache is not None and' | |
| if old_early_return in content: | |
| # Find the full early-return block | |
| start = content.find(old_early_return) | |
| # Find end of that if block (the return statement) | |
| end = content.find('\n', content.find('return _stats_cache', start)) + 1 | |
| early_block = content[start:end] | |
| print('Found early return block:') | |
| print(repr(early_block)) | |
| # After the early return, add lock + double-check | |
| old_driver_line = ' driver = get_driver()' | |
| if old_driver_line in content: | |
| content = content.replace( | |
| old_driver_line, | |
| ' # BACKEND-2 FIX: lock prevents two concurrent requests both\n' | |
| ' # seeing _stats_cache is None and both running the full graph scan\n' | |
| ' with _stats_lock:\n' | |
| ' import time as _tc\n' | |
| ' if _stats_cache is not None and (_tc.monotonic() - _stats_cached_at) < _STATS_TTL:\n' | |
| ' return _stats_cache\n' | |
| ' driver = get_driver()', | |
| 1 | |
| ) | |
| print('OK: double-check lock added before get_driver()') | |
| else: | |
| print('WARNING: "driver = get_driver()" not found in get_stats') | |
| else: | |
| print('WARNING: early return not found') | |
| with open('api/main.py', 'w', encoding='utf-8') as f: | |
| f.write(content) | |
| # Verify syntax | |
| import ast | |
| try: | |
| ast.parse(content) | |
| print('SYNTAX OK') | |
| except SyntaxError as e: | |
| print(f'SYNTAX ERROR at line {e.lineno}: {e.msg}') | |