Some updates, mostly problems with postgres permissions and logs permissions, mess with improper master management service processing from .env

This commit is contained in:
aperez 2025-09-17 12:04:11 +03:00
parent b3fee7eec2
commit 2786f5ba72
14 changed files with 301 additions and 47 deletions

View File

@ -1,4 +1,35 @@
# This file should be generated from ansible/templates/.env.ytdlp.j2
# Do not edit manually - your changes will be overwritten.
#
# To generate this file, run the Ansible playbook that processes the templates.
# This file is managed by Ansible. Do not edit manually.
# --- Common Settings ---
HOSTNAME="af-green"
COMPOSE_PROJECT_NAME="ytdlp-ops-management"
TZ="Europe/Moscow"
service_role="management"
# --- Docker Image Settings ---
YTDLP_OPS_IMAGE="pangramia/ytdlp-ops-server:latest"
AIRFLOW_IMAGE_NAME="pangramia/ytdlp-ops-airflow:latest"
# --- Network Settings ---
ENVOY_PORT=9080
ENVOY_ADMIN_PORT=9901
YTDLP_BASE_PORT=9090
YTDLP_WORKERS=3
MANAGEMENT_SERVICE_PORT=9091
REDIS_PORT=52909
POSTGRES_PORT=5432
# --- Security Settings ---
REDIS_PASSWORD="rOhTAIlTFFylXsjhqwxnYxDChFc"
POSTGRES_PASSWORD="pgdb_pwd_A7bC2xY9zE1wV5uP"
AIRFLOW_ADMIN_PASSWORD="2r234sdfrt3q454arq45q355"
FLOWER_PASSWORD="dO4eXm7UkF81OdMvT8E2tIKFtPYPCzyzwlcZ4RyOmCsmG4qzrNFqM5sNTOT9"
# --- User and Group IDs ---
AIRFLOW_UID=1003
AIRFLOW_GID=1001
# --- Master-specific settings ---
MASTER_HOST_IP=89.253.221.173
# Camoufox is not used on master, but the config generator expects the variable.
CAMOUFOX_PROXIES=

View File

@ -149,6 +149,7 @@ services:
retries: 5
start_period: 5s
restart: always
user: "999:999"
redis:
# Redis is limited to 7.2-bookworm due to licencing change

View File

@ -78,7 +78,7 @@ services:
- "--service-role"
- "{{ service_role }}"
{% if service_role != 'management' %}
{% if service_role is defined and service_role != 'management' %}
# --- Parameters for worker/all-in-one roles ONLY ---
- "--script-dir"
- "/app"
@ -100,9 +100,7 @@ services:
volumes:
context-data:
{% if service_role == 'management' or not camoufox_proxies %}
networks:
proxynet:
name: airflow_proxynet
external: true
{% endif %}

View File

@ -8,6 +8,6 @@ services:
- ./.env
volumes:
# Mount the entire project directory to access scripts and write output files
- ../:/app
- .:/app
command: >
sh -c "pip install jinja2 && python3 /app/generate_envoy_config.py"

View File

