|
| 1 | +# CI Threading Crash Fix - Session Summary |
| 2 | + |
| 3 | +## Problem |
| 4 | +CI tests were failing with threading crashes during pytest collection/execution. Threads were stuck in `queue.get()` calls causing Python to abort. |
| 5 | + |
| 6 | +``` |
| 7 | +Fatal Python error: Aborted |
| 8 | +Thread X: File "queue.py", line 180 in get |
| 9 | + File "cdisplayagain.py", line 424 in _run |
| 10 | +``` |
| 11 | + |
| 12 | +## Attempted Solutions |
| 13 | + |
| 14 | +### 1. Try/Finally Blocks ✅ (Commit: e179690) |
| 15 | +- Added `try/finally` blocks to 16 tests across 3 files |
| 16 | +- Ensures `worker.stop()` is called before Tk root destruction |
| 17 | +- **Result**: Tests pass locally, CI still crashes |
| 18 | +- **Issue**: Too verbose, not Pythonic |
| 19 | + |
| 20 | +### 2. Context Manager ✅ (Commit: c19ea81) |
| 21 | +- Converted `ImageWorker` to a context manager |
| 22 | +- Added `__enter__` and `__exit__` methods |
| 23 | +- Updated tests to use `with ImageWorker() as worker:` pattern |
| 24 | +- **Result**: Tests pass locally (411 passed, 95.99% coverage) |
| 25 | +- **Issue**: CI still crashes with same threading errors |
| 26 | + |
| 27 | +### 3. Worker Thread Safety Improvements |
| 28 | +- Added `_stopped` flag to `ImageWorker` |
| 29 | +- Added early exit checks in `_run()` loop |
| 30 | +- Set `self._app = None` in `stop()` to prevent access after cleanup |
| 31 | +- Added exception handling in `thread.join()` |
| 32 | +- **Current State**: Tests pass locally, but: |
| 33 | + - Pyright type checking fails on 2 lines (app._app could be None) |
| 34 | + - CI is likely still failing |
| 35 | + |
| 36 | +## Root Cause Analysis |
| 37 | + |
| 38 | +The issue appears to be that even with context managers, worker threads are processing tasks when the Tk root is destroyed during test teardown. The threads are: |
| 39 | +1. Stuck in `queue.get()` waiting for work |
| 40 | +2. Or in the middle of processing (calling `get_resized_pil()`) |
| 41 | +3. Trying to access Tk (`after_idle`) when Tk is already destroyed |
| 42 | + |
| 43 | +## Current Code State |
| 44 | + |
| 45 | +### ImageWorker Changes |
| 46 | +```python |
| 47 | +class ImageWorker: |
| 48 | + _app: ComicViewer | None = None |
| 49 | + |
| 50 | + def __init__(self, app, num_workers: int = 4): |
| 51 | + self._app = app |
| 52 | + self._stopped: bool = False |
| 53 | + # ... start threads |
| 54 | + |
| 55 | + def stop(self): |
| 56 | + self._stopped = True |
| 57 | + self._app = None # Prevent access after cleanup |
| 58 | + # ... send stop signals to queue |
| 59 | + |
| 60 | + def __enter__(self): |
| 61 | + return self |
| 62 | + |
| 63 | + def __exit__(self, exc_type, exc_val, exc_tb): |
| 64 | + self.stop() |
| 65 | + return False |
| 66 | +``` |
| 67 | + |
| 68 | +### ComicViewer Changes |
| 69 | +```python |
| 70 | +class ComicViewer(tk.Frame): |
| 71 | + def __del__(self): |
| 72 | + self.cleanup() |
| 73 | + |
| 74 | + def cleanup(self): |
| 75 | + if hasattr(self, "_worker") and self._worker: |
| 76 | + self._worker.stop() |
| 77 | +``` |
| 78 | + |
| 79 | +## Remaining Issues |
| 80 | + |
| 81 | +### Type Checking Errors ✅ FIXED |
| 82 | +- Added `assert app is not None` after null check to satisfy pyright |
| 83 | +- Added `if source is None: break` check to handle None source gracefully |
| 84 | +- All type checking errors resolved (0 errors, 0 warnings, 0 informations) |
| 85 | + |
| 86 | +### Coverage ✅ FIXED |
| 87 | +- Current: 96.00% |
| 88 | +- Target: 96% |
| 89 | +- Gap: 0% (target reached!) |
| 90 | +- Added test `test_worker_handles_none_source_gracefully` to cover source=None path |
| 91 | + |
| 92 | +## Files Modified |
| 93 | + |
| 94 | +### cdisplayagain.py |
| 95 | +- Added context manager methods to `ImageWorker` |
| 96 | +- Added `_stopped` flag for graceful shutdown |
| 97 | +- Added `cleanup()` method to `ComicViewer` |
| 98 | +- Modified `_run()` to check `_stopped` flag multiple times |
| 99 | +- Modified `stop()` to handle `queue.Full` and set `_app = None` |
| 100 | +- ✅ Added `assert app is not None` after null check (fixes pyright errors) |
| 101 | +- ✅ Added `if source is None: break` check (handles shutdown gracefully) |
| 102 | + |
| 103 | +### tests/test_parallel_workers.py |
| 104 | +- Converted 10 tests from `try/finally` to context manager pattern |
| 105 | +- ✅ Added `test_worker_handles_none_source_gracefully` to increase coverage to 96% |
| 106 | + |
| 107 | +### tests/test_threading.py |
| 108 | +- Converted 4 tests from `try/finally` to context manager pattern |
| 109 | + |
| 110 | +### tests/test_benchmark_parallel.py |
| 111 | +- Converted 2 tests from `try/finally` to context manager pattern |
| 112 | + |
| 113 | +### tests/conftest.py |
| 114 | +- Tried adding autouse fixture to track/cleanup all viewers (reverted - caused issues) |
| 115 | + |
| 116 | +## Next Steps |
| 117 | + |
| 118 | +1. ~~**Fix type checking errors**: Use local variable `app = self._app` after null check to satisfy pyright~~ ✅ DONE |
| 119 | +2. **Investigate CI-specific issue**: The threading crash still occurs in CI but not locally - suggests: |
| 120 | + - Timing difference in CI environment |
| 121 | + - Different garbage collection behavior |
| 122 | + - xvfb-run virtual display interaction |
| 123 | + - ✅ Type checking and coverage are now fixed, ready to test on CI |
| 124 | +3. **Alternative approaches to consider if CI still fails**: |
| 125 | + - Make workers non-daemon and ensure explicit cleanup |
| 126 | + - Add a test-wide fixture that ensures all workers are stopped before any Tk root is destroyed |
| 127 | + - Increase timeout in `thread.join()` |
| 128 | + - Use a different shutdown mechanism (e.g., event instead of queue sentinel) |
| 129 | + |
| 130 | +## Commands to Run |
| 131 | + |
| 132 | +```bash |
| 133 | +# Run tests |
| 134 | +uv run pytest tests/ --ignore=tests/test_benchmark_parallel.py |
| 135 | + |
| 136 | +# Run linting |
| 137 | +make lint |
| 138 | + |
| 139 | +# Check type errors specifically |
| 140 | +uv run pyright cdisplayagain.py |
| 141 | +``` |
| 142 | + |
| 143 | +## Git Status |
| 144 | + |
| 145 | +- Branch: `move-test-coverage-marker` |
| 146 | +- Last commits: |
| 147 | + - e179690: Fix CI threading crash by stopping worker threads in tests (try/finally) |
| 148 | + - c19ea81: Refactor worker cleanup to use context manager |
| 149 | +- Working tree: Dirty (uncommitted changes to cdisplayagain.py and tests/test_parallel_workers.py) |
| 150 | + - Fixed type checking errors (assert app is not None) |
| 151 | + - Added source=None check in worker loop |
| 152 | + - Added test_worker_handles_none_source_gracefully test |
| 153 | + - Coverage now at 96.00%, all linting passes |
0 commit comments