Add concurrent anime processing support
- Modified BackgroundLoaderService to use multiple workers (default: 5) - Anime additions now process in parallel without blocking - Added comprehensive unit tests for concurrent behavior - Updated integration tests for compatibility - Updated architecture documentation
This commit is contained in:
118
tests/api/test_concurrent_anime_add.py
Normal file
118
tests/api/test_concurrent_anime_add.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Integration test for concurrent anime additions via API endpoint.
|
||||
|
||||
This test verifies that the /api/anime/add endpoint can handle
|
||||
multiple concurrent requests without blocking.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from src.server.fastapi_app import app
|
||||
from src.server.services.auth_service import auth_service
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def authenticated_client():
|
||||
"""Create authenticated async client."""
|
||||
if not auth_service.is_configured():
|
||||
auth_service.setup_master_password("TestPass123!")
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
# Login to get token
|
||||
r = await client.post(
|
||||
"/api/auth/login", json={"password": "TestPass123!"}
|
||||
)
|
||||
if r.status_code == 200:
|
||||
token = r.json()["access_token"]
|
||||
client.headers["Authorization"] = f"Bearer {token}"
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_anime_add_requests(authenticated_client):
|
||||
"""Test that multiple anime add requests can be processed concurrently.
|
||||
|
||||
This test sends multiple anime add requests simultaneously and verifies:
|
||||
1. All requests return 202 Accepted
|
||||
2. All requests complete within a reasonable time (indicating no blocking)
|
||||
3. Each anime is added successfully with correct response structure
|
||||
"""
|
||||
# Define multiple anime to add
|
||||
anime_list = [
|
||||
{"link": "https://aniworld.to/anime/stream/test-anime-1", "name": "Test Anime 1"},
|
||||
{"link": "https://aniworld.to/anime/stream/test-anime-2", "name": "Test Anime 2"},
|
||||
{"link": "https://aniworld.to/anime/stream/test-anime-3", "name": "Test Anime 3"},
|
||||
]
|
||||
|
||||
# Track start time
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
# Send all requests concurrently
|
||||
tasks = []
|
||||
for anime in anime_list:
|
||||
task = authenticated_client.post("/api/anime/add", json=anime)
|
||||
tasks.append(task)
|
||||
|
||||
# Wait for all responses
|
||||
responses = await asyncio.gather(*tasks)
|
||||
|
||||
# Calculate total time
|
||||
total_time = time.time() - start_time
|
||||
|
||||
# Verify all responses
|
||||
for i, response in enumerate(responses):
|
||||
# All should return 202 or handle existing anime
|
||||
assert response.status_code in (202, 200), (
|
||||
f"Request {i} failed with status {response.status_code}"
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
|
||||
# Verify response structure
|
||||
assert "status" in data
|
||||
assert data["status"] in ("success", "exists")
|
||||
assert "key" in data
|
||||
assert "folder" in data
|
||||
assert "loading_status" in data
|
||||
assert "loading_progress" in data
|
||||
|
||||
# Verify requests completed quickly (indicating non-blocking behavior)
|
||||
# With blocking, 3 requests might take 3x the time of a single request
|
||||
# With concurrent processing, they should complete in similar time
|
||||
assert total_time < 5.0, (
|
||||
f"Concurrent requests took {total_time:.2f}s, "
|
||||
f"indicating possible blocking issues"
|
||||
)
|
||||
|
||||
print(f"✓ 3 concurrent anime add requests completed in {total_time:.2f}s")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_same_anime_concurrent_add(authenticated_client):
|
||||
"""Test that adding the same anime twice concurrently is handled correctly.
|
||||
|
||||
The second request should return 'exists' status rather than creating
|
||||
a duplicate entry.
|
||||
"""
|
||||
anime = {"link": "https://aniworld.to/anime/stream/concurrent-test", "name": "Concurrent Test"}
|
||||
|
||||
# Send two requests for the same anime concurrently
|
||||
task1 = authenticated_client.post("/api/anime/add", json=anime)
|
||||
task2 = authenticated_client.post("/api/anime/add", json=anime)
|
||||
|
||||
responses = await asyncio.gather(task1, task2)
|
||||
|
||||
# At least one should succeed
|
||||
statuses = [r.json()["status"] for r in responses]
|
||||
assert "success" in statuses or all(s == "exists" for s in statuses), (
|
||||
"Expected at least one success or all exists responses"
|
||||
)
|
||||
|
||||
# Both should have the same key
|
||||
keys = [r.json()["key"] for r in responses]
|
||||
assert keys[0] == keys[1], "Both responses should have the same key"
|
||||
|
||||
print(f"✓ Concurrent same-anime requests handled correctly: {statuses}")
|
||||
Reference in New Issue
Block a user