fix(y_finance): make suppress_yfinance_warnings thread-safe
The previous implementation redirected sys.stderr to /dev/null using a context manager. This is not thread-safe: 8 concurrent scanner threads each mutate sys.stderr, and when one thread's context manager closes the devnull file, another thread that captured devnull as its saved stderr attempts to write to the closed fd and raises "I/O operation on closed file". This corrupted sys.stderr state caused _fetch_batch_prices to fail and all per-ticker get_stock_price fallback calls to return None, resulting in every candidate being dropped with "no data available". Fix by suppressing at the Python logging level instead of redirecting sys.stderr. Logger.setLevel() is protected by internal locks and is safe to call from concurrent threads. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3a69427dc8
commit
c09cc7ec25
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
|
|
@ -20,17 +19,39 @@ logger = get_logger(__name__)
|
|||
|
||||
@contextmanager
|
||||
def suppress_yfinance_warnings():
|
||||
"""Suppress yfinance stderr warnings about delisted tickers."""
|
||||
"""Suppress yfinance log and warning output in a thread-safe way.
|
||||
|
||||
Previous implementation redirected sys.stderr to /dev/null, but that is
|
||||
NOT thread-safe: concurrent scanner threads each mutate the process-global
|
||||
sys.stderr, causing race conditions where one thread closes a file descriptor
|
||||
that another thread is still writing to ("I/O operation on closed file").
|
||||
|
||||
This implementation suppresses at the Python logging level, which is
|
||||
protected by internal locks and therefore safe to call from many threads.
|
||||
"""
|
||||
import logging
|
||||
|
||||
yf_logger_names = [
|
||||
"yfinance",
|
||||
"yfinance.base",
|
||||
"yfinance.utils",
|
||||
"peewee",
|
||||
"urllib3.connectionpool",
|
||||
"urllib3",
|
||||
]
|
||||
saved_levels = {}
|
||||
for name in yf_logger_names:
|
||||
lgr = logging.getLogger(name)
|
||||
saved_levels[name] = lgr.level
|
||||
lgr.setLevel(logging.CRITICAL)
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore")
|
||||
# Redirect stderr to devnull temporarily
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stderr.close()
|
||||
sys.stderr = old_stderr
|
||||
for name, level in saved_levels.items():
|
||||
logging.getLogger(name).setLevel(level)
|
||||
|
||||
|
||||
def get_ticker_info(symbol: str) -> dict:
|
||||
|
|
|
|||
Loading…
Reference in New Issue