|
3 | 3 | """ |
4 | 4 |
|
5 | 5 | import datetime |
| 6 | +import threading |
| 7 | +import time |
6 | 8 | from unittest.mock import patch |
7 | 9 |
|
8 | 10 | import pytest |
@@ -977,3 +979,47 @@ def test_render_with_consts(context_manager): |
977 | 979 | assert ( |
978 | 980 | result == expected_result |
979 | 981 | ), f"Expected '{expected_result}', but got '{result}'" |
| 982 | + |
| 983 | + |
| 984 | +def test_concurrent_render_no_stderr_race(context_manager): |
| 985 | + """Test that concurrent render calls don't cause a race condition on sys.stderr. |
| 986 | +
|
| 987 | + Before the fix, multiple threads calling render() simultaneously could hit a race |
| 988 | + where one thread restores sys.stderr to the original TextIOWrapper while another |
| 989 | + thread tries to call .getvalue() on sys.stderr, causing: |
| 990 | + AttributeError: '_io.TextIOWrapper' object has no attribute 'getvalue' |
| 991 | +
|
| 992 | + The fix uses a local StringIO reference instead of reading from sys.stderr. |
| 993 | + See: https://github.com/keephq/keep/issues/6079 |
| 994 | + """ |
| 995 | + iohandler = IOHandler(context_manager) |
| 996 | + errors = [] |
| 997 | + barrier = threading.Barrier(10) |
| 998 | + |
| 999 | + # Add a small delay inside render_recursively to force thread interleaving, |
| 1000 | + # making the race condition deterministic rather than timing-dependent. |
| 1001 | + original_render = iohandler.render_recursively |
| 1002 | + |
| 1003 | + def slow_render(key, context): |
| 1004 | + result = original_render(key, context) |
| 1005 | + time.sleep(0.001) |
| 1006 | + return result |
| 1007 | + |
| 1008 | + iohandler.render_recursively = slow_render |
| 1009 | + |
| 1010 | + def render_template(thread_id): |
| 1011 | + try: |
| 1012 | + barrier.wait(timeout=5) |
| 1013 | + for _ in range(20): |
| 1014 | + result = iohandler.render(f"hello from thread {thread_id}") |
| 1015 | + assert result == f"hello from thread {thread_id}" |
| 1016 | + except Exception as e: |
| 1017 | + errors.append(e) |
| 1018 | + |
| 1019 | + threads = [threading.Thread(target=render_template, args=(i,)) for i in range(10)] |
| 1020 | + for t in threads: |
| 1021 | + t.start() |
| 1022 | + for t in threads: |
| 1023 | + t.join(timeout=30) |
| 1024 | + |
| 1025 | + assert not errors, f"Concurrent render raised errors: {errors}" |
0 commit comments