From 6412db2e932c348d3c721f0f81de5f5037a21144 Mon Sep 17 00:00:00 2001 From: Blake Oxford Date: Thu, 14 May 2026 13:54:05 -0400 Subject: [PATCH 1/4] perf: skip unnecessary shared user queries Avoid loading shared_users during partial updates unless the request actually changes sharing, and reuse the pre-update shared user snapshot in adapter updates instead of querying it twice. Add regression tests covering the non-sharing update path and the adapter query reuse path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/adapter_processor_v2/views.py | 11 ++-- backend/api_v2/api_deployment_views.py | 7 ++- backend/connector_v2/views.py | 7 ++- backend/pipeline_v2/views.py | 7 ++- .../prompt_studio_core_v2/views.py | 8 ++- .../test_shared_user_query_optimizations.py | 60 +++++++++++++++++++ backend/workflow_manager/workflow_v2/views.py | 9 ++- 7 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 backend/utils/tests/test_shared_user_query_optimizations.py diff --git a/backend/adapter_processor_v2/views.py b/backend/adapter_processor_v2/views.py index 3e59d12594..dfc9555ec5 100644 --- a/backend/adapter_processor_v2/views.py +++ b/backend/adapter_processor_v2/views.py @@ -294,16 +294,17 @@ def destroy( def partial_update( self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any] ) -> Response: - # Store current shared users before update (for email notifications) adapter = self.get_object() - current_shared_users = set(adapter.shared_users.all()) + shared_users_updated = AdapterKeys.SHARED_USERS in request.data + current_shared_users = set() - if AdapterKeys.SHARED_USERS in request.data: + if shared_users_updated: + current_shared_users = set(adapter.shared_users.all()) # find the deleted users shared_users = { int(user_id) for user_id in request.data.get("shared_users", {}) } - current_users = {user.id for user in adapter.shared_users.all()} + current_users = {user.id for user in current_shared_users} removed_users = current_users.difference(shared_users) # if removed user use this adapter as default @@ -345,7 +346,7 @@ def partial_update( response = super().partial_update(request, *args, **kwargs) # Send email notifications to newly shared users - if response.status_code == 200 and AdapterKeys.SHARED_USERS in request.data: + if response.status_code == 200 and shared_users_updated: try: adapter.refresh_from_db() new_shared_users = set(adapter.shared_users.all()) diff --git a/backend/api_v2/api_deployment_views.py b/backend/api_v2/api_deployment_views.py index e636ca01ec..da81496a86 100644 --- a/backend/api_v2/api_deployment_views.py +++ b/backend/api_v2/api_deployment_views.py @@ -373,7 +373,10 @@ def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Respons """Override partial_update to handle sharing notifications.""" # Get current instance and shared users instance = self.get_object() - current_shared_users = set(instance.shared_users.all()) + shared_users_updated = "shared_users" in request.data + current_shared_users = set() + if shared_users_updated: + current_shared_users = set(instance.shared_users.all()) # Perform the update response = super().partial_update(request, *args, **kwargs) @@ -381,7 +384,7 @@ def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Respons # If successful and shared_users changed, send notifications if ( response.status_code == 200 - and "shared_users" in request.data + and shared_users_updated and notification_plugin ): try: diff --git a/backend/connector_v2/views.py b/backend/connector_v2/views.py index 37525e4d5a..8bcd406f72 100644 --- a/backend/connector_v2/views.py +++ b/backend/connector_v2/views.py @@ -204,13 +204,16 @@ def perform_destroy(self, instance: ConnectorInstance) -> None: def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response: """Override to handle sharing notifications.""" instance = self.get_object() - current_shared_users = set(instance.shared_users.all()) + shared_users_updated = "shared_users" in request.data + current_shared_users = set() + if shared_users_updated: + current_shared_users = set(instance.shared_users.all()) response = super().partial_update(request, *args, **kwargs) if ( response.status_code == 200 - and "shared_users" in request.data + and shared_users_updated and bool(notification_plugin) ): try: diff --git a/backend/pipeline_v2/views.py b/backend/pipeline_v2/views.py index 0d902a792c..06d80c4054 100644 --- a/backend/pipeline_v2/views.py +++ b/backend/pipeline_v2/views.py @@ -143,13 +143,16 @@ def list_of_shared_users(self, request: Request, pk: str | None = None) -> Respo def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response: """Override to handle sharing notifications.""" instance = self.get_object() - current_shared_users = set(instance.shared_users.all()) + shared_users_updated = "shared_users" in request.data + current_shared_users = set() + if shared_users_updated: + current_shared_users = set(instance.shared_users.all()) response = super().partial_update(request, *args, **kwargs) if ( response.status_code == 200 - and "shared_users" in request.data + and shared_users_updated and notification_plugin ): try: diff --git a/backend/prompt_studio/prompt_studio_core_v2/views.py b/backend/prompt_studio/prompt_studio_core_v2/views.py index 99edefd0b4..0458283aad 100644 --- a/backend/prompt_studio/prompt_studio_core_v2/views.py +++ b/backend/prompt_studio/prompt_studio_core_v2/views.py @@ -266,15 +266,17 @@ def destroy( def partial_update( self, request: Request, *args: tuple[Any], **kwargs: dict[str, Any] ) -> Response: - # Store current shared users before update for email notifications custom_tool = self.get_object() - current_shared_users = set(custom_tool.shared_users.all()) + shared_users_updated = "shared_users" in request.data + current_shared_users = set() + if shared_users_updated: + current_shared_users = set(custom_tool.shared_users.all()) # Perform the update response = super().partial_update(request, *args, **kwargs) # Send email notifications to newly shared users - if response.status_code == 200 and "shared_users" in request.data: + if response.status_code == 200 and shared_users_updated: from plugins import get_plugin notification_plugin = get_plugin("notification") diff --git a/backend/utils/tests/test_shared_user_query_optimizations.py b/backend/utils/tests/test_shared_user_query_optimizations.py new file mode 100644 index 0000000000..d734debc5b --- /dev/null +++ b/backend/utils/tests/test_shared_user_query_optimizations.py @@ -0,0 +1,60 @@ +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from rest_framework import status, viewsets +from rest_framework.response import Response + +from adapter_processor_v2.views import AdapterInstanceViewSet +from workflow_manager.workflow_v2.views import WorkflowViewSet + + +def test_workflow_partial_update_skips_shared_user_lookup_without_share_changes() -> None: + view = WorkflowViewSet() + workflow = MagicMock() + workflow.shared_users.all.side_effect = AssertionError( + "shared_users should not be queried for non-sharing updates" + ) + request = SimpleNamespace(data={"workflow_name": "renamed"}, user=MagicMock()) + + with ( + patch.object(WorkflowViewSet, "get_object", return_value=workflow), + patch.object( + viewsets.ModelViewSet, + "partial_update", + autospec=True, + return_value=Response(status=status.HTTP_200_OK), + ), + ): + response = view.partial_update(request) + + assert response.status_code == status.HTTP_200_OK + workflow.shared_users.all.assert_not_called() + + +def test_adapter_partial_update_reuses_pre_update_shared_users() -> None: + view = AdapterInstanceViewSet() + shared_user_1 = SimpleNamespace(id=1) + shared_user_2 = SimpleNamespace(id=2) + adapter = MagicMock(adapter_type="LLM", adapter_name="Adapter", id=1) + adapter.shared_users.all.side_effect = [ + [shared_user_1, shared_user_2], + [shared_user_1, shared_user_2], + ] + request = SimpleNamespace( + data={"shared_users": ["1", "2"]}, + user=MagicMock(), + ) + + with ( + patch.object(AdapterInstanceViewSet, "get_object", return_value=adapter), + patch.object( + viewsets.ModelViewSet, + "partial_update", + autospec=True, + return_value=Response(status=status.HTTP_200_OK), + ), + ): + response = view.partial_update(request) + + assert response.status_code == status.HTTP_200_OK + assert adapter.shared_users.all.call_count == 2 diff --git a/backend/workflow_manager/workflow_v2/views.py b/backend/workflow_manager/workflow_v2/views.py index 629b52bb22..8371426147 100644 --- a/backend/workflow_manager/workflow_v2/views.py +++ b/backend/workflow_manager/workflow_v2/views.py @@ -140,9 +140,12 @@ def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Respons """Override partial_update to handle sharing notifications.""" # Get the workflow instance before update workflow = self.get_object() + shared_users_updated = "shared_users" in request.data - # Store current shared users for comparison - current_shared_users = set(workflow.shared_users.all()) + # Store current shared users for comparison only when sharing changes. + current_shared_users = set() + if shared_users_updated: + current_shared_users = set(workflow.shared_users.all()) # Perform the standard partial update response = super().partial_update(request, *args, **kwargs) @@ -150,7 +153,7 @@ def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Respons # If update was successful and shared_users field was modified if ( response.status_code == 200 - and "shared_users" in request.data + and shared_users_updated and bool(notification_plugin) ): try: From 3b9a507a3026770ea058502747a2dc2d4c6fae3c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 17:58:29 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- backend/api_v2/api_deployment_views.py | 6 +----- backend/pipeline_v2/views.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/backend/api_v2/api_deployment_views.py b/backend/api_v2/api_deployment_views.py index da81496a86..8ab725f381 100644 --- a/backend/api_v2/api_deployment_views.py +++ b/backend/api_v2/api_deployment_views.py @@ -382,11 +382,7 @@ def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Respons response = super().partial_update(request, *args, **kwargs) # If successful and shared_users changed, send notifications - if ( - response.status_code == 200 - and shared_users_updated - and notification_plugin - ): + if response.status_code == 200 and shared_users_updated and notification_plugin: try: instance.refresh_from_db() new_shared_users = set(instance.shared_users.all()) diff --git a/backend/pipeline_v2/views.py b/backend/pipeline_v2/views.py index 06d80c4054..d047ab3a7e 100644 --- a/backend/pipeline_v2/views.py +++ b/backend/pipeline_v2/views.py @@ -150,11 +150,7 @@ def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Respons response = super().partial_update(request, *args, **kwargs) - if ( - response.status_code == 200 - and shared_users_updated - and notification_plugin - ): + if response.status_code == 200 and shared_users_updated and notification_plugin: try: instance.refresh_from_db() new_shared_users = set(instance.shared_users.all()) From 77916364307426d06d8b671c4cfd9188b8116a40 Mon Sep 17 00:00:00 2001 From: Blake Oxford Date: Thu, 14 May 2026 14:38:48 -0400 Subject: [PATCH 3/4] fix: add missing notification_plugin guard in adapter partial_update The other 4 resource viewsets (workflow, pipeline, connector, api_deployment) all gate their post-update shared_users comparison block on notification_plugin being truthy. The adapter viewset was missing this check, causing an extra refresh_from_db() + shared_users.all() round-trip on every share/unshare even when no notification plugin is installed. Update the test to patch notification_plugin=None explicitly and assert call_count==1 (pre-update snapshot only), which precisely pins the elimination of the old duplicate ID-extraction query. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/adapter_processor_v2/views.py | 2 +- .../tests/test_shared_user_query_optimizations.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/adapter_processor_v2/views.py b/backend/adapter_processor_v2/views.py index dfc9555ec5..b908a3c847 100644 --- a/backend/adapter_processor_v2/views.py +++ b/backend/adapter_processor_v2/views.py @@ -346,7 +346,7 @@ def partial_update( response = super().partial_update(request, *args, **kwargs) # Send email notifications to newly shared users - if response.status_code == 200 and shared_users_updated: + if response.status_code == 200 and shared_users_updated and bool(notification_plugin): try: adapter.refresh_from_db() new_shared_users = set(adapter.shared_users.all()) diff --git a/backend/utils/tests/test_shared_user_query_optimizations.py b/backend/utils/tests/test_shared_user_query_optimizations.py index d734debc5b..fd970bd6dc 100644 --- a/backend/utils/tests/test_shared_user_query_optimizations.py +++ b/backend/utils/tests/test_shared_user_query_optimizations.py @@ -31,15 +31,15 @@ def test_workflow_partial_update_skips_shared_user_lookup_without_share_changes( workflow.shared_users.all.assert_not_called() -def test_adapter_partial_update_reuses_pre_update_shared_users() -> None: +def test_adapter_partial_update_queries_shared_users_once_without_notification_plugin() -> None: + # When notification_plugin is absent (the common test-env case), the post-update + # comparison block is skipped entirely. Only one pre-update snapshot query should + # be made — the old code made two (snapshot + duplicate ID extraction). view = AdapterInstanceViewSet() shared_user_1 = SimpleNamespace(id=1) shared_user_2 = SimpleNamespace(id=2) adapter = MagicMock(adapter_type="LLM", adapter_name="Adapter", id=1) - adapter.shared_users.all.side_effect = [ - [shared_user_1, shared_user_2], - [shared_user_1, shared_user_2], - ] + adapter.shared_users.all.return_value = [shared_user_1, shared_user_2] request = SimpleNamespace( data={"shared_users": ["1", "2"]}, user=MagicMock(), @@ -53,8 +53,10 @@ def test_adapter_partial_update_reuses_pre_update_shared_users() -> None: autospec=True, return_value=Response(status=status.HTTP_200_OK), ), + patch("adapter_processor_v2.views.notification_plugin", None), ): response = view.partial_update(request) assert response.status_code == status.HTTP_200_OK - assert adapter.shared_users.all.call_count == 2 + # Pre-update snapshot only — the duplicate ID-extraction query is gone. + assert adapter.shared_users.all.call_count == 1 From 2c552a3db1ae80dec5bde935490c2549250f4f2f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 18:39:25 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- backend/adapter_processor_v2/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/adapter_processor_v2/views.py b/backend/adapter_processor_v2/views.py index b908a3c847..7660225735 100644 --- a/backend/adapter_processor_v2/views.py +++ b/backend/adapter_processor_v2/views.py @@ -346,7 +346,11 @@ def partial_update( response = super().partial_update(request, *args, **kwargs) # Send email notifications to newly shared users - if response.status_code == 200 and shared_users_updated and bool(notification_plugin): + if ( + response.status_code == 200 + and shared_users_updated + and bool(notification_plugin) + ): try: adapter.refresh_from_db() new_shared_users = set(adapter.shared_users.all())