| # Chaos Engineering Test: [Failure Scenario] | |
| ## Purpose | |
| Validate system resilience to [failure type: network latency, database drop, memory pressure, service crash] | |
| **What this test validates:** | |
| - System behavior during failure: [graceful degradation, error handling, retry logic] | |
| - Recovery behavior: [automatic recovery, data integrity, rollback] | |
| - No data loss or corruption during failure | |
| **Target:** | |
| - Service: `[service_name]` | |
| - Component: `[component_name]` | |
| - Dependency: `[database, API, external service]` | |
| ## Dependencies | |
| **Required Libraries:** | |
| ```bash | |
| # Network chaos (Toxiproxy) | |
| pip install toxiproxy-python | |
| # Memory pressure | |
| pip install memory-profiler | |
| # Service chaos | |
| pip install pytest-container | |
| ``` | |
| **Target Service:** | |
| - `backend/core/[service].py` - [description of target] | |
| - `backend/api/[routes].py` - [description of target API] | |
| **Blast Radius:** | |
| - **Test database only:** `[database_name]` (NEVER production) | |
| - **Isolated test environment:** Docker compose test network | |
| - **Failure duration limit:** 60 seconds maximum | |
| ## Setup | |
| **1. Create isolated test database:** | |
| ```python | |
| @pytest.fixture(scope="function") | |
| def chaos_db_session(): | |
| """ | |
| Isolated database for chaos testing. | |
| IMPORTANT: This must be a separate database from other tests | |
| to prevent interference with concurrent test runs. | |
| """ | |
| # Create test database | |
| db_url = "sqlite:///./test_chaos.db" | |
| engine = create_engine(db_url) | |
| Base.metadata.create_all(engine) | |
| Session = sessionmaker(bind=engine) | |
| session = Session() | |
| yield session | |
| # Cleanup: Drop database after test | |
| session.close() | |
| os.remove("./test_chaos.db") | |
| ``` | |
| **2. Configure failure injection limits:** | |
| ```python | |
| # Chaos test configuration | |
| CHAOS_CONFIG = { | |
| "network_latency": { | |
| "max_latency_ms": 5000, # Maximum 5 seconds | |
| "duration_seconds": 30, # Maximum 30 seconds | |
| }, | |
| "database_drop": { | |
| "duration_seconds": 10, # Maximum 10 seconds | |
| "retry_interval_seconds": 1, | |
| }, | |
| "memory_pressure": { | |
| "max_mb": 1024, # Maximum 1GB | |
| "duration_seconds": 30, | |
| } | |
| } | |
| ``` | |
| **3. Set monitoring/alerting:** | |
| ```python | |
| import psutil | |
| import time | |
| def monitor_system_during_test(duration_seconds=60): | |
| """ | |
| Monitor system resources during chaos test. | |
| Returns: | |
| Dict with CPU, memory, disk usage metrics | |
| """ | |
| metrics = { | |
| "cpu_percent": [], | |
| "memory_mb": [], | |
| "disk_io": [], | |
| } | |
| start_time = time.time() | |
| while time.time() - start_time < duration_seconds: | |
| metrics["cpu_percent"].append(psutil.cpu_percent()) | |
| metrics["memory_mb"].append(psutil.virtual_memory().used / (1024 * 1024)) | |
| metrics["disk_io"].append(psutil.disk_io_counters()) | |
| time.sleep(1) | |
| return metrics | |
| ``` | |
| ## Test Procedure | |
| **Step 1: Baseline measurement** | |
| ```python | |
| @pytest.mark.chaos | |
| def test_[scenario]_chaos(chaos_db_session): | |
| """ | |
| Test system resilience to [failure scenario]. | |
| Scenario: [description of failure] | |
| Duration: [seconds] | |
| Blast radius: test database only | |
| """ | |
| # 1. Measure baseline operation | |
| baseline_metrics = measure_system_health() | |
| print(f"Baseline: {baseline_metrics}") | |
| # Create test data before injecting failure | |
| test_agent = create_test_agent(chaos_db_session, name="chaos_test") | |
| chaos_db_session.commit() | |
| ``` | |
| **Step 2: Inject failure** | |
| ```python | |
| # 2. Inject failure | |
| with failure_injector("[failure_type]", duration=30): | |
| # System should degrade gracefully | |
| result = execute_operation(test_agent.id) | |
| # Monitor system behavior during failure | |
| failure_metrics = measure_system_health() | |
| print(f"During failure: {failure_metrics}") | |
| # Assert graceful degradation (not crash) | |
| assert result.status in ["completed", "failed", "timeout"], \ | |
| f"Unexpected status: {result.status}" | |
| # Assert no crash (HTTP 500) | |
| if hasattr(result, "http_status"): | |
| assert result.http_status != 500, "Server crashed during failure" | |
| ``` | |
| **Step 3: Monitor and rollback** | |
| ```python | |
| # 3. Rollback: Remove failure injection | |
| # (handled by failure_injector context manager) | |
| # 4. Verify system recovers to baseline | |
| recovery_metrics = measure_system_health() | |
| print(f"Recovery: {recovery_metrics}") | |
| # Assert recovery within acceptable bounds | |
| assert abs(recovery_metrics["cpu_percent"] - baseline_metrics["cpu_percent"]) < 20, \ | |
| "CPU usage did not recover to baseline" | |
| # 5. Verify data integrity | |
| recovered_agent = chaos_db_session.query(AgentRegistry).filter_by(id=test_agent.id).first() | |
| assert recovered_agent is not None, "Agent was lost during failure" | |
| assert recovered_agent.name == "chaos_test", "Agent data corrupted" | |
| ``` | |
| **Example: Network latency chaos** | |
| ```python | |
| from toxiproxy import Toxiproxy | |
| @pytest.mark.chaos | |
| def test_network_latency_chaos(chaos_db_session): | |
| """ | |
| Test system resilience to network latency. | |
| Scenario: 2000ms network latency to database | |
| Duration: 30 seconds | |
| Blast radius: test database only | |
| """ | |
| # Setup Toxiproxy | |
| toxiproxy = Toxiproxy.create_toxiproxy("localhost:8474") | |
| proxy = toxiproxy.create_proxy( | |
| name="db_proxy", | |
| upstream="database:5432", | |
| listen="localhost:5555" | |
| ) | |
| try: | |
| # Baseline | |
| baseline_time = measure_query_time() | |
| # Inject network latency | |
| with proxy.toxic("latency", latency_ms=2000, jitter=0): | |
| # System should handle latency gracefully | |
| start_time = time.time() | |
| result = execute_database_query() | |
| latency = time.time() - start_time | |
| # Assert timeout or graceful degradation | |
| assert latency > 2, "Latency not applied" | |
| assert result.status in ["timeout", "completed"], \ | |
| f"Unexpected status: {result.status}" | |
| # Verify recovery | |
| recovery_time = measure_query_time() | |
| assert abs(recovery_time - baseline_time) < 0.5, "System did not recover" | |
| finally: | |
| # Cleanup: Remove proxy | |
| toxiproxy.destroy() | |
| ``` | |
| **Example: Database connection drop chaos** | |
| ```python | |
| @pytest.mark.chaos | |
| def test_database_drop_chaos(chaos_db_session): | |
| """ | |
| Test system resilience to database connection drops. | |
| Scenario: Database connection drops for 10 seconds | |
| Duration: 10 seconds | |
| Blast radius: test database only | |
| """ | |
| # Create test data before drop | |
| agent = create_test_agent(chaos_db_session, name="drop_test") | |
| agent_id = agent.id | |
| chaos_db_session.commit() | |
| # Baseline: Agent exists | |
| assert agent_id is not None | |
| # Inject database drop | |
| with database_connection_drop(duration=10): | |
| # System should handle connection error gracefully | |
| with pytest.raises((DatabaseError, OperationalError)): | |
| # Query should fail gracefully (not crash) | |
| chaos_db_session.query(AgentRegistry).filter_by(id=agent_id).first() | |
| # Retry logic should kick in | |
| result = retry_database_query(agent_id, max_retries=5) | |
| assert result.status in ["retry_exhausted", "timeout"], \ | |
| f"Unexpected status: {result.status}" | |
| # Verify recovery: Connection restored | |
| recovered_agent = chaos_db_session.query(AgentRegistry).filter_by(id=agent_id).first() | |
| assert recovered_agent is not None, "Agent lost after database recovery" | |
| assert recovered_agent.name == "drop_test", "Data corrupted after recovery" | |
| ``` | |
| **Example: Memory pressure chaos** | |
| ```python | |
| @pytest.mark.chaos | |
| def test_memory_pressure_chaos(chaos_db_session): | |
| """ | |
| Test system resilience to memory pressure. | |
| Scenario: Allocate 500MB memory for 30 seconds | |
| Duration: 30 seconds | |
| Blast radius: test process only | |
| """ | |
| # Baseline memory | |
| baseline_mb = psutil.virtual_memory().used / (1024 * 1024) | |
| # Inject memory pressure | |
| memory_allocator = allocate_memory(mb=500, duration=30) | |
| try: | |
| # System should handle memory pressure gracefully | |
| result = execute_memory_intensive_operation() | |
| # Assert no crash (not OutOfMemoryError) | |
| assert result is not None, "Operation failed under memory pressure" | |
| # Assert performance degradation (not crash) | |
| current_mb = psutil.virtual_memory().used / (1024 * 1024) | |
| assert current_mb > baseline_mb + 400, "Memory pressure not applied" | |
| finally: | |
| # Cleanup: Release memory | |
| memory_allocator.release() | |
| ``` | |
| ## Expected Behavior | |
| **During failure:** | |
| - System degrades gracefully (no crashes) | |
| - Error handling works (appropriate error messages) | |
| - Retry logic activates (if applicable) | |
| - No data loss or corruption | |
| - No cascading failures (other services unaffected) | |
| **After recovery:** | |
| - System returns to baseline performance | |
| - Data integrity maintained (no lost/corrupted data) | |
| - No zombie processes or resource leaks | |
| - All connections restored | |
| **Blast radius:** | |
| - Failure scoped to test database only | |
| - No impact on other test runs | |
| - No impact on production systems | |
| - Isolated test network (Docker compose) | |
| ## Blast Radius Controls | |
| **Isolation mechanisms:** | |
| 1. **Test database:** | |
| - Database: `./test_chaos.db` (separate from main test database) | |
| - Scope: Function-level fixture (fresh database per test) | |
| - Verification: Query database name before injecting failure | |
| 2. **Failure duration limit:** | |
| - Maximum: 60 seconds (enforced by pytest timeout) | |
| - Monitoring: Log start/end timestamps | |
| - Auto-rollback: Context manager ensures cleanup | |
| 3. **Injection scope:** | |
| - Network: Test network only (Docker compose network) | |
| - Database: Test database only (NEVER production) | |
| - Services: Test containers only (NEVER production) | |
| 4. **Resource limits:** | |
| - Memory: Maximum 1GB allocation | |
| - CPU: Maximum 80% usage | |
| - Duration: Maximum 60 seconds | |
| **Verification commands:** | |
| ```bash | |
| # Verify test database (NEVER production) | |
| echo $DATABASE_URL # Should be sqlite:///./test_chaos.db | |
| # Verify failure duration | |
| grep "duration_seconds" backend/tests/bug_discovery/test_*_chaos.py # Should be <= 60 | |
| # Verify isolation | |
| docker ps # Should show test containers only | |
| ``` | |
| ## Bug Filing | |
| **Automatic bug filing on resilience failure:** | |
| ```python | |
| from tests.bug_discovery.bug_filing_service import BugFilingService | |
| def file_bug_from_resilience_failure(test_name, failure_details): | |
| """ | |
| File bug for chaos engineering resilience failure. | |
| Args: | |
| test_name: Name of chaos test | |
| failure_details: Dict with failure metadata | |
| """ | |
| BugFilingService.file_bug( | |
| test_name=f"test_{test_name}_chaos", | |
| error_message=f"Resilience failure: {failure_details['failure_type']}", | |
| metadata={ | |
| "test_type": "chaos", | |
| "failure_scenario": failure_details["scenario"], | |
| "injection_duration": failure_details["duration"], | |
| "blast_radius": "test_database_only", | |
| "baseline_metrics": failure_details["baseline"], | |
| "failure_metrics": failure_details["during_failure"], | |
| "recovery_metrics": failure_details["after_recovery"], | |
| "data_loss": failure_details.get("data_loss", False), | |
| "data_corruption": failure_details.get("data_corruption", False), | |
| }, | |
| expected_behavior=f"System should degrade gracefully during {failure_details['scenario']}", | |
| actual_behavior=f"System crashed or failed to recover: {failure_details['error']}" | |
| ) | |
| ``` | |
| **Manual bug filing (if not automatic):** | |
| ```bash | |
| # Bug title: [Bug] Resilience failure: [Failure Scenario] Chaos | |
| # Bug body: | |
| ## Bug Description | |
| Chaos engineering test discovered resilience failure in [service_name]. | |
| ## Failure Scenario | |
| - Type: [network latency / database drop / memory pressure] | |
| - Duration: [seconds] | |
| - Blast radius: test database only | |
| ## Steps to Reproduce | |
| 1. Run chaos test: `pytest backend/tests/bug_discovery/test_[scenario]_chaos.py -v` | |
| 2. Inject failure: [specific failure injection steps] | |
| 3. Observe system behavior: [description of failure] | |
| ## Actual Behavior | |
| - System crashed with error: [error message] | |
| - Data loss: [yes/no] | |
| - Data corruption: [yes/no] | |
| - Cascading failures: [description] | |
| ## Expected Behavior | |
| - System should degrade gracefully (return error, not crash) | |
| - No data loss or corruption | |
| - System should recover after failure injection removed | |
| ## Metrics | |
| **Baseline:** | |
| - CPU: [percent] | |
| - Memory: [MB] | |
| - Response time: [ms] | |
| **During failure:** | |
| - CPU: [percent] | |
| - Memory: [MB] | |
| - Response time: [ms] | |
| **After recovery:** | |
| - CPU: [percent] | |
| - Memory: [MB] | |
| - Response time: [ms] | |
| ## Blast Radius Verification | |
| - Test database: [database_name] | |
| - Test network: [docker network] | |
| - Duration limit: [seconds] | |
| - Production impact: NONE (verified) | |
| ``` | |
| ## TQ Compliance | |
| **TQ-01 (Test Independence):** | |
| - Isolated test database per test (`chaos_db_session` fixture) | |
| - No shared state between chaos tests | |
| - Each test is self-contained with setup/teardown | |
| **TQ-02 (Pass Rate):** | |
| - Chaos tests are deterministic (same failure = same behavior) | |
| - Same failure injection produces same system response | |
| - 98%+ pass rate expected (failures = real bugs) | |
| **TQ-03 (Performance):** | |
| - Failure injection capped at 60s (enforced by pytest timeout) | |
| - Most chaos tests complete in 30-45s | |
| - Use `@pytest.mark.timeout(60)` to enforce limit | |
| **TQ-04 (Determinism):** | |
| - Same failure injection produces same results | |
| - Toxiproxy provides deterministic network conditions | |
| - Memory allocation is deterministic (fixed MB) | |
| **TQ-05 (Coverage Quality):** | |
| - Tests resilience behavior (observable system behavior) | |
| - Not implementation details (internal error handling) | |
| - Validates graceful degradation (user-facing) | |
| ## pytest.ini Marker | |
| Add to `backend/pytest.ini`: | |
| ```ini | |
| [pytest] | |
| markers = | |
| chaos: Chaos engineering tests (failure injection, isolated environment, slow) | |
| ``` | |
| Run only chaos tests: | |
| ```bash | |
| pytest backend/tests/bug_discovery/ -v -m chaos | |
| ``` | |
| Skip chaos tests in fast CI: | |
| ```bash | |
| pytest backend/tests/ -v -m "not chaos" | |
| ``` | |
| ## Safety Checks | |
| **Before running chaos tests:** | |
| ```bash | |
| # Verify environment | |
| echo $ENVIRONMENT # Should be "test" or "development" | |
| # Verify database | |
| echo $DATABASE_URL # Should be test database | |
| # Verify no production access | |
| ping production.example.com # Should FAIL (no access) | |
| ``` | |
| **During chaos tests:** | |
| ```python | |
| # Assert blast radius | |
| def assert_blast_radius(): | |
| """Ensure failure is scoped to test environment only.""" | |
| db_url = os.getenv("DATABASE_URL") | |
| assert "test" in db_url or "dev" in db_url, \ | |
| f"Unsafe: Database URL appears to be production: {db_url}" | |
| # Assert no production endpoints | |
| production_endpoints = ["api.production.com", "prod-db.example.com"] | |
| for endpoint in production_endpoints: | |
| assert endpoint not in db_url, \ | |
| f"Unsafe: Production endpoint in URL: {endpoint}" | |
| ``` | |
| **After chaos tests:** | |
| ```bash | |
| # Verify cleanup | |
| ls -la ./test_chaos.db # Should be removed | |
| docker ps # Should show no running toxiproxy containers | |
| ``` | |
| ## See Also | |
| - [Chaos Engineering Principles](https://principlesofchaos.org) | |
| - [Toxiproxy Python Documentation](https://github.com/ihucos/toxiproxy-python) | |
| - `backend/docs/TEST_QUALITY_STANDARDS.md` - TQ-01 through TQ-05 | |
| - `backend/tests/bug_discovery/TEMPLATES/README.md` - Template usage guide | |