-
Notifications
You must be signed in to change notification settings - Fork 87
[STT-1648] improve: Clear resource cache in background thread #3195
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
Merged
MarkLark86
merged 13 commits into
superdesk:develop
from
MarkLark86:STT-1648-redis-cache
Apr 15, 2026
+44
−14
Merged
Changes from 5 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
9d88e42
[STT-1648] improve: Clear resource cache in background thread
MarkLark86 86d8953
Merge branch 'develop' into STT-1648-redis-cache
MarkLark86 db81e49
pin pydantic to v2.12
MarkLark86 532ad09
Add tests
MarkLark86 0942a1a
fix test
MarkLark86 0ce8bde
Remove `task.cancel`, as `wait_for` already does this for us
MarkLark86 1ef2a80
Merge branch 'develop' into STT-1648-redis-cache
MarkLark86 4d7cd78
test: use old in-place cache clean
MarkLark86 6bbca20
Revert "fix test"
MarkLark86 ffb725d
Only clean cache for resources with it enabled
MarkLark86 3f3e29c
Enable cache on Vocabularies resource
MarkLark86 cfeffd5
Remove core.tasks and use Quart.add_background_task instead
MarkLark86 c7a6c0b
fix circular import
MarkLark86 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| from typing import Any, Callable | ||
| import asyncio | ||
| import logging | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
| _thread_tasks: set[asyncio.Task] = set() | ||
|
|
||
|
|
||
| def run_in_thread(func: Callable, *args: Any, **kwargs: Any) -> asyncio.Task: | ||
| """Run a callable in a thread pool without awaiting it.""" | ||
|
|
||
| # ``asyncio.to_thread`` copies the current context, so Quart/Flask functionality should still work | ||
| task_name = f"thread_task__{func.__module__}_{func.__qualname__}" | ||
| coroutine = asyncio.to_thread(func, *args, **kwargs) | ||
| task = asyncio.create_task(coroutine, name=task_name) | ||
| _thread_tasks.add(task) | ||
| task.add_done_callback(_handle_background_task_result) | ||
| return task | ||
|
MarkLark86 marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| async def wait_thread_tasks_to_complete(timeout: float = 10) -> None: | ||
| """Wait for all background tasks to complete | ||
|
|
||
| :param timeout: The maximum time to wait for the tasks to complete, in seconds. | ||
| """ | ||
|
|
||
| if not _thread_tasks: | ||
| return | ||
|
|
||
| # 1. Signal cancellation to all tasks | ||
| for task in _thread_tasks: | ||
|
MarkLark86 marked this conversation as resolved.
Outdated
|
||
| task.cancel() | ||
|
|
||
| try: | ||
| # 2. Wrap the gather in a wait_for to enforce the timeout | ||
| await asyncio.wait_for( | ||
| asyncio.gather(*_thread_tasks, return_exceptions=True), | ||
| timeout, | ||
| ) | ||
| except asyncio.TimeoutError: | ||
| # 3. Handle tasks that refused to stop in time | ||
| still_running = [t for t in _thread_tasks if not t.done()] | ||
| logger.warning(f"Background threads shutdown timed out. {len(still_running)} tasks still active.") | ||
| finally: | ||
| # 4. Clear the set to release references | ||
| _thread_tasks.clear() | ||
|
MarkLark86 marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| def _handle_background_task_result(task: asyncio.Task[Any]) -> None: | ||
| task_name = task.get_name() | ||
|
|
||
| try: | ||
| task.result() | ||
| except asyncio.CancelledError: | ||
| logger.warning("Background task was cancelled", extra={"task_name": task_name}) | ||
| except Exception: | ||
| logger.exception("Background task failed", extra={"task_name": task_name}) | ||
| finally: | ||
| _thread_tasks.discard(task) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import asyncio | ||
| import time | ||
| from unittest.mock import patch | ||
| from superdesk.core import tasks | ||
| from superdesk.tests import AsyncTestCase | ||
|
|
||
|
|
||
| class TasksTestCase(AsyncTestCase): | ||
| async def asyncTearDown(self): | ||
| # Clean up any remaining tasks | ||
| await tasks.wait_thread_tasks_to_complete(timeout=1) | ||
| await super().asyncTearDown() | ||
|
|
||
| async def test_run_in_thread(self): | ||
| """Test run_in_thread correctly executes the function and returns a task.""" | ||
|
|
||
| def test_func(arg1, kwarg1=None): | ||
| self.assertEqual(arg1, "foo") | ||
| self.assertEqual(kwarg1, "bar") | ||
| return "success" | ||
|
|
||
| task = tasks.run_in_thread(test_func, "foo", kwarg1="bar") | ||
|
|
||
| self.assertIsInstance(task, asyncio.Task) | ||
| self.assertIn(task, tasks._thread_tasks) | ||
| self.assertTrue(task.get_name().startswith("thread_task__")) | ||
|
|
||
| # Wait for the task itself to finish and its callback to run | ||
| await task | ||
| result = task.result() | ||
| self.assertEqual(result, "success") | ||
|
|
||
| # # The task should be removed from _thread_tasks by the callback | ||
| self.assertNotIn(task, tasks._thread_tasks) | ||
|
|
||
| async def test_wait_thread_tasks_to_complete(self): | ||
| """Test wait_thread_tasks_to_complete cancels and waits for tasks.""" | ||
|
|
||
| def long_running_func(): | ||
| # This will be interrupted if the thread is cancelled, | ||
| time.sleep(5) | ||
| raise Exception("This should not be reached") | ||
|
|
||
| with patch("superdesk.core.tasks.logger") as mock_logger: | ||
| task = tasks.run_in_thread(long_running_func) | ||
| self.assertIn(task, tasks._thread_tasks) | ||
|
|
||
| await tasks.wait_thread_tasks_to_complete(timeout=1) | ||
|
|
||
| mock_logger.warning.assert_called_with( | ||
| "Background task was cancelled", extra={"task_name": task.get_name()} | ||
| ) | ||
|
|
||
|
MarkLark86 marked this conversation as resolved.
Outdated
|
||
| self.assertTrue(task.done()) | ||
| self.assertTrue(task.cancelled()) | ||
| self.assertEqual(len(tasks._thread_tasks), 0) | ||
|
MarkLark86 marked this conversation as resolved.
Outdated
|
||
|
|
||
| async def test_handle_background_task_result_exception(self): | ||
| """Test _handle_background_task_result logs exceptions.""" | ||
|
|
||
| def failing_func(): | ||
| raise ValueError("test exception") | ||
|
|
||
| with patch("superdesk.core.tasks.logger") as mock_logger: | ||
| task = tasks.run_in_thread(failing_func) | ||
|
|
||
| with self.assertRaises(ValueError): | ||
| await task | ||
|
|
||
| mock_logger.exception.assert_called_with("Background task failed", extra={"task_name": task.get_name()}) | ||
| self.assertNotIn(task, tasks._thread_tasks) | ||
|
|
||
| async def test_handle_background_task_result_cancelled(self): | ||
| """Test _handle_background_task_result logs cancellation.""" | ||
|
|
||
| def slow_func(): | ||
| time.sleep(0.5) | ||
|
|
||
| with patch("superdesk.core.tasks.logger") as mock_logger: | ||
| task = tasks.run_in_thread(slow_func) | ||
| task.cancel() | ||
|
|
||
| with self.assertRaises(asyncio.CancelledError): | ||
| await task | ||
|
|
||
| mock_logger.warning.assert_called_with( | ||
| "Background task was cancelled", extra={"task_name": task.get_name()} | ||
| ) | ||
| self.assertNotIn(task, tasks._thread_tasks) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.