fix: eliminate greenlet crash + memory leak protection
- docker-compose: switch worker to --pool=solo --concurrency=1 --max-tasks-per-child=1 - tasks.py: remove ThreadPoolExecutor wrapper from _run_browser_job (greenlet crash) - scraper.py: browser fallback runs sequentially instead of ThreadPoolExecutor - scraper.py: add gc.collect() after scraper close to prevent memory leaks
This commit is contained in:
@@ -144,9 +144,9 @@ services:
|
|||||||
stop_grace_period: 60s
|
stop_grace_period: 60s
|
||||||
command: >
|
command: >
|
||||||
celery -A iaai_scraper.worker.celery_app worker
|
celery -A iaai_scraper.worker.celery_app worker
|
||||||
--loglevel=info --concurrency=${CELERY_WORKER_CONCURRENCY:-2} --pool=prefork
|
--loglevel=info --concurrency=1 --pool=solo
|
||||||
--pidfile=/tmp/celery-worker.pid
|
--pidfile=/tmp/celery-worker.pid
|
||||||
-Q scraping --max-tasks-per-child=${CELERY_WORKER_MAX_TASKS_PER_CHILD:-3}
|
-Q scraping --max-tasks-per-child=${CELERY_WORKER_MAX_TASKS_PER_CHILD:-1}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "test -f /tmp/celery-worker.pid && kill -0 $(cat /tmp/celery-worker.pid)"]
|
test: ["CMD-SHELL", "test -f /tmp/celery-worker.pid && kill -0 $(cat /tmp/celery-worker.pid)"]
|
||||||
interval: 60s
|
interval: 60s
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import gc
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -201,6 +202,7 @@ class IAAIScraper:
|
|||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
self._http_pool = None
|
self._http_pool = None
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
def _new_context(self) -> BrowserContext:
|
def _new_context(self) -> BrowserContext:
|
||||||
if self.browser is None:
|
if self.browser is None:
|
||||||
@@ -1623,19 +1625,13 @@ class IAAIScraper:
|
|||||||
"protection_events": local_protection,
|
"protection_events": local_protection,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Одна страница — в главном потоке.
|
# Playwright sync API использует greenlets, привязанные к потоку.
|
||||||
if n_pages == 1:
|
# Запуск в ThreadPoolExecutor вызывает greenlet crash.
|
||||||
res = _process_slice(slices[0] if slices else [])
|
# Обрабатываем все слайсы последовательно в текущем потоке.
|
||||||
records.extend(res["records"])
|
for s in slices:
|
||||||
failures.extend(res["failures"])
|
if not s:
|
||||||
cars_failed += res["cars_failed"]
|
continue
|
||||||
protection_events += res["protection_events"]
|
res = _process_slice(s)
|
||||||
else:
|
|
||||||
# Несколько страниц — параллельно, каждый поток со своей Page.
|
|
||||||
with ThreadPoolExecutor(max_workers=n_pages) as executor:
|
|
||||||
futures = [executor.submit(_process_slice, s) for s in slices if s]
|
|
||||||
for fut in as_completed(futures):
|
|
||||||
res = fut.result()
|
|
||||||
records.extend(res["records"])
|
records.extend(res["records"])
|
||||||
failures.extend(res["failures"])
|
failures.extend(res["failures"])
|
||||||
cars_failed += res["cars_failed"]
|
cars_failed += res["cars_failed"]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# Задачи Celery для синхронизации автомобилей и листинга IAAI.
|
# Задачи Celery для синхронизации автомобилей и листинга IAAI.
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -61,35 +60,11 @@ def _retry_with_backoff(func, *, attempts: int = 5, base_delay_s: float = 1.0):
|
|||||||
|
|
||||||
|
|
||||||
def _run_browser_job(func, *args, **kwargs):
|
def _run_browser_job(func, *args, **kwargs):
|
||||||
# Браузерный код запускаем в отдельном потоке без активного loop.
|
# С pool=solo Celery worker работает в одном процессе/потоке.
|
||||||
# НЕ используем `with` — иначе shutdown(wait=True) заблокирует main thread
|
# Playwright sync API использует greenlets, которые привязаны к потоку.
|
||||||
# если SoftTimeLimitExceeded прервёт future.result(), а browser thread ещё работает.
|
# Запуск в отдельном потоке вызывает greenlet.error: cannot switch to a different thread.
|
||||||
settings = Settings()
|
# Поэтому запускаем напрямую в текущем потоке.
|
||||||
soft = settings.celery.task_soft_time_limit
|
return func(*args, **kwargs)
|
||||||
hard = settings.celery.task_time_limit
|
|
||||||
# Таймаут для future.result(): берём hard limit (или soft + 120), чтобы не висеть вечно.
|
|
||||||
wait_timeout = min(hard, soft + 120) if soft and hard else None
|
|
||||||
|
|
||||||
executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="iaai-browser")
|
|
||||||
future = executor.submit(func, *args, **kwargs)
|
|
||||||
try:
|
|
||||||
result = future.result(timeout=wait_timeout)
|
|
||||||
except SoftTimeLimitExceeded:
|
|
||||||
# Отпускаем executor без ожидания — thread умрёт когда Celery убьёт процесс (hard limit).
|
|
||||||
future.cancel()
|
|
||||||
executor.shutdown(wait=False, cancel_futures=True)
|
|
||||||
raise
|
|
||||||
except TimeoutError:
|
|
||||||
# future.result(timeout=...) вышел по таймауту — browser thread завис.
|
|
||||||
future.cancel()
|
|
||||||
executor.shutdown(wait=False, cancel_futures=True)
|
|
||||||
raise SoftTimeLimitExceeded("Browser thread did not finish within time limit")
|
|
||||||
except Exception:
|
|
||||||
executor.shutdown(wait=False, cancel_futures=True)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
executor.shutdown(wait=True)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _sync_listing_lock_ttl_seconds() -> int:
|
def _sync_listing_lock_ttl_seconds() -> int:
|
||||||
|
|||||||
Reference in New Issue
Block a user