feat: add invalidate_all_caches() and GET /__hot_reload__ endpoint
invalidate_all_caches() clears module-level caches: - rbac: UserPermissions.ur_caches + rp_caches - pricing: PricingProgram.pricing_data - uapi: UAPIData.apidata + org_users - llmage: _uapi_cache + _uapiio_cache GET /__hot_reload__ endpoint (registered only when hot_reload enabled): - Manual trigger for cache flush during development - Returns JSON with list of cleared caches Automatic trigger: - config.json change → invalidate_all_caches() called automatically Each module cache is cleared independently with try/except so one module's import failure doesn't block others.
This commit is contained in:
parent
31d66aa91b
commit
cd578de80d
Binary file not shown.
@ -22,7 +22,7 @@ from .serverenv import ServerEnv
|
|||||||
from .filestorage import TmpFileRecord
|
from .filestorage import TmpFileRecord
|
||||||
from .loadplugins import load_plugins
|
from .loadplugins import load_plugins
|
||||||
from .real_ip import real_ip_middleware
|
from .real_ip import real_ip_middleware
|
||||||
from .hotreload import HotReloader, get_i18n_paths, hot_reload_task
|
from .hotreload import HotReloader, get_i18n_paths, hot_reload_task, hot_reload_handler, invalidate_all_caches
|
||||||
|
|
||||||
startup_coros = []
|
startup_coros = []
|
||||||
cleanupctx_coros = []
|
cleanupctx_coros = []
|
||||||
@ -135,6 +135,8 @@ class ConfiguredServer:
|
|||||||
i18n_paths = get_i18n_paths(workdir)
|
i18n_paths = get_i18n_paths(workdir)
|
||||||
reloader = HotReloader(config_path, i18n_paths)
|
reloader = HotReloader(config_path, i18n_paths)
|
||||||
reloader.set_interval(interval)
|
reloader.set_interval(interval)
|
||||||
|
# Register HTTP endpoint for manual cache flush
|
||||||
|
self.app.router.add_get('/__hot_reload__', hot_reload_handler)
|
||||||
async def _hot_reload_startup(app):
|
async def _hot_reload_startup(app):
|
||||||
task = asyncio.create_task(hot_reload_task(app, reloader))
|
task = asyncio.create_task(hot_reload_task(app, reloader))
|
||||||
app['hot_reload_task'] = task
|
app['hot_reload_task'] = task
|
||||||
@ -148,7 +150,7 @@ class ConfiguredServer:
|
|||||||
pass
|
pass
|
||||||
self.app.on_startup.append(_hot_reload_startup)
|
self.app.on_startup.append(_hot_reload_startup)
|
||||||
self.app.cleanup_ctx.append(_hot_reload_cleanup)
|
self.app.cleanup_ctx.append(_hot_reload_cleanup)
|
||||||
print(f'hot_reload enabled, interval={interval}s, pid will check independently')
|
print(f'hot_reload enabled, interval={interval}s, endpoint=GET /__hot_reload__')
|
||||||
|
|
||||||
web.run_app(self.build_app(),host=config.website.host or '0.0.0.0',
|
web.run_app(self.build_app(),host=config.website.host or '0.0.0.0',
|
||||||
port=port,
|
port=port,
|
||||||
|
|||||||
@ -6,6 +6,10 @@ Watches file mtimes and triggers reload of cached resources:
|
|||||||
- i18n files (MiniI18N singleton)
|
- i18n files (MiniI18N singleton)
|
||||||
- Jinja2 template cache (auto_reload already handles this)
|
- Jinja2 template cache (auto_reload already handles this)
|
||||||
|
|
||||||
|
Module caches (rbac/pricing/uapi/llmage) can be cleared via:
|
||||||
|
- HTTP endpoint: GET /__hot_reload__
|
||||||
|
- Automatic when config.json changes
|
||||||
|
|
||||||
Multi-process safe: each process independently checks mtimes via stat(),
|
Multi-process safe: each process independently checks mtimes via stat(),
|
||||||
no cross-process coordination needed. When a file changes on disk,
|
no cross-process coordination needed. When a file changes on disk,
|
||||||
all processes detect it on their next check cycle.
|
all processes detect it on their next check cycle.
|
||||||
@ -112,12 +116,16 @@ class HotReloader:
|
|||||||
return reloaded
|
return reloaded
|
||||||
|
|
||||||
def _reload_config(self):
|
def _reload_config(self):
|
||||||
"""Clear JsonConfig singleton so next getConfig() call reloads from disk."""
|
"""Clear JsonConfig singleton so next getConfig() call reloads from disk.
|
||||||
|
Also clear all module caches since config changes may affect them.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
from appPublic.jsonConfig import JsonConfig
|
from appPublic.jsonConfig import JsonConfig
|
||||||
# SingletonDecorator stores instance as .instance
|
# SingletonDecorator stores instance as .instance
|
||||||
JsonConfig.instance = None
|
JsonConfig.instance = None
|
||||||
info('[hot_reload] config.json changed, cache cleared')
|
info('[hot_reload] config.json changed, cache cleared')
|
||||||
|
# Also clear module caches since config may affect module_cache settings
|
||||||
|
invalidate_all_caches()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
warning(f'[hot_reload] failed to reload config: {e}')
|
warning(f'[hot_reload] failed to reload config: {e}')
|
||||||
|
|
||||||
@ -168,3 +176,73 @@ async def hot_reload_task(app, reloader):
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
info('[hot_reload] stopped')
|
info('[hot_reload] stopped')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_all_caches():
|
||||||
|
"""Clear all module caches (rbac/pricing/uapi/llmage).
|
||||||
|
|
||||||
|
Called automatically when config.json changes, or manually via
|
||||||
|
GET /__hot_reload__ endpoint.
|
||||||
|
|
||||||
|
Each module cache is cleared independently with try/except to
|
||||||
|
prevent one module's failure from blocking others.
|
||||||
|
"""
|
||||||
|
cleared = []
|
||||||
|
|
||||||
|
# rbac: UserPermissions singleton with LRU caches
|
||||||
|
try:
|
||||||
|
from rbac.userperm import UserPermissions
|
||||||
|
up = UserPermissions()
|
||||||
|
up.ur_caches.clear()
|
||||||
|
up.invalidate_rp_cache()
|
||||||
|
cleared.append('rbac')
|
||||||
|
except Exception as e:
|
||||||
|
debug(f'[hot_reload] rbac cache clear skipped: {e}')
|
||||||
|
|
||||||
|
# pricing: PricingProgram class-level pricing_data dict
|
||||||
|
try:
|
||||||
|
from pricing.pricing import PricingProgram
|
||||||
|
PricingProgram.pricing_data.clear()
|
||||||
|
cleared.append('pricing')
|
||||||
|
except Exception as e:
|
||||||
|
debug(f'[hot_reload] pricing cache clear skipped: {e}')
|
||||||
|
|
||||||
|
# uapi: UAPIData singleton with apidata and org_users dicts
|
||||||
|
try:
|
||||||
|
from uapi.apidata import UAPIData
|
||||||
|
ud = UAPIData()
|
||||||
|
ud.apidata.clear()
|
||||||
|
ud.org_users.clear()
|
||||||
|
cleared.append('uapi')
|
||||||
|
except Exception as e:
|
||||||
|
debug(f'[hot_reload] uapi cache clear skipped: {e}')
|
||||||
|
|
||||||
|
# llmage: module-level _uapi_cache and _uapiio_cache
|
||||||
|
try:
|
||||||
|
from llmage.utils import invalidate_uapi_cache
|
||||||
|
invalidate_uapi_cache() # clears both _uapi_cache and _uapiio_cache
|
||||||
|
cleared.append('llmage')
|
||||||
|
except Exception as e:
|
||||||
|
debug(f'[hot_reload] llmage cache clear skipped: {e}')
|
||||||
|
|
||||||
|
if cleared:
|
||||||
|
info(f'[hot_reload] cleared caches: {cleared}')
|
||||||
|
return cleared
|
||||||
|
|
||||||
|
|
||||||
|
async def hot_reload_handler(request):
|
||||||
|
"""HTTP endpoint handler for GET /__hot_reload__.
|
||||||
|
|
||||||
|
Manually triggers cache invalidation. Useful for development
|
||||||
|
when database changes aren't detected automatically.
|
||||||
|
|
||||||
|
Returns JSON with list of cleared caches.
|
||||||
|
"""
|
||||||
|
from aiohttp import web
|
||||||
|
cleared = invalidate_all_caches()
|
||||||
|
return web.json_response({
|
||||||
|
'status': 'ok',
|
||||||
|
'cleared': cleared,
|
||||||
|
'timestamp': time.time()
|
||||||
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user