Files
hyungi_document_server/app/services/storage/local.py
T
hyungi cb7c0fdc4f fix(workers): blocking I/O off-thread — watch_inbox·getaddrinfo·file stream (R5)
AsyncIOScheduler 가 FastAPI lifespan 과 같은 이벤트 루프를 공유하는데 동기 blocking
I/O 가 루프를 점유 → 같은 루프의 모든 1분 주기 consumer + FastAPI 요청 동시 정지.
- watch_inbox: NFS rglob walk + GB 파일 SHA-256(file_hash)을 asyncio.to_thread 오프로드.
  스캔 루프가 순차라 file_hash 직렬화 유지(병렬 해싱 X = NFS 2.5GbE 대역폭·메모리 blowup 방지).
- news create_source: validate_feed_url 의 getaddrinfo(blocking DNS) off-thread.
- storage/local stream: 청크 f.read off-thread.
marker_worker/mailplus to_thread 컨벤션 재사용. daily_digest blocking 은 R8(TZ)과 한 패스.

검증: py_compile 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:35:44 +09:00

52 lines
1.8 KiB
Python

"""LocalBackend — 현행 NAS NFS(volume4) 마운트. /file 동작 불변 (plan D-1)."""
from __future__ import annotations
import asyncio
import os
from collections.abc import AsyncIterator
from pathlib import Path
from .base import StatResult, StorageBackend
_STREAM_CHUNK = 256 * 1024
class LocalBackend(StorageBackend):
"""루트(=settings.nas_mount_path) 하위 상대경로를 로컬 파일시스템으로 해석."""
is_local = True
def __init__(self, root: str) -> None:
self._root = Path(root)
def local_path(self, rel_path: str) -> os.PathLike[str]:
return self._root / rel_path
async def stat(self, rel_path: str) -> StatResult:
p = self._root / rel_path
if not p.exists():
return StatResult(exists=False, size=0)
return StatResult(exists=True, size=p.stat().st_size)
async def stream(
self, rel_path: str, *, start: int | None = None, end: int | None = None
) -> AsyncIterator[bytes]:
"""로컬 파일을 청크 stream (Range 지원). /file 의 로컬 경로는 FileResponse 가
Range 를 자동 처리하므로 이 메서드는 인터페이스 대칭/원격 동등성을 위한 구현."""
p = self._root / rel_path
with p.open("rb") as f:
if start:
f.seek(start)
remaining = None if end is None else (end - (start or 0) + 1)
while True:
to_read = _STREAM_CHUNK if remaining is None else min(_STREAM_CHUNK, remaining)
if to_read <= 0:
break
data = await asyncio.to_thread(f.read, to_read)
if not data:
break
yield data
if remaining is not None:
remaining -= len(data)