@ -0,0 +1,135 @@
# Архитектура и описание YTDLP Airflow DAGs
Этот документ описывает архитектуру и назначение DAG'ов, используемых для скачивания видео с YouTube. Система построена на модели непрерывного, самоподдерживающегося цикла для параллельной и отказоустойчивой обработки.
## Основной цикл обработки
Обработка выполняется двумя основными DAG'ами, которые работают в паре: оркестратор и воркер.
### `ytdlp_ops_orchestrator` (Система "зажигания")
- **Назначение:** Этот DAG действует как "система зажигания" для запуска обработки. Он запускается вручную для старта указанного количества параллельных циклов-воркеров.
- **Принцип работы:**
- Он **не** обрабатывает URL-адреса самостоятельно.
- Его единственная задача — запустить сконфигурированное количество DAG'ов `ytdlp_ops_worker_per_url`.
- Он передает всю необходимую конфигурацию (пул аккаунтов, подключение к Redis и т.д.) воркерам.
### `ytdlp_ops_worker_per_url` (Самоподдерживающийся воркер)
- **Назначение:** Этот DAG обрабатывает один URL и спроектирован для работы в непрерывном цикле.
- **Принцип работы:**
1. **Запуск:** Начальный запуск инициируется `ytdlp_ops_orchestrator`.
2. **Получение задачи:** Воркер извлекает один URL из очереди `_inbox` в Redis. Если очередь пуста, выполнение воркера завершается, и его "линия" обработки останавливается.
3. **Обработка:** Он взаимодействует с сервисом `ytdlp-ops-server` для получения `info.json` и прокси, после чего скачивает видео.
4. **Продолжение или остановка:**
- **В случае успеха:** Он запускает новый экземпляр самого себя, создавая непрерывный цикл для обработки следующего URL.
- **В случае сбоя:** Цикл прерывается (если `stop_on_failure` установлено в `True`), останавливая эту "линию" обработки. Это предотвращает остановку всей системы из-за одного проблемного URL или аккаунта.
## Управляющие DAG'и
### `ytdlp_mgmt_proxy_account`
- **Назначение:** Это основной инструмент для мониторинга и управления состоянием ресурсов, используемых `ytdlp-ops-server`.
- **Функциональность:**
- **Просмотр статусов:** Позволяет увидеть текущий статус всех прокси и аккаунтов (например, `ACTIVE`, `BANNED`, `RESTING`).
- **Управление прокси:** Позволяет вручную банить, разбанивать или сбрасывать статус прокси.
- **Управление аккаунтами:** Позволяет вручную банить или разбанивать аккаунты.
### `ytdlp_mgmt_queues`
- **Назначение:** Предоставляет набор инструментов для управления очередями Redis, используемыми в конвейере обработки.
- **Функциональность (через параметр `action`):**
- `add_videos`: Добавление одного или нескольких URL-адресов YouTube в очередь.
- `clear_queue`: Очистка (удаление) указанного ключа Redis.
- `list_contents`: Просмотр содержимого ключа Redis (списка или хэша).
- `check_status`: Проверка общего состояния очередей (тип, размер).
- `requeue_failed`: Перемещение всех URL-адресов из очереди сбоев `_fail` обратно в очередь `_inbox` для повторной обработки.
## Стратегия управления ресурсами (Прокси и Аккаунты)
Система использует интеллектуальную стратегию для управления жизненным циклом и состоянием аккаунтов и прокси, чтобы максимизировать процент успеха и минимизировать блокировки.
- **Жизненный цикл аккаунта ("Cooldown"):**
- Чтобы предотвратить "выгорание", аккаунты автоматически переходят в состояние "отдыха" (`RESTING`) после периода интенсивного использования.
- По истечении периода отдыха они автоматически возвращаются в `ACTIVE` и снова становятся доступными для воркеров.
- **Умная стратегия банов:**
- **Сначала бан аккаунта:** При возникновении серьезной ошибки (например, `BOT_DETECTED`) система наказывает **только аккаунт**, который вызвал сбой. Прокси при этом продолжает работать.
- **Бан прокси по "скользящему окну":** Прокси банится автоматически, только если он демонстрирует **систематические сбои с РАЗНЫМИ аккаунтами** за короткий промежуток времени. Это является надежным индикатором того, что проблема именно в прокси.
- **Мониторинг:**
- DAG `ytdlp_mgmt_proxy_account` является основным инструментом для мониторинга. Он показывает текущий статус всех ресурсов, включая время, оставшееся до активации забаненных или отдыхающих аккаунтов.
- Граф выполнения DAG `ytdlp_ops_worker_per_url` теперь явно показывает шаги, такие как `assign_account`, `get_token`, `ban_account`, `retry_get_token`, что делает процесс отладки более наглядным.
## Внешние сервисы
### `ytdlp-ops-server` (Thrift Service)
- **Назначение:** Внешний сервис, который предоставляет аутентификационные данные (токены, cookies, proxy) для скачивания видео.
- **Взаимодействие:** Worker DAG (`ytdlp_ops_worker_per_url`) обращается к этому сервису перед началом загрузки для получения необходимых данных для `yt-dlp`.
## Логика работы Worker DAG (`ytdlp_ops_worker_per_url`)
Этот DAG является "рабочей лошадкой" системы. Он спроектирован как самоподдерживающийся цикл для обработки одного URL за запуск.
### Задачи и их назначение:
- **`pull_url_from_redis`**: Извлекает один URL из очереди `_inbox` в Redis. Если очередь пуста, DAG завершается со статусом `skipped`, останавливая эту "линию" обработки.
- **`assign_account`**: Выбирает аккаунт для выполнения задачи. Он будет повторно использовать тот же аккаунт, который был успешно использован в предыдущем запуске в своей "линии" (привязка аккаунта). Если это первый запуск, он выбирает случайный аккаунт.
- **`get_token`**: Основная задача. Она обращается к `ytdlp-ops-server` для получения `info.json`.
- **`handle_bannable_error_branch`**: Если `get_token` завершается с ошибкой, требующей бана, эта задача-развилка решает, что делать дальше, в зависимости от политики `on_bannable_failure`.
- **`ban_account_and_prepare_for_retry`**: Если политика разрешает повтор, эта задача банит сбойный аккаунт и выбирает новый для повторной попытки.
- **`retry_get_token`**: Выполняет вторую попытку получить токен с новым аккаунтом.
- **`ban_second_account_and_proxy`**: Если и вторая попытка неудачна, эта задача банит второй аккаунт и использованный прокси.
- **`download_and_probe`**: Если `get_token` (или `retry_get_token`) завершилась успешно, эта задача использует `yt-dlp` для скачивания медиа и `ffmpeg` для проверки целостности скачанного файла.
- **`mark_url_as_success`**: Если `download_and_probe` завершилась успешно, эта задача записывает результат в хэш `_result` в Redis.
- **`handle_generic_failure`**: Если любая из основных задач завершается с неисправимой ошибкой, эта задача записывает подробную информацию об ошибке в хэш `_fail` в Redis.
- **`decide_what_to_do_next`**: Задача-развилка, которая запускается после успеха или неудачи. Она решает, продолжать ли цикл.
- **`trigger_self_run`**: Задача, которая фактически запускает следующий экземпляр DAG, создавая непрерывный цикл.
## Механизм привязки воркеров к конкретным машинам (Worker Pinning / Affinity)
Для обеспечения того, чтобы все задачи, связанные с обработкой одного конкретного URL, выполнялись на одной и той же машине (воркере), система использует комбинацию из трех компонентов: Оркестратора, Диспетчера и специального хука Airflow.
### 1. `ytdlp_ops_orchestrator` (Оркестратор)
- **Роль:** Инициирует процесс обработки.
- **Действие:** При запуске он создает несколько DAG-запусков `ytdlp_ops_dispatcher`. Каждый такой запуск предназначен для обработки одного URL.
- **Передача параметров:** Оркестратор передает свои параметры конфигурации (например, `account_pool`, `redis_conn_id`, `service_ip`) каждому запуску диспетчера.
### 2. `ytdlp_ops_dispatcher` (Диспетчер)
- **Роль:** Основной механизм обеспечения привязки.
- **Действие:**
1. **Получает URL:** Извлекает один URL из очереди Redis (`_inbox`).
2. **Определяет воркер:** Использует `socket.gethostname()` для определения имени текущей машины (воркера), на которой он выполняется.
3. **Формирует имя очереди:** Создает уникальное имя очереди для этого воркера, например, `queue-dl-dl-worker-1`.
4. **Запускает Worker DAG:** Инициирует запуск DAG `ytdlp_ops_worker_per_url`, передавая ему:
* Извлеченный `url_to_process`.
* Сформированное имя очереди `worker_queue` через параметр `conf`.
* Все остальные параметры, полученные от оркестратора.
- **Ключевой момент:** Именно на этом этапе устанавливается связь между конкретным URL и конкретным воркером, на котором началась обработка этого URL.
### 3. `task_instance_mutation_hook` (Хук изменения задач)
- **Расположение:** `airflow/config/custom_task_hooks.py`
- **Роль:** Является механизмом, который обеспечивает выполнение *всех* задач Worker DAG на нужной машине.
- **Как это работает:**
1. **Регистрация:** Хук регистрируется в конфигурации Airflow и вызывается перед запуском *каждой* задачи.
2. **Проверка DAG ID:** Хук проверяет, принадлежит ли задача (`TaskInstance`) DAG `ytdlp_ops_worker_per_url`.
3. **Извлечение `conf`:** Если да, он безопасно извлекает `conf` из `DagRun`, связанного с этой задачей.
4. **Изменение очереди:**
* Если в `conf` найден ключ `worker_queue` (что будет true для всех запусков, инициированных диспетчером), хук *переопределяет* стандартную очередь задачи на это значение.
* Это означает, что Airflow планировщик поставит эту задачу именно в ту очередь, которая прослушивается нужным воркером.
5. **Резервный вариант:** Если `worker_queue` не найден (например, DAG запущен вручную), задача возвращается в стандартную очередь `queue-dl`.
- **Ключевой момент:** Этот хук гарантирует, что *все последующие задачи* в рамках одного запуска `ytdlp_ops_worker_per_url` (например, `get_token`, `download_and_probe`, `mark_url_as_success`) будут выполнены на том же воркере, который изначально получил URL в диспетчере.
### Резюме
Комбинация `Оркестратор -> Диспетчер -> Хук` эффективно реализует привязку задач к воркерам:
1. **Оркестратор** запускает процесс.
2. **Диспетчер** связывает конкретный URL с конкретным воркером, определяя его имя хоста и передавая его как `worker_queue` в Worker DAG.
3. **Хук** гарантирует, что все задачи Worker DAG выполняются в очереди, соответствующей этому воркеру.
Это позволяет системе использовать локальные ресурсы воркера (например, кэш, временные файлы) эффективно и предсказуемо для обработки каждого отдельного URL.

