-
Notifications
You must be signed in to change notification settings - Fork 36
Allow to live update attributes without server restart #898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
| import collections | ||
| import functools | ||
| import threading | ||
| from typing import Any | ||
| from typing import Any, Callable | ||
|
|
||
| from ayon_server.lib.postgres import Postgres | ||
| from ayon_server.logging import logger | ||
|
|
@@ -29,6 +29,8 @@ def __init__(self) -> None: | |
| # in the same format as the attributes endpoint | ||
| self.info_data: list[Any] = [] | ||
|
|
||
| self._invalidation_callbacks: list[Callable[[], None]] = [] | ||
|
|
||
| # We need to load attribute data in a separate thread | ||
| # with a separate event loop, because the main event loop | ||
| # is already running and we cannot run another one | ||
|
|
@@ -146,5 +148,58 @@ def by_name_scoped(self, entity_type: str, name: str) -> dict[str, Any]: | |
| return attr | ||
| raise KeyError(f"Attribute {name} not found for entity type {entity_type}") | ||
|
|
||
| def register_invalidation_callback(self, callback: Callable[[], None]) -> None: | ||
| """Register a callback to be called when attributes are reloaded.""" | ||
| self._invalidation_callbacks.append(callback) | ||
|
|
||
| async def reload(self) -> None: | ||
| """Reload attributes from the database and invalidate all cached models. | ||
|
|
||
| Fetches fresh data from the database, updates the in-memory attribute | ||
| lists in-place (preserving list object identity so ModelSet.attributes | ||
| references remain valid), and invalidates all cached Pydantic models | ||
| so they are regenerated on next access. | ||
| """ | ||
| query = "SELECT * FROM public.attributes ORDER BY position" | ||
| result = await Postgres.fetch(query) | ||
|
|
||
|
Comment on lines
+155
to
+165
|
||
| # Build new data in a temporary structure first to minimize | ||
| # the window of inconsistency | ||
| new_data: collections.defaultdict[str, list[Any]] = collections.defaultdict(list) | ||
| new_info_data: list[Any] = [] | ||
|
|
||
|
BigRoy marked this conversation as resolved.
|
||
| for row in result: | ||
| new_info_data.append(row) | ||
| for scope in row["scope"]: | ||
| attrd = {"name": row["name"], **row["data"]} | ||
| if (scope != "project") and ("default" in attrd): | ||
| del attrd["default"] | ||
| new_data[scope].append(attrd) | ||
|
|
||
| # Update self.data in-place to preserve list object identity. | ||
| # ModelSet instances hold direct references to these list objects, | ||
| # so we must mutate them rather than replace them. | ||
| all_scopes = set(self.data.keys()) | set(new_data.keys()) | ||
| for scope in all_scopes: | ||
| self.data[scope].clear() | ||
| self.data[scope].extend(new_data.get(scope, [])) | ||
|
|
||
| self.info_data = new_info_data | ||
|
|
||
| # Clear functools caches since the underlying data has changed | ||
| AttributeLibrary.inheritable_attributes.cache_clear() | ||
| AttributeLibrary.by_name.cache_clear() | ||
| AttributeLibrary.by_name_scoped.cache_clear() | ||
|
|
||
| # Invalidate all registered ModelSet Pydantic model caches | ||
| for callback in self._invalidation_callbacks: | ||
| callback() | ||
|
|
||
| logger.info("Attribute library reloaded") | ||
|
|
||
| async def reload_handler(self, event: Any = None) -> None: | ||
| """Event handler adapter for reload(), for use with EventStream.subscribe.""" | ||
| await self.reload() | ||
|
|
||
|
|
||
| attribute_library = AttributeLibrary() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ | |
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| import aiocache | ||
|
|
||
| from ayon_server.lib.redis import Redis | ||
| from ayon_server.logging import logger | ||
|
|
||
|
|
@@ -16,8 +18,19 @@ async def clear_settings_cache(event: "EventModel"): | |
| await Redis.delete_ns("all-settings") | ||
|
|
||
|
|
||
| async def clear_attribute_info_cache(event: "EventModel"): | ||
| """Clear the in-memory aiocache for the /info attributes response. | ||
|
|
||
| Called on all nodes via global hook so each instance flushes its own | ||
| local cache immediately when attributes are updated. | ||
| """ | ||
| logger.trace("Clearing attribute info cache") | ||
| await aiocache.caches.get("default").clear() | ||
|
Comment on lines
+21
to
+28
|
||
|
|
||
|
|
||
| DEFAULT_HOOKS: list[tuple[str, HandlerType, bool]] = [ | ||
| ("settings.changed", clear_settings_cache, False), | ||
| ("bundle.created", clear_settings_cache, False), | ||
| ("bundle.updated", clear_settings_cache, False), | ||
| ("server.attributes_updated", clear_attribute_info_cache, True), | ||
| ] | ||
Uh oh!
There was an error while loading. Please reload this page.