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:
yumoqing 2026-06-01 16:20:59 +08:00
parent 31d66aa91b
commit cd578de80d
3 changed files with 83 additions and 3 deletions

View File

@ -22,7 +22,7 @@ from .serverenv import ServerEnv
from .filestorage import TmpFileRecord
from .loadplugins import load_plugins
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 = []
cleanupctx_coros = []
@ -135,6 +135,8 @@ class ConfiguredServer:
i18n_paths = get_i18n_paths(workdir)
reloader = HotReloader(config_path, i18n_paths)
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):
task = asyncio.create_task(hot_reload_task(app, reloader))
app['hot_reload_task'] = task
@ -148,7 +150,7 @@ class ConfiguredServer:
pass
self.app.on_startup.append(_hot_reload_startup)
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',
port=port,

View File

@ -6,6 +6,10 @@ Watches file mtimes and triggers reload of cached resources:
- i18n files (MiniI18N singleton)
- 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(),
no cross-process coordination needed. When a file changes on disk,
all processes detect it on their next check cycle.
@ -112,12 +116,16 @@ class HotReloader:
return reloaded
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:
from appPublic.jsonConfig import JsonConfig
# SingletonDecorator stores instance as .instance
JsonConfig.instance = None
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:
warning(f'[hot_reload] failed to reload config: {e}')
@ -168,3 +176,73 @@ async def hot_reload_task(app, reloader):
except asyncio.CancelledError:
info('[hot_reload] stopped')
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()
})