Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions api/addons/delete_addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@
from ayon_server.api.responses import EmptyResponse
from ayon_server.api.system import require_server_restart
from ayon_server.exceptions import AyonException, ForbiddenException, NotFoundException
from ayon_server.installer.hotreload import (
notify_clients_addon_reload,
trigger_hotreload,
)
from ayon_server.logging import logger

# from ayon_server.lib.postgres import Postgres
from .router import router


async def delete_addon_directory(addon_name: str, addon_version: str | None = None):
"""Delete an addon or addon version"""
"""Delete an addon or addon version."""
library = AddonLibrary.getinstance()
addon_definition = library.get(addon_name)
if addon_definition is None:
Expand Down Expand Up @@ -45,7 +49,26 @@ async def delete_addon_directory(addon_name: str, addon_version: str | None = No
except Exception as e:
raise AyonException(f"Failed to delete {addon_name} directory: {e}")
library.data.pop(addon_name, None)
await require_server_restart(None, "Restart the server to apply the addon changes.")

# Try hot-reload first, fall back to requiring server restart
reload_success = await trigger_hotreload(mode="addon")

if reload_success:
await notify_clients_addon_reload()
logger.info(
"Addon deleted successfully with hot-reload",
addon_name=addon_name,
addon_version=addon_version,
)
else:
await require_server_restart(
None, "Restart the server to apply the addon changes."
)
logger.warning(
"Hot-reload failed after addon deletion, server restart required",
addon_name=addon_name,
addon_version=addon_version,
)


@router.delete("/{addon_name}")
Expand Down
44 changes: 42 additions & 2 deletions api/system/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
"dbimport",
]

from fastapi import Response

from ayon_server.api.dependencies import CurrentUser
from ayon_server.api.system import clear_server_restart_required, require_server_restart
from ayon_server.events import EventStream
from ayon_server.exceptions import ForbiddenException
from ayon_server.installer.hotreload import get_hotreload_manager
from ayon_server.lib.postgres import Postgres
from ayon_server.logging import logger
from ayon_server.types import Field, OPModel
from fastapi import Response

from . import dbimport, frontend_modules, info, metrics, secrets, sites
from .router import router
Expand Down Expand Up @@ -86,3 +86,43 @@ async def set_restart_required(

if not user.is_admin:
raise ForbiddenException("Only administrators can set server restart required")


class ReloadStatusModel(OPModel):
"""Model for hot-reload status information."""

last_reload: str | None = Field(
None,
description="ISO timestamp of the last successful hot-reload",
)
reload_count: int = Field(
0,
description="Total number of successful hot-reloads since server start",
)
callbacks_registered: int = Field(
0,
description="Number of reload callbacks registered",
)


@router.get("/health/reload")
async def get_reload_status() -> ReloadStatusModel:
"""Get hot-reload status information.

This endpoint provides information about the hot-reload subsystem,
including when the last reload occurred and how many reloads have
been performed since server start.

This is useful for:
- Monitoring reload activity
- Verifying that hot-reloads are working
- Debugging addon installation issues
"""
manager = get_hotreload_manager()
status = await manager.get_status()

return ReloadStatusModel(
last_reload=status["last_reload"],
reload_count=status["reload_count"],
callbacks_registered=status["callbacks_registered"],
)
53 changes: 49 additions & 4 deletions ayon_server/installer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from ayon_server.events import EventStream
from ayon_server.installer.addons import install_addon_from_url, unpack_addon
from ayon_server.installer.dependency_packages import download_dependency_package
from ayon_server.installer.hotreload import (
notify_clients_addon_reload,
trigger_hotreload,
)
from ayon_server.installer.installers import download_installer
from ayon_server.lib.postgres import Postgres
from ayon_server.logging import log_traceback, logger
Expand All @@ -23,24 +27,64 @@ class TooManyRetries(Exception):
pass


async def handle_need_restart(installer: "BackgroundInstaller") -> None:
async def handle_need_restart(
installer: "BackgroundInstaller",
event_id: str | None = None,
) -> None:
"""
Handle post-installation restart/reload logic.

This function is called after an addon installation completes to
determine whether a hot-reload or full restart is needed.

Args:
installer: The background installer instance.
event_id: The event ID that triggered this action (passed explicitly
to avoid race conditions with concurrent events).
"""
await asyncio.sleep(1)
if installer.event_queue.empty() and installer.restart_needed:

if not installer.event_queue.empty() or not installer.restart_needed:
return

# Try hot-reload first
reload_success = await trigger_hotreload(
mode="addon",
event_id=event_id,
)

if reload_success:
# Hot-reload succeeded, notify clients
await notify_clients_addon_reload(event_id=event_id)
logger.info(
"Addon installed successfully with hot-reload",
event_id=event_id,
)
else:
# Hot-reload failed, fall back to requiring restart
await require_server_restart(
None, "Restart the server to apply the addon changes."
)
logger.warning(
"Hot-reload failed, server restart required",
event_id=event_id,
)


class BackgroundInstaller(BackgroundWorker):
def initialize(self) -> None:
self.event_queue: asyncio.Queue[str] = asyncio.Queue()
self.restart_needed: bool = False
# Note: current_event_id removed - event_id is now passed explicitly
# through the call chain to avoid race conditions

async def enqueue(self, event_id: str) -> None:
logger.debug("Background installer: enqueuing event", event_id=event_id)
await self.event_queue.put(event_id)

async def process_event(self, event_id: str) -> None:
# Note: event_id is now passed explicitly to handle_need_restart
# instead of storing it as instance state (avoids race conditions)
res = await Postgres().fetch(
" SELECT topic, status, summary, retries FROM events WHERE id = $1 ",
event_id,
Expand Down Expand Up @@ -83,7 +127,8 @@ async def process_event(self, event_id: str) -> None:
event_id=event_id,
)

asyncio.create_task(handle_need_restart(self))
# Pass event_id explicitly to avoid race conditions
asyncio.create_task(handle_need_restart(self, event_id=event_id))

async def run(self) -> None:
# load past unprocessed events
Expand Down Expand Up @@ -122,4 +167,4 @@ async def run(self) -> None:
await self.enqueue(event_id)


background_installer = BackgroundInstaller()
background_installer = BackgroundInstaller()
Loading