phase3: plugin routes static only (no dynamic registration) + require_active_plugin Redis cache + cache invalidation on activate/deactivate
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
+23
-23
@@ -609,29 +609,9 @@ class PluginRegistry:
|
||||
# with the automation plugin if it is active
|
||||
await self._register_contributions_for_plugin(db, name, plugin)
|
||||
|
||||
# Register routes on FastAPI app if available
|
||||
# Track actual route objects by identity to avoid cross-plugin removal
|
||||
if self._app is not None:
|
||||
routers = plugin.get_routes()
|
||||
mounted_routes: list[Any] = []
|
||||
for router in routers:
|
||||
# Check if routes from this router are already registered (P1.2 fix)
|
||||
# This prevents duplicate route registration when plugins are
|
||||
# statically registered in main.py AND dynamically activated
|
||||
existing_paths = {getattr(r, 'path', None) for r in self._app.router.routes}
|
||||
router_paths = {getattr(r, 'path', None) for r in router.routes}
|
||||
if router_paths & existing_paths:
|
||||
# Routes already registered — skip to avoid duplicates
|
||||
logger.debug("Plugin '%s' routes already registered, skipping", name)
|
||||
continue
|
||||
# Snapshot existing route object IDs before inclusion
|
||||
existing_ids = {id(r) for r in self._app.router.routes}
|
||||
self._app.include_router(router)
|
||||
# Collect newly added route objects
|
||||
for r in self._app.router.routes:
|
||||
if id(r) not in existing_ids:
|
||||
mounted_routes.append(r)
|
||||
self._mounted_routes[name] = mounted_routes
|
||||
# NOTE: Routes are registered statically in main.py at app creation time.
|
||||
# Activation status is enforced per-request via require_active_plugin().
|
||||
# No dynamic route registration here — prevents duplicate routes.
|
||||
|
||||
# Sync notification types from this plugin
|
||||
await self.sync_notification_types(db)
|
||||
@@ -642,6 +622,16 @@ class PluginRegistry:
|
||||
await db.flush()
|
||||
self._db_status[name] = record
|
||||
|
||||
# Invalidate Redis cache for this plugin across all tenants
|
||||
try:
|
||||
from app.core.redis import get_redis
|
||||
redis = get_redis()
|
||||
if redis is not None:
|
||||
async for key in redis.scan_iter(f"plugin-activation:*:{name}"):
|
||||
await redis.delete(key)
|
||||
except Exception:
|
||||
pass # Cache invalidation is best-effort
|
||||
|
||||
return record
|
||||
|
||||
async def deactivate(self, db: AsyncSession, name: str) -> PluginModel:
|
||||
@@ -704,6 +694,16 @@ class PluginRegistry:
|
||||
await db.flush()
|
||||
self._db_status[name] = record
|
||||
|
||||
# Invalidate Redis cache for this plugin across all tenants
|
||||
try:
|
||||
from app.core.redis import get_redis
|
||||
redis = get_redis()
|
||||
if redis is not None:
|
||||
async for key in redis.scan_iter(f"plugin-activation:*:{name}"):
|
||||
await redis.delete(key)
|
||||
except Exception:
|
||||
pass # Cache invalidation is best-effort
|
||||
|
||||
return record
|
||||
|
||||
async def uninstall(
|
||||
|
||||
Reference in New Issue
Block a user