View File

@ -33,7 +33,7 @@ def load_dotenv(dotenv_path):
"""
if not os.path.exists(dotenv_path):
logging.warning(f".env file not found at {dotenv_path}. Using system environment variables or defaults.")
return
return False
try:
with open(dotenv_path) as f:
for line in f:
@ -43,6 +43,7 @@ def load_dotenv(dotenv_path):
key = key.strip()
value = value.strip()
# Remove surrounding quotes which are common in .env files
# Handle both single and double quotes
if (value.startswith('"') and value.endswith('"')) or \
(value.startswith("'") and value.endswith("'")):
value = value[1:-1]
@ -53,9 +54,10 @@ def load_dotenv(dotenv_path):
if key not in os.environ:
os.environ[key] = value
logging.info(f"Successfully loaded variables from {dotenv_path}")
return True
except Exception as e:
logging.error(f"Failed to read or parse {dotenv_path}: {e}")
# Continue, will use defaults or system env vars
return False
def _get_port_from_proxy_url(url: str) -> Optional[str]:
"""Extracts the port from a proxy URL string."""
@ -94,9 +96,18 @@ def generate_configs():
project_root = os.path.dirname(os.path.abspath(__file__)) # This will be /app
configs_dir = os.path.join(project_root, 'configs')
# Load .env from the 'configs' directory.
dotenv_path = os.path.join(configs_dir, '.env')
load_dotenv(dotenv_path)
# Load .env from the project root ONLY - no fallback
dotenv_path = os.path.join(project_root, '.env')
logging.info(f"Looking for .env file at: {dotenv_path}")
if os.path.exists(dotenv_path):
if load_dotenv(dotenv_path):
logging.info(f"Using .env file from: {dotenv_path}")
else:
logging.error(f"Failed to load .env file from: {dotenv_path}")
exit(1)
else:
logging.warning(f".env file not found at {dotenv_path}. Using system environment variables or defaults.")
# --- Common Configuration ---
ytdlp_workers_str = os.getenv('YTDLP_WORKERS', '3').strip()
@ -117,7 +128,13 @@ def generate_configs():
env.globals['_get_port_from_proxy_url'] = _get_port_from_proxy_url
# Get service role from environment to determine what to generate
service_role = os.getenv('SERVICE_ROLE', 'all-in-one')
# Ensure we strip any remaining quotes that might have slipped through
service_role = os.getenv('service_role', 'management')
# Additional stripping of quotes for robustness
if (service_role.startswith('"') and service_role.endswith('"')) or \
(service_role.startswith("'") and service_role.endswith("'")):
service_role = service_role[1:-1]
logging.info(f"Service role for generation: '{service_role}'")
# --- Camoufox Configuration (only for worker/all-in-one roles) ---

View File

@ -17,7 +17,7 @@ external_access_ips: []
file_permissions: '0644'
host_timezone: Europe/Moscow
management_service_port: 9091
master_host_ip: 89.253.223.97
master_host_ip: 89.253.221.173
postgres_port: 5432
redis_port: 52909
rsync_default_opts:

View File

@ -0,0 +1,22 @@
---
# Variables for af-green
master_host_ip: 89.253.221.173
redis_port: 52909
shadowsocks_proxies:
sslocal-rust-1087:
server: "91.103.252.51"
server_port: 8388
local_port: 1087
vault_password_key: "vault_ss_password_1"
sslocal-rust-1086:
server: "62.60.178.45"
server_port: 8388
local_port: 1086
vault_password_key: "vault_ss_password_2"
sslocal-rust-1081:
server: "79.137.207.43"
server_port: 8388
local_port: 1081
vault_password_key: "vault_ss_password_2"
worker_proxies:
- "socks5://sslocal-rust-1087:1087"

View File

@ -0,0 +1,22 @@
---
# Variables for dl001
master_host_ip: 89.253.221.173
redis_port: 52909
shadowsocks_proxies:
sslocal-rust-1087:
server: "91.103.252.51"
server_port: 8388
local_port: 1087
vault_password_key: "vault_ss_password_1"
sslocal-rust-1086:
server: "62.60.178.45"
server_port: 8388
local_port: 1086
vault_password_key: "vault_ss_password_2"
sslocal-rust-1081:
server: "79.137.207.43"
server_port: 8388
local_port: 1081
vault_password_key: "vault_ss_password_2"
worker_proxies:
- "socks5://sslocal-rust-1087:1087"

View File

@ -0,0 +1,22 @@
---
# Variables for dl003
master_host_ip: 89.253.221.173
redis_port: 52909
shadowsocks_proxies:
sslocal-rust-1087:
server: "91.103.252.51"
server_port: 8388
local_port: 1087
vault_password_key: "vault_ss_password_1"
sslocal-rust-1086:
server: "62.60.178.45"
server_port: 8388
local_port: 1086
vault_password_key: "vault_ss_password_2"
sslocal-rust-1081:
server: "79.137.207.43"
server_port: 8388
local_port: 1081
vault_password_key: "vault_ss_password_2"
worker_proxies:
- "socks5://sslocal-rust-1087:1087"

View File

@ -3,7 +3,8 @@
# Edit cluster.yml and re-run the generator instead.
[airflow_master]
af-test ansible_host=89.253.223.97 ansible_port=22
af-green ansible_host=89.253.221.173 ansible_port=22
[airflow_workers]
dl002 ansible_host=62.60.178.54
dl003 ansible_host=62.60.245.103
dl001 ansible_host=109.107.189.106

View File

@ -47,6 +47,7 @@
- "docker-compose-ytdlp-ops.yaml.j2"
- "docker-compose.config-generate.yaml"
- "envoy.yaml.j2"
- "docker-compose.camoufox.yaml.j2"
- name: Create .env file for YT-DLP master service
template:

View File

@ -1,55 +1,59 @@
# This file is managed by Ansible.
# Set the timezone for all services to ensure consistency in logs.
TZ=Europe/Moscow
# This file is managed by Ansible. Do not edit manually.
# --- Common Settings ---
HOSTNAME="{{ inventory_hostname }}"
SERVICE_ROLE={{ service_role }}
{% if server_identity is defined %}
SERVER_IDENTITY={{ server_identity }}
{% endif %}
COMPOSE_PROJECT_NAME="ytdlp-ops-{{ service_role | default('all-in-one') }}"
TZ="{{ host_timezone }}"
service_role={{ service_role | default('all-in-one') }}
# Passwords
# --- Docker Image Settings ---
YTDLP_OPS_IMAGE="{{ ytdlp_ops_image }}"
AIRFLOW_IMAGE_NAME="{{ airflow_image_name }}"
# --- Network Settings ---
ENVOY_PORT={{ envoy_port }}
ENVOY_ADMIN_PORT={{ envoy_admin_port }}
YTDLP_BASE_PORT={{ ytdlp_base_port }}
YTDLP_WORKERS={{ ytdlp_workers | default(3) }}
MANAGEMENT_SERVICE_PORT={{ management_service_port }}
REDIS_PORT={{ redis_port }}
POSTGRES_PORT={{ postgres_port }}
# --- Security Settings ---
REDIS_PASSWORD="{{ vault_redis_password }}"
POSTGRES_PASSWORD="{{ vault_postgres_password }}"
# Common settings
AIRFLOW_UID={{ airflow_uid | default(1003) }}
AIRFLOW_GID={{ deploy_group_gid | default(1001) }}
YTDLP_BASE_PORT={{ ytdlp_base_port }}
REDIS_PORT={{ redis_port }}
# Master-specific settings
{% if 'master' in service_role or 'management' in service_role %}
AIRFLOW_ADMIN_PASSWORD="{{ vault_airflow_admin_password }}"
FLOWER_PASSWORD="{{ vault_flower_password }}"
AIRFLOW_VAR_MASTER_HOST_IP={{ hostvars[groups['airflow_master'][0]].ansible_host }}
# MASTER_HOST_IP is not needed on the master node itself for ytdlp-ops,
# as it connects to Redis via the internal Docker service name 'redis'.
# It is defined for workers to connect back to the master.
# --- User and Group IDs ---
AIRFLOW_UID={{ airflow_uid | default(1003) }}
AIRFLOW_GID={{ deploy_group_gid | default(1001) }}
# --- Master-specific settings ---
{% if 'master' in service_role or 'management' in service_role %}
MASTER_HOST_IP={{ hostvars[groups['airflow_master'][0]].ansible_host }}
# Camoufox is not used on master, but the config generator expects the variable.
CAMOUFOX_PROXIES=
{% endif %}
# Worker-specific settings
# --- Worker-specific settings ---
{% if 'worker' in service_role %}
AIRFLOW_PROJ_DIR={{ airflow_worker_dir }}
MASTER_HOST_IP={{ hostvars[groups['airflow_master'][0]].ansible_host }}
# --- Envoy & Worker Configuration ---
ENVOY_PORT={{ envoy_port }}
ENVOY_ADMIN_PORT={{ envoy_admin_port }}
MANAGEMENT_SERVICE_PORT={{ management_service_port }}
YTDLP_WORKERS=4
ENVOY_BACKEND_ADDRESS=ytdlp-ops-service
YTDLP_TIMEOUT=600
# --- Camoufox (Browser) Configuration ---
CAMOUFOX_PROXIES="{{ (worker_proxies | default([])) | join(',') }}"
VNC_PASSWORD="{{ vault_vnc_password }}"
CAMOUFOX_BASE_VNC_PORT={{ camoufox_base_vnc_port }}
CAMOUFOX_PORT=12345
CAMOUFOX_PORT={{ camoufox_port }}
# --- Account Manager Configuration ---
ACCOUNT_ACTIVE_DURATION_MIN=7
ACCOUNT_COOLDOWN_DURATION_MIN=30
ACCOUNT_ACTIVE_DURATION_MIN={{ account_active_duration_min | default(7) }}
ACCOUNT_COOLDOWN_DURATION_MIN={{ account_cooldown_duration_min | default(30) }}
{% endif %}

View File

@ -88,7 +88,7 @@ shadowsocks_proxies:
master:
af-green:
ip: 89.253.223.97
ip: 89.253.221.173
port: 22
proxies:
- "socks5://sslocal-rust-1087:1087"