Beethoven es un servidor central (orquestador) que reparte trabajo entre nodos conectados por WebSocket. La idea es parecida a un pool de minería: quien más aporta (aquí: dispositivos activos y tiempo en línea) acumula más créditos y tiende a recibir más asignaciones; cada asignación consume crédito y corresponde a una tarea (por ejemplo, una URL a ejecutar en el nodo).
Los nodos no deciden a quién se envía cada tarea: solo se conectan, informan estado, envían URLs al pool global y ejecutan lo que el servidor les manda.
| Término | Significado |
|---|---|
| Nodo | Cliente WebSocket que ejecuta tareas y puede enviar URLs al pool. |
| Pool | Cola global de tareas (URL + dueño + prioridad + reintentos). |
| Crédito | Unidad de saldo del nodo; el servidor la descuenta al enviar un execute según device_spread. |
device_spread |
one_device (defecto): 1 crédito por execute. all_devices: N créditos con N = ranuras idle del ejecutor (último heartbeat). Misma URL en todas las idle. |
device_slots |
Obligatorio en cada heartbeat: una entrada por ranura (idle / executing / unavailable). Capacidad diaria = idle + executing; active_devices debe ser len(device_slots). |
kind / track_count |
En tareas con una sola url: kind puede ser track o playlist. Si es playlist, track_count (nº de pistas) es obligatorio. Beethoven lo reenvía en execute para que el nodo estime carga; no multiplica los créditos del servidor por ese número. |
limit_count / status |
Opcionales en cada ítem de tarea. limit_count: tope de streams para esa tarea. status: active | paused; en paused el scheduler no asigna hasta actualizar (p. ej. sync_tasks). Van en el execute hacia el nodo (limit_count puede ser null). |
execute + task_progress |
El execute incluye client_id y owner_machine_id. El ejecutor puede enviar task_progress (streams_total); Beethoven reenvía al nodo owner para actualizar conteos sin cerrar la tarea. |
| Owner | Nodo que envió esa URL al pool (auditoría); puede ejecutarla otro o el mismo nodo (auto-tráfico permitido). |
| Scheduler | Bucle que acumula créditos, elige nodo y tarea, y envía execute. |
| Dead letter | Tarea descartada tras superar el máximo de reintentos fallidos o timeouts. |
- El nodo se registra y manda heartbeats periódicos con
device_slots(obligatorio) yactive_devices= número de ranuras reportadas. - Cualquier nodo puede meter URLs en el pool (
submit_tasks). - El servidor suma créditos a los nodos en línea (con techo por día y por dispositivo; no arrastran al día siguiente).
- Cuando hay créditos y hay tareas, el servidor elige un par (nodo, tarea) de forma que el nodo tenga saldo suficiente para el coste de esa tarea (por prioridad entre las asequibles; puede ser trabajo que el propio nodo envió). Cada ítem puede llevar
device_spread(ver abajo). Un playlist de 110 URLs se encola como 110 tareas; el coste en créditos depende del modo por fila. - El nodo ejecuta y responde
result(ok/fail). Los fallos y los timeouts reencolan la tarea hasta un máximo; luego va a dead letter. - Si un nodo deja de estar online, las tareas que él había enviado pasan a
ownerlibre para que otros las ejecuten (si no está online, no “merece” que le repartan trabajo).
Para mensajes JSON exactos y diagramas, ver también NODE_INTEGRATION.md.
| Puerto | Uso |
|---|---|
PORT (default 8081) |
WebSocket de nodos. |
HTTP_ADMIN_PORT (default 9090) |
Solo operación: |
| Ruta | Qué hace |
|---|---|
GET /health |
El proceso responde (útil para “¿vive?”). |
GET /ready |
Listo para tráfico; si REQUIRE_REDIS_READY=true y Redis no está bien, devuelve error. |
GET /metrics |
Métricas Prometheus (cola, nodos online, contadores, etc.). |
| Variable | Default | Para qué sirve |
|---|---|---|
PORT |
8081 |
Puerto WebSocket. |
HTTP_ADMIN_PORT |
9090 |
Puerto HTTP admin. |
INSTANCE_ID |
host + PID | Identidad de esta instancia (Redis / lock). |
| Variable | Default | Para qué sirve |
|---|---|---|
CREDITS_PER_DEVICE_PER_DAY |
1440 |
Techo de créditos por dispositivo y día UTC (creditDayKey). |
OFFLINE_AFTER_SEC |
15 |
Sin heartbeat → nodo offline. |
TASK_TIMEOUT_SEC |
60 |
Sin result → se reencola la tarea. |
SCHEDULER_TICK_MS |
100 |
Frecuencia del bucle del scheduler. |
Dinámica de créditos y dispositivos
- Tasa fija mientras estás en línea:
(cap_devices × CREDITS_PER_DEVICE_PER_DAY) / 86400créditos por segundo, dondecap_deviceses siempreidle + executingdel último heartbeat (sin snapshot previo → 0 hasta el primer heartbeat). El saldo nunca supera el tope “por reloj” del día UTC:cap × (segundos desde medianoche UTC / 86400). - El techo acumulable del día sigue a
cap_devicesen cada heartbeat (ranurasunavailableno suman al techo). - Si ya tenías más saldo que el nuevo techo (porque bajaron dispositivos), el saldo se ajusta al techo; lo que ya gastaste en
executeno vuelve (sigue descontado). - Si suben dispositivos, el techo sube y puedes seguir acumulando hasta ese máximo con el saldo que te quedaba.
- Reconectar el mismo día con el mismo
machine_id: otroregisterconserva créditos del día,creditDayKeyy reputación (no arranca de cero). El techo se alinea otra vez con el próximo heartbeat. - Con
device_slotsvacío (active_devices: 0) no se acumula; el saldo no se anula por el techo: puedes gastar lo que ya tenías hasta volver a tener ranuras con capacidad.
Techo cumplido y reparto de tareas
- Cuando un nodo llega al techo de acumulación del día (
cap_devices ×créditos/día), se marca una vez ese día UTC y dejan de asignarse al pool las tareas cuyoowneres ese nodo (sus URLs quedan en cola hasta el día siguiente). - Ese mismo nodo sigue pudiendo recibir
executede tareas de otros (o conownernull), si sigue online, con créditos ≥ 1 y al menos una ranuraidleen el último heartbeat. - Objetivo: seguir trabajando aunque ya no se “promocione” tu contenido propio ese día. Si en cola solo quedan tareas de nodos ya en techo, puede no haber nada asignable hasta que entre trabajo de otros o cambie el día.
| Variable | Default | Para qué sirve |
|---|---|---|
MAX_POOL_TASKS |
100000 |
Tope de tareas en cola. |
MAX_WS_MESSAGE_BYTES |
1048576 |
Tamaño máximo de un mensaje WS. |
SUBMIT_RATE_WINDOW_MS |
1000 |
Ventana para rate limit de submit_tasks. |
SUBMIT_RATE_MAX |
200 |
Máx. submit_tasks aceptados por nodo en esa ventana. |
TASK_MAX_ATTEMPTS |
5 |
Reintentos antes de dead letter. |
MAX_PLAYLIST_ITEMS |
2000 |
Máx. URLs por ítem playlist / album en un submit_tasks. |
MAX_DEVICE_SLOTS_HEARTBEAT |
512 |
Máx. entradas en heartbeat.device_slots. |
MAX_TASK_TRACK_COUNT |
50000 |
Máx. track_count en tareas kind: "playlist" (URL única). |
MAX_TASK_LIMIT_COUNT |
50000000 |
Máx. limit_count por tarea. |
| Variable | Default | Para qué sirve |
|---|---|---|
ALLOW_HTTP_URLS |
desactivado | Pon true para permitir http://. |
BLOCK_PRIVATE_HOSTS |
activado | Pon false para no bloquear localhost / IPs privadas (solo si confías en quien envía URLs). |
| Variable | Efecto |
|---|---|
| (ninguna) | Sin auth (none). |
BEETHOVEN_SHARED_SECRET |
Todos los nodos usan el mismo auth_token en register. |
BEETHOVEN_NODE_TOKENS |
JSON {"machine_id":"token",...}: token distinto por nodo. |
Si defines secret compartido y mapa JSON, gana el modo compartido (prioridad en el código actual).
| Variable | Para qué sirve |
|---|---|
REDIS_URL |
Persistencia de snapshot + lock del scheduler entre réplicas. |
PERSISTENCE_DEBOUNCE_MS |
Retraso antes de guardar en Redis (menos escrituras). |
SCHEDULER_LOCK_TTL_SEC |
TTL del candado del scheduler (segundos). |
REQUIRE_REDIS_READY |
true → /ready falla si Redis no está usable. |
Sin Redis, todo vive en memoria: un reinicio borra cola y estado (salvo lo que los nodos reenvíen).
npm install
npm startCon Redis:
set REDIS_URL=redis://127.0.0.1:6379
npm start(En Linux/macOS usa export en lugar de set.)
Pruebas:
npm test¿Puedo tener varias copias del servidor?
Solo con cuidado: varias instancias compitiendo sin Redis duplican trabajo. Con Redis, el lock del scheduler hace que una sola instancia reparta en cada momento (ajusta INSTANCE_ID por proceso).
¿Los créditos se acumulan de un día para otro?
No. Cada día UTC se reinicia el saldo de créditos del día; hay techo diario por dispositivo.
¿Los créditos pueden ser negativos?
No; el servidor evita bajar de cero.
¿Dónde está el contrato de mensajes?
En NODE_INTEGRATION.md (campos JSON, errores y diagramas Mermaid).
Implementación: Node.js · Contrato actual: protocol_version === 1 · Constante en código: src/protocol/constants.js.