mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-14 21:07:36 +00:00
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
570 lines
23 KiB
Python
570 lines
23 KiB
Python
import asyncio
|
||
import json
|
||
import time
|
||
from collections.abc import Awaitable, Callable, Mapping
|
||
from functools import cache
|
||
|
||
from fastapi import FastAPI
|
||
|
||
from common.constant import LockConstant
|
||
from plugins.core.runtime.diagnostics_store import PluginDiagnosticsReporter, diagnostics_namespace
|
||
from plugins.core.runtime.explicit import ExplicitPluginRuntime
|
||
from plugins.core.runtime.startup import PluginRuntimeStartupManager
|
||
from plugins.core.runtime.startup_coordination import PluginStartupGenerationResolver
|
||
from utils.log_util import logger
|
||
|
||
from .service.lifecycle_lock import NoopPluginLifecycleLock, PluginLifecycleLock
|
||
|
||
|
||
class PluginApplicationRuntime:
|
||
"""
|
||
应用入口侧插件运行时适配器。
|
||
|
||
server.py 只依赖该适配器提供的高层扩展点,插件实体导入、二次建表、启动资源安装、
|
||
多 worker ready barrier 和生命周期钩子等细节留在插件运行时内部。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
startup_manager: PluginRuntimeStartupManager | None = None,
|
||
*,
|
||
ready_key: str = LockConstant.PLUGIN_STARTUP_READY_KEY,
|
||
ready_expire_seconds: int = LockConstant.PLUGIN_STARTUP_READY_EXPIRE_SECONDS,
|
||
failed_expire_seconds: int = LockConstant.PLUGIN_STARTUP_FAILED_EXPIRE_SECONDS,
|
||
ready_wait_timeout_seconds: int = LockConstant.PLUGIN_STARTUP_READY_WAIT_TIMEOUT_SECONDS,
|
||
ready_wait_interval_seconds: int = LockConstant.PLUGIN_STARTUP_READY_WAIT_INTERVAL_SECONDS,
|
||
lifecycle_lock: PluginLifecycleLock | None = None,
|
||
startup_generation: str | None = None,
|
||
release_worker: object | None = None,
|
||
) -> None:
|
||
"""
|
||
初始化插件应用运行时适配器。
|
||
|
||
:param startup_manager: 插件启动协调器
|
||
:param ready_key: 插件启动 ready 标记 key
|
||
:param ready_expire_seconds: ready 标记过期时间
|
||
:param failed_expire_seconds: 失败标记过期时间
|
||
:param ready_wait_timeout_seconds: 等待 ready 超时时间
|
||
:param ready_wait_interval_seconds: 等待 ready 轮询间隔
|
||
:param lifecycle_lock: 插件生命周期全局锁
|
||
:param startup_generation: 测试或部署显式注入的启动代际
|
||
:param release_worker: 维护当前发布快照和 worker 心跳的可选协调器
|
||
:return: None
|
||
"""
|
||
self.startup_manager = startup_manager or PluginRuntimeStartupManager()
|
||
self.ready_key = ready_key
|
||
self.ready_expire_seconds = ready_expire_seconds
|
||
self.failed_expire_seconds = failed_expire_seconds
|
||
self.ready_wait_timeout_seconds = ready_wait_timeout_seconds
|
||
self.ready_wait_interval_seconds = ready_wait_interval_seconds
|
||
self.lifecycle_lock = lifecycle_lock or NoopPluginLifecycleLock()
|
||
self.startup_generation = startup_generation
|
||
self.release_worker = release_worker
|
||
|
||
def bind_app(self, app: FastAPI) -> None:
|
||
"""
|
||
绑定插件运行时到 FastAPI app。
|
||
|
||
:param app: FastAPI对象
|
||
:return: None
|
||
"""
|
||
app.state.plugin_application_runtime = self
|
||
self.startup_manager.bind_app(app)
|
||
|
||
def prepare_metadata(self, app: FastAPI) -> None:
|
||
"""
|
||
准备插件平台自身元数据。
|
||
|
||
:param app: FastAPI对象
|
||
:return: None
|
||
"""
|
||
self._ensure_bound(app)
|
||
self.startup_manager.import_builtin_entities()
|
||
if self.release_worker is not None:
|
||
from plugins.core.management.entity.do import release_models # noqa: F401, PLC0415
|
||
|
||
async def startup(
|
||
self,
|
||
app: FastAPI,
|
||
*,
|
||
create_tables: Callable[[], Awaitable[None]],
|
||
) -> None:
|
||
"""
|
||
启动插件运行时。
|
||
|
||
同一发布代际只有一个 worker 能在全局生命周期锁内执行写入;其他 worker 等待
|
||
当前代际 ready 后仅执行进程本地激活。插件启动不再依赖长期持有的调度器主节点锁。
|
||
|
||
:param app: FastAPI对象
|
||
:param create_tables: 数据库建表回调
|
||
:return: None
|
||
"""
|
||
try:
|
||
if self.release_worker is not None:
|
||
await self.release_worker.start(
|
||
app, self.startup_manager.builder, self.lifecycle_lock, timeout=self.ready_wait_timeout_seconds
|
||
)
|
||
self.startup_generation = self.release_worker.generation(self.resolve_startup_generation())
|
||
app.state.plugin_release_worker = self.release_worker
|
||
await self._startup_coordinated(app, create_tables=create_tables)
|
||
if self.release_worker is not None:
|
||
await self.release_worker.ready()
|
||
runtime = getattr(app.state, 'plugin_explicit_runtime', None)
|
||
if isinstance(runtime, ExplicitPluginRuntime):
|
||
if self.release_worker is not None:
|
||
# 即使全部显式插件准备失败,空运行时也属于已有发布 worker。
|
||
runtime.metrics.worker_id = self.release_worker.worker_id
|
||
runtime.metrics_reporter.start(getattr(app.state, 'redis', None), f'{self.ready_key}:metrics')
|
||
reporter = PluginDiagnosticsReporter(runtime.diagnostic_snapshot, runtime.metrics.worker_id)
|
||
app.state.plugin_diagnostics_reporter = reporter
|
||
reporter.start(getattr(app.state, 'redis', None), diagnostics_namespace(self.ready_key))
|
||
except BaseException:
|
||
# 激活后的 ready 发布或锁释放也可能失败,此时 server 尚未标记启动完成。
|
||
# 显式资源需在这里释放,不能依赖正常 shutdown 分支。
|
||
runtime = getattr(app.state, 'plugin_explicit_runtime', None)
|
||
try:
|
||
if isinstance(runtime, ExplicitPluginRuntime):
|
||
await runtime.shutdown()
|
||
finally:
|
||
await self._stop_diagnostics(app)
|
||
if self.release_worker is not None:
|
||
try:
|
||
await self.release_worker.stop(failed=True)
|
||
except Exception:
|
||
logger.exception('插件启动失败后的 worker 状态上报失败;旧心跳将按 TTL 失效')
|
||
raise
|
||
|
||
async def _startup_coordinated(
|
||
self,
|
||
app: FastAPI,
|
||
*,
|
||
create_tables: Callable[[], Awaitable[None]],
|
||
) -> None:
|
||
"""
|
||
执行 writer/reader 协调,包括激活后的 ready 发布及锁释放。
|
||
|
||
:param app: 宿主 FastAPI 应用
|
||
:param create_tables: 在允许写入的 worker 中创建数据库表的异步回调
|
||
:return: None
|
||
"""
|
||
self._ensure_bound(app)
|
||
if self.release_worker is not None:
|
||
await self.release_worker.assert_snapshot_current()
|
||
generation = self.resolve_startup_generation()
|
||
app.state.plugin_startup_generation = generation
|
||
app.state.plugin_startup_write_enabled = False
|
||
|
||
ready_state = await self.get_startup_state(app, generation)
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='reader',
|
||
ready_status=ready_state.get('status', 'missing'),
|
||
).debug('🔎 开始检查插件启动代际 ready 状态')
|
||
activated, stale_ready_reported = await self._process_startup_state(
|
||
app,
|
||
generation,
|
||
ready_state,
|
||
stale_ready_reported=False,
|
||
ready_message='复用当前代际插件 ready,开始本地激活',
|
||
failed_message='检测到当前代际插件启动 failed marker',
|
||
)
|
||
if activated:
|
||
return
|
||
|
||
deadline = time.monotonic() + self.ready_wait_timeout_seconds
|
||
wait_reported = False
|
||
while time.monotonic() < deadline:
|
||
async with self.lifecycle_lock.lock('__runtime__', f'startup:{generation}') as lock_result:
|
||
if lock_result.acquired:
|
||
ready_state = await self.get_startup_state(app, generation)
|
||
activated, stale_ready_reported = await self._process_startup_state(
|
||
app,
|
||
generation,
|
||
ready_state,
|
||
stale_ready_reported=stale_ready_reported,
|
||
ready_message='锁内复用当前代际插件 ready,开始本地激活',
|
||
failed_message='锁内检测到当前代际插件启动 failed marker',
|
||
)
|
||
if activated:
|
||
return
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='writer',
|
||
ready_status=ready_state.get('status', 'missing'),
|
||
).info('🎯 当前 worker 成为插件启动 writer')
|
||
await self._run_startup_writer(app, generation, create_tables)
|
||
return
|
||
|
||
ready_state = await self.get_startup_state(app, generation)
|
||
activated, stale_ready_reported = await self._process_startup_state(
|
||
app,
|
||
generation,
|
||
ready_state,
|
||
stale_ready_reported=stale_ready_reported,
|
||
ready_message='等待结束,复用当前代际插件 ready',
|
||
failed_message='等待期间检测到当前代际插件启动 failed marker',
|
||
)
|
||
if activated:
|
||
return
|
||
if not wait_reported:
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='reader',
|
||
ready_status=ready_state.get('status', 'missing'),
|
||
).debug('⏳ 等待插件启动 writer 发布当前代际 ready')
|
||
wait_reported = True
|
||
await asyncio.sleep(self.ready_wait_interval_seconds)
|
||
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='reader',
|
||
ready_status=ready_state.get('status', 'missing'),
|
||
).error('❌ 等待插件启动 ready 超时')
|
||
raise TimeoutError(f'等待插件启动代际 {generation} ready 超时')
|
||
|
||
async def _process_startup_state(
|
||
self,
|
||
app: FastAPI,
|
||
generation: str,
|
||
ready_state: Mapping[str, str],
|
||
*,
|
||
stale_ready_reported: bool,
|
||
ready_message: str,
|
||
failed_message: str,
|
||
) -> tuple[bool, bool]:
|
||
"""
|
||
处理一次插件启动状态读取结果。
|
||
|
||
:param app: FastAPI对象
|
||
:param generation: 插件启动代际
|
||
:param ready_state: 当前ready状态
|
||
:param stale_ready_reported: 是否已记录过期ready告警
|
||
:param ready_message: 复用ready时的日志
|
||
:param failed_message: 发现failed marker时的日志
|
||
:return: 是否已完成reader激活、是否已记录过期ready告警
|
||
"""
|
||
ready_status = ready_state.get('status', 'missing')
|
||
reader_logger = logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='reader',
|
||
ready_status=ready_status,
|
||
)
|
||
if ready_status == 'success':
|
||
if await self.requires_startup_write():
|
||
if not stale_ready_reported:
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='reader',
|
||
ready_status=ready_status,
|
||
stale_ready_ignored=True,
|
||
startup_write_required=True,
|
||
startup_write_reason='missing_default_plugin_state',
|
||
).warning('⚠️ 数据库默认插件状态缺失,忽略当前代际旧 ready 标记')
|
||
return False, True
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='reader',
|
||
ready_status=ready_status,
|
||
plugin_install_lifecycle='skipped',
|
||
plugin_resource_sync='skipped',
|
||
plugin_entity_table_sync='skipped',
|
||
).info(
|
||
'⏭️ 复用插件 ready 状态,跳过启动期全局写入:'
|
||
'plugin_install_lifecycle=skipped,'
|
||
'plugin_resource_sync=skipped,'
|
||
'plugin_entity_table_sync=skipped'
|
||
)
|
||
reader_logger.debug(f'🔄 {ready_message}')
|
||
await self._activate_startup_reader(app, generation)
|
||
return True, stale_ready_reported
|
||
if ready_status == 'failed':
|
||
reader_logger.error(f'❌ {failed_message}')
|
||
raise RuntimeError(self._build_startup_failure_message(ready_state))
|
||
return False, stale_ready_reported
|
||
|
||
async def requires_startup_write(self) -> bool:
|
||
"""
|
||
校验 ready 标记对应的数据库状态是否仍然完整。
|
||
|
||
测试替身或旧适配器未实现该能力时保持原有行为;生产启动管理器会检查
|
||
默认启用插件是否仍具备安装状态。
|
||
|
||
:return: 是否必须重新执行启动期写入
|
||
"""
|
||
checker = getattr(type(self.startup_manager), 'requires_startup_write', None)
|
||
if checker is None:
|
||
return False
|
||
return bool(await self.startup_manager.requires_startup_write())
|
||
|
||
async def _run_startup_writer(
|
||
self,
|
||
app: FastAPI,
|
||
generation: str,
|
||
create_tables: Callable[[], Awaitable[None]],
|
||
) -> None:
|
||
"""
|
||
执行当前代际唯一的插件启动写入。
|
||
|
||
:param app: FastAPI对象
|
||
:param generation: 插件启动代际
|
||
:param create_tables: 数据库建表回调
|
||
:return: None
|
||
"""
|
||
app.state.plugin_startup_write_enabled = True
|
||
writer_logger = logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='writer',
|
||
ready_status='initializing',
|
||
)
|
||
writer_logger.info('🔄 开始同步插件全局启动资源')
|
||
await self.clear_startup_ready(app, generation)
|
||
try:
|
||
await self.startup_manager.prepare_enabled_plugins(app, startup_write_enabled=True)
|
||
await create_tables()
|
||
await self.startup_manager.activate_enabled_plugins(app, startup_write_enabled=True)
|
||
except Exception as exc:
|
||
await self.mark_startup_failed(app, generation, exc)
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='writer',
|
||
ready_status='failed',
|
||
).exception('❌ 插件全局启动资源同步失败,已写入 failed marker')
|
||
raise
|
||
await self.mark_startup_ready(app, generation)
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='writer',
|
||
ready_status='success',
|
||
).info('✅ 插件全局启动资源同步完成')
|
||
self._log_startup_completed(app, generation, role='writer')
|
||
|
||
async def _activate_startup_reader(self, app: FastAPI, generation: str) -> None:
|
||
"""
|
||
在当前 worker 执行无全局写入的本地插件激活。
|
||
|
||
:param app: FastAPI对象
|
||
:param generation: 插件启动代际
|
||
:return: None
|
||
"""
|
||
reader_logger = logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role='reader',
|
||
ready_status='success',
|
||
)
|
||
reader_logger.debug('🔄 开始执行插件本地实体、Hook和路由激活')
|
||
await self.startup_manager.prepare_enabled_plugins(app, startup_write_enabled=False)
|
||
await self.startup_manager.activate_enabled_plugins(app, startup_write_enabled=False)
|
||
reader_logger.debug('✅ 插件本地实体、Hook和路由激活完成')
|
||
self._log_startup_completed(app, generation, role='reader')
|
||
|
||
@staticmethod
|
||
def _log_startup_completed(app: FastAPI, generation: str, *, role: str) -> None:
|
||
"""
|
||
为每个成功启动的worker输出一条稳定可见的插件运行时摘要。
|
||
|
||
writer和reader内部过程仍按原有级别记录;该INFO摘要用于避免命中ready后
|
||
只有DEBUG日志而表现为插件日志随机缺失。
|
||
|
||
:param app: FastAPI对象
|
||
:param generation: 插件启动代际
|
||
:param role: 当前worker的插件启动角色
|
||
:return: None
|
||
"""
|
||
plugin_registry = getattr(app.state, 'plugin_registry', None)
|
||
enabled_plugin_ids = (
|
||
sorted(str(plugin.plugin_id) for plugin in plugin_registry.list_enabled_plugins())
|
||
if plugin_registry is not None
|
||
else []
|
||
)
|
||
enabled_plugins = ','.join(enabled_plugin_ids) or 'none'
|
||
logger.bind(
|
||
startup_generation=generation,
|
||
plugin_startup_role=role,
|
||
ready_status='success',
|
||
enabled_plugin_ids=enabled_plugin_ids,
|
||
enabled_plugin_count=len(enabled_plugin_ids),
|
||
).info(f'✅ 插件运行时启动完成:role={role},generation={generation[:8]},enabled={enabled_plugins}')
|
||
|
||
async def shutdown(self, app: FastAPI) -> None:
|
||
"""
|
||
关闭插件运行时。
|
||
|
||
:param app: FastAPI对象
|
||
:return: None
|
||
"""
|
||
self._ensure_bound(app)
|
||
startup_write_enabled = bool(getattr(app.state, 'plugin_startup_write_enabled', False))
|
||
try:
|
||
await self.startup_manager.shutdown(app, startup_write_enabled=startup_write_enabled)
|
||
finally:
|
||
await self._stop_diagnostics(app)
|
||
if self.release_worker is not None:
|
||
await self.release_worker.stop()
|
||
|
||
@staticmethod
|
||
async def _stop_diagnostics(app: FastAPI) -> None:
|
||
"""
|
||
关闭诊断采样,观测清理失败不阻止发布 worker 上报最终状态。
|
||
|
||
:param app: 宿主 FastAPI 应用
|
||
:return: None
|
||
"""
|
||
reporter = getattr(app.state, 'plugin_diagnostics_reporter', None)
|
||
if isinstance(reporter, PluginDiagnosticsReporter):
|
||
try:
|
||
await reporter.stop()
|
||
except Exception:
|
||
logger.warning('插件运行诊断上报关闭失败,远程快照将按 TTL 过期')
|
||
|
||
async def clear_startup_ready(self, app: FastAPI, generation: str | None = None) -> None:
|
||
"""
|
||
清除指定代际的插件启动 ready 标记。
|
||
|
||
:param app: FastAPI对象
|
||
:param generation: 插件启动代际
|
||
:return: None
|
||
"""
|
||
resolved_generation = generation or self.resolve_startup_generation()
|
||
await app.state.redis.delete(self.build_ready_key(resolved_generation))
|
||
|
||
async def mark_startup_ready(self, app: FastAPI, generation: str | None = None) -> None:
|
||
"""
|
||
标记指定代际已完成插件启动写入。
|
||
|
||
:param app: FastAPI对象
|
||
:param generation: 插件启动代际
|
||
:return: None
|
||
"""
|
||
resolved_generation = generation or self.resolve_startup_generation()
|
||
await app.state.redis.set(
|
||
self.build_ready_key(resolved_generation),
|
||
json.dumps({'generation': resolved_generation, 'status': 'success'}, ensure_ascii=False),
|
||
ex=self.ready_expire_seconds,
|
||
)
|
||
writer_logger = logger.bind(
|
||
startup_generation=resolved_generation,
|
||
plugin_startup_role='writer',
|
||
ready_status='success',
|
||
)
|
||
failed_plugin_ids = getattr(app.state, 'plugin_dependency_failed_plugin_ids', set())
|
||
if failed_plugin_ids:
|
||
writer_logger.warning(
|
||
f'⚠️ 插件启动协调已完成,依赖检查失败插件已隔离:{"、".join(sorted(failed_plugin_ids))}'
|
||
)
|
||
return
|
||
writer_logger.info('✅ 插件启动资源已就绪')
|
||
|
||
async def mark_startup_failed(self, app: FastAPI, generation: str, error: Exception) -> None:
|
||
"""
|
||
标记指定插件启动代际初始化失败。
|
||
|
||
:param app: FastAPI对象
|
||
:param generation: 插件启动代际
|
||
:param error: 启动异常
|
||
:return: None
|
||
"""
|
||
await app.state.redis.set(
|
||
self.build_ready_key(generation),
|
||
json.dumps(
|
||
{
|
||
'generation': generation,
|
||
'status': 'failed',
|
||
'error': str(error)[:1000],
|
||
},
|
||
ensure_ascii=False,
|
||
),
|
||
ex=self.failed_expire_seconds,
|
||
)
|
||
|
||
async def get_startup_state(self, app: FastAPI, generation: str) -> dict[str, str]:
|
||
"""
|
||
读取指定代际的插件启动状态。
|
||
|
||
:param app: FastAPI对象
|
||
:param generation: 插件启动代际
|
||
:return: 启动状态
|
||
"""
|
||
raw_state = await app.state.redis.get(self.build_ready_key(generation))
|
||
if not raw_state:
|
||
return {}
|
||
try:
|
||
state = json.loads(raw_state)
|
||
except (TypeError, json.JSONDecodeError):
|
||
return {}
|
||
if not isinstance(state, dict) or state.get('generation') != generation:
|
||
return {}
|
||
return {str(key): str(value) for key, value in state.items()}
|
||
|
||
def resolve_startup_generation(self) -> str:
|
||
"""
|
||
解析当前插件启动代际。
|
||
|
||
:return: 插件启动代际
|
||
"""
|
||
if self.startup_generation:
|
||
return self.startup_generation
|
||
generation_resolver = PluginStartupGenerationResolver(self.startup_manager.builder.backend_root)
|
||
self.startup_generation = generation_resolver.resolve()
|
||
return self.startup_generation
|
||
|
||
def build_ready_key(self, generation: str) -> str:
|
||
"""
|
||
构建代际隔离的 ready key。
|
||
|
||
:param generation: 插件启动代际
|
||
:return: Redis key
|
||
"""
|
||
return f'{self.ready_key}:{generation}'
|
||
|
||
@staticmethod
|
||
def _build_startup_failure_message(state: Mapping[str, str]) -> str:
|
||
"""
|
||
构建启动失败错误消息。
|
||
|
||
:param state: 启动状态
|
||
:return: 错误消息
|
||
"""
|
||
error_message = state.get('error') or '未知错误'
|
||
return f'插件启动代际 {state.get("generation", "")} 初始化失败:{error_message}'
|
||
|
||
def _ensure_bound(self, app: FastAPI) -> None:
|
||
"""
|
||
确保插件运行时已绑定到 app。
|
||
|
||
:param app: FastAPI对象
|
||
:return: None
|
||
"""
|
||
if getattr(app.state, 'plugin_application_runtime', None) is not self:
|
||
self.bind_app(app)
|
||
|
||
|
||
@cache
|
||
def get_plugin_application_runtime() -> PluginApplicationRuntime:
|
||
"""
|
||
获取应用插件运行时适配器。
|
||
|
||
:return: 应用插件运行时适配器
|
||
"""
|
||
from plugins.core.deployment.config import PluginDeploymentConfig # noqa: PLC0415
|
||
from plugins.core.management.service.startup_gateway import ( # noqa: PLC0415
|
||
PluginManagementRouteStateGateway,
|
||
PluginManagementStartupGateway,
|
||
)
|
||
from plugins.core.runtime.service.lifecycle_lock import RedisPluginLifecycleLock # noqa: PLC0415
|
||
|
||
startup_manager = PluginRuntimeStartupManager(
|
||
management_gateway=PluginManagementStartupGateway(),
|
||
route_state_gateway=PluginManagementRouteStateGateway(),
|
||
)
|
||
deployment_config = PluginDeploymentConfig.from_settings(startup_manager.builder.backend_root)
|
||
release_worker = None
|
||
if deployment_config.enabled:
|
||
from plugins.core.deployment.worker import PluginReleaseWorker # noqa: PLC0415
|
||
|
||
release_worker = PluginReleaseWorker(deployment_config)
|
||
return PluginApplicationRuntime(
|
||
startup_manager=startup_manager,
|
||
lifecycle_lock=RedisPluginLifecycleLock(),
|
||
release_worker=release_worker,
|
||
)
|