Spaces:
Sleeping
Sleeping
Upload 18 files
Browse files- Dockerfile.txt +57 -0
- central/secrets/.env.example +2 -2
- docker-compose.yml +4 -4
- env.example +1 -1
- public/wiki.html +14 -14
- worker/index.js +162 -0
- worker/settings.json +2 -2
Dockerfile.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-stage build for Translator API
|
| 2 |
+
FROM node:20-alpine AS builder
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Copy package files
|
| 7 |
+
COPY package*.json ./
|
| 8 |
+
COPY central/package*.json ./central/
|
| 9 |
+
COPY worker/package*.json ./worker/
|
| 10 |
+
|
| 11 |
+
# Install all dependencies
|
| 12 |
+
RUN npm ci && \
|
| 13 |
+
cd central && npm ci && \
|
| 14 |
+
cd ../worker && npm ci
|
| 15 |
+
|
| 16 |
+
# Production stage
|
| 17 |
+
FROM node:20-alpine AS production
|
| 18 |
+
|
| 19 |
+
WORKDIR /app
|
| 20 |
+
|
| 21 |
+
# Install dumb-init for proper signal handling
|
| 22 |
+
RUN apk add --no-cache dumb-init
|
| 23 |
+
|
| 24 |
+
# Create non-root user
|
| 25 |
+
RUN addgroup -g 1001 -S nodejs && \
|
| 26 |
+
adduser -S nodejs -u 1001
|
| 27 |
+
|
| 28 |
+
# Copy built dependencies
|
| 29 |
+
COPY --from=builder /app/node_modules ./node_modules
|
| 30 |
+
COPY --from=builder /app/central/node_modules ./central/node_modules
|
| 31 |
+
COPY --from=builder /app/worker/node_modules ./worker/node_modules
|
| 32 |
+
|
| 33 |
+
# Copy source code
|
| 34 |
+
COPY --chown=nodejs:nodejs package.json ./
|
| 35 |
+
COPY --chown=nodejs:nodejs central ./central
|
| 36 |
+
COPY --chown=nodejs:nodejs worker ./worker
|
| 37 |
+
|
| 38 |
+
# Create logs directory
|
| 39 |
+
RUN mkdir -p central/logs worker/logs && chown -R nodejs:nodejs central/logs worker/logs
|
| 40 |
+
|
| 41 |
+
# Switch to non-root user
|
| 42 |
+
USER nodejs
|
| 43 |
+
|
| 44 |
+
# Expose port (HuggingFace Spaces uses 7860)
|
| 45 |
+
EXPOSE 7860
|
| 46 |
+
|
| 47 |
+
# Environment variables
|
| 48 |
+
ENV PORT=7860
|
| 49 |
+
ENV NODE_ENV=production
|
| 50 |
+
|
| 51 |
+
# Health check
|
| 52 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
| 53 |
+
CMD wget --no-verbose --tries=1 --spider http://localhost:7860/api/health || exit 1
|
| 54 |
+
|
| 55 |
+
# Start central server
|
| 56 |
+
ENTRYPOINT ["dumb-init", "--"]
|
| 57 |
+
CMD ["node", "central/src/index.js"]
|
central/secrets/.env.example
CHANGED
|
@@ -8,8 +8,8 @@ ADMIN_ACCESS_CODE=your-secure-admin-code-here
|
|
| 8 |
# Tailscale API key for worker discovery (optional)
|
| 9 |
TAILSCALE_API_KEY=tskey-xxxxxxxxx
|
| 10 |
|
| 11 |
-
# Optional: Custom port (default
|
| 12 |
-
# PORT=
|
| 13 |
|
| 14 |
# Optional: Node environment
|
| 15 |
# NODE_ENV=production
|
|
|
|
| 8 |
# Tailscale API key for worker discovery (optional)
|
| 9 |
TAILSCALE_API_KEY=tskey-xxxxxxxxx
|
| 10 |
|
| 11 |
+
# Optional: Custom port (default 7860)
|
| 12 |
+
# PORT=7860
|
| 13 |
|
| 14 |
# Optional: Node environment
|
| 15 |
# NODE_ENV=production
|
docker-compose.yml
CHANGED
|
@@ -5,10 +5,10 @@ services:
|
|
| 5 |
build: .
|
| 6 |
container_name: translator-central
|
| 7 |
ports:
|
| 8 |
-
- "
|
| 9 |
environment:
|
| 10 |
- NODE_ENV=production
|
| 11 |
-
- PORT=
|
| 12 |
- ADMIN_ACCESS_CODE=${ADMIN_ACCESS_CODE}
|
| 13 |
- TAILSCALE_API_KEY=${TAILSCALE_API_KEY}
|
| 14 |
volumes:
|
|
@@ -23,7 +23,7 @@ services:
|
|
| 23 |
cpus: '1'
|
| 24 |
memory: 8G
|
| 25 |
healthcheck:
|
| 26 |
-
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:
|
| 27 |
interval: 30s
|
| 28 |
timeout: 10s
|
| 29 |
retries: 3
|
|
@@ -36,7 +36,7 @@ services:
|
|
| 36 |
environment:
|
| 37 |
- NODE_ENV=production
|
| 38 |
- CENTRAL_HOST=central
|
| 39 |
-
- CENTRAL_PORT=
|
| 40 |
depends_on:
|
| 41 |
central:
|
| 42 |
condition: service_healthy
|
|
|
|
| 5 |
build: .
|
| 6 |
container_name: translator-central
|
| 7 |
ports:
|
| 8 |
+
- "7860:7860"
|
| 9 |
environment:
|
| 10 |
- NODE_ENV=production
|
| 11 |
+
- PORT=7860
|
| 12 |
- ADMIN_ACCESS_CODE=${ADMIN_ACCESS_CODE}
|
| 13 |
- TAILSCALE_API_KEY=${TAILSCALE_API_KEY}
|
| 14 |
volumes:
|
|
|
|
| 23 |
cpus: '1'
|
| 24 |
memory: 8G
|
| 25 |
healthcheck:
|
| 26 |
+
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7860/api/health"]
|
| 27 |
interval: 30s
|
| 28 |
timeout: 10s
|
| 29 |
retries: 3
|
|
|
|
| 36 |
environment:
|
| 37 |
- NODE_ENV=production
|
| 38 |
- CENTRAL_HOST=central
|
| 39 |
+
- CENTRAL_PORT=7860
|
| 40 |
depends_on:
|
| 41 |
central:
|
| 42 |
condition: service_healthy
|
env.example
CHANGED
|
@@ -9,7 +9,7 @@ ADMIN_ACCESS_CODE=your_strong_admin_code_here
|
|
| 9 |
# Get from: https://login.tailscale.com/admin/settings/keys
|
| 10 |
TAILSCALE_API_KEY=tskey-xxxxxxxxxxxxxxxxx
|
| 11 |
|
| 12 |
-
# Port (default:
|
| 13 |
PORT=7860
|
| 14 |
|
| 15 |
# Node environment
|
|
|
|
| 9 |
# Get from: https://login.tailscale.com/admin/settings/keys
|
| 10 |
TAILSCALE_API_KEY=tskey-xxxxxxxxxxxxxxxxx
|
| 11 |
|
| 12 |
+
# Port (default: 7860)
|
| 13 |
PORT=7860
|
| 14 |
|
| 15 |
# Node environment
|
public/wiki.html
CHANGED
|
@@ -87,10 +87,10 @@
|
|
| 87 |
|
| 88 |
<h3>URL de base</h3>
|
| 89 |
<p>Remplacez <code>VOTRE_HOTE</code> par l'adresse de votre instance :</p>
|
| 90 |
-
<pre><code>https://VOTRE_HOTE:
|
| 91 |
|
| 92 |
<h3>Test rapide</h3>
|
| 93 |
-
<pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:
|
| 94 |
-H "Content-Type: application/json" \
|
| 95 |
-d '{"text": "Bonjour le monde", "source": "fr", "target": "en"}'</code></pre>
|
| 96 |
|
|
@@ -172,7 +172,7 @@
|
|
| 172 |
</div>
|
| 173 |
|
| 174 |
<pre class="code-block active" id="js-fetch"><code class="language-javascript">async function translate(text, source = 'auto', target = 'fr') {
|
| 175 |
-
const response = await fetch('https://VOTRE_HOTE:
|
| 176 |
method: 'POST',
|
| 177 |
headers: { 'Content-Type': 'application/json' },
|
| 178 |
body: JSON.stringify({ text, source, target })
|
|
@@ -195,7 +195,7 @@ translate('Bonjour le monde', 'fr', 'en')
|
|
| 195 |
|
| 196 |
async function translate(text, source = 'auto', target = 'fr') {
|
| 197 |
try {
|
| 198 |
-
const { data } = await axios.post('https://VOTRE_HOTE:
|
| 199 |
text, source, target
|
| 200 |
});
|
| 201 |
return data;
|
|
@@ -216,7 +216,7 @@ function translate(text, source = 'auto', target = 'fr') {
|
|
| 216 |
|
| 217 |
const options = {
|
| 218 |
hostname: 'VOTRE_HOTE',
|
| 219 |
-
port:
|
| 220 |
path: '/translate',
|
| 221 |
method: 'POST',
|
| 222 |
headers: {
|
|
@@ -247,7 +247,7 @@ function translate(text, source = 'auto', target = 'fr') {
|
|
| 247 |
|
| 248 |
<h3>Classe réutilisable (ES6)</h3>
|
| 249 |
<pre><code class="language-javascript">class TranslatorAPI {
|
| 250 |
-
constructor(baseUrl = 'https://VOTRE_HOTE:
|
| 251 |
this.baseUrl = baseUrl;
|
| 252 |
}
|
| 253 |
|
|
@@ -298,7 +298,7 @@ console.log(result.translatedText); // "Hola"</code></pre>
|
|
| 298 |
|
| 299 |
<pre class="code-block active" id="py-requests"><code class="language-python">import requests
|
| 300 |
|
| 301 |
-
def translate(text, source='auto', target='fr', base_url='https://VOTRE_HOTE:
|
| 302 |
\"\"\"Traduit un texte via l'API Translator.\"\"\"
|
| 303 |
response = requests.post(
|
| 304 |
f'{base_url}/translate',
|
|
@@ -326,7 +326,7 @@ except requests.HTTPError as e:
|
|
| 326 |
import asyncio
|
| 327 |
|
| 328 |
class TranslatorClient:
|
| 329 |
-
def __init__(self, base_url='https://VOTRE_HOTE:
|
| 330 |
self.base_url = base_url
|
| 331 |
self.client = httpx.AsyncClient(timeout=30.0)
|
| 332 |
|
|
@@ -372,7 +372,7 @@ asyncio.run(main())</code></pre>
|
|
| 372 |
<pre class="code-block" id="py-stdlib"><code class="language-python">import urllib.request
|
| 373 |
import json
|
| 374 |
|
| 375 |
-
def translate(text, source='auto', target='fr', base_url='https://VOTRE_HOTE:
|
| 376 |
data = json.dumps({'text': text, 'source': source, 'target': target}).encode('utf-8')
|
| 377 |
|
| 378 |
req = urllib.request.Request(
|
|
@@ -459,7 +459,7 @@ public class TranslatorClient {
|
|
| 459 |
|
| 460 |
// Usage
|
| 461 |
public static void main(String[] args) throws Exception {
|
| 462 |
-
TranslatorClient translator = new TranslatorClient("https://VOTRE_HOTE:
|
| 463 |
TranslationResult result = translator.translate("Bonjour", "fr", "en");
|
| 464 |
System.out.println(result.translatedText); // "Hello"
|
| 465 |
}
|
|
@@ -588,12 +588,12 @@ public class TranslatorClient {
|
|
| 588 |
<h2>🌐 cURL / Ligne de commande</h2>
|
| 589 |
|
| 590 |
<h3>Traduction simple</h3>
|
| 591 |
-
<pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:
|
| 592 |
-H "Content-Type: application/json" \
|
| 593 |
-d '{"text": "Bonjour le monde", "source": "fr", "target": "en"}'</code></pre>
|
| 594 |
|
| 595 |
<h3>Avec détection automatique</h3>
|
| 596 |
-
<pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:
|
| 597 |
-H "Content-Type: application/json" \
|
| 598 |
-d '{"text": "Hello world", "target": "fr"}'</code></pre>
|
| 599 |
|
|
@@ -601,7 +601,7 @@ public class TranslatorClient {
|
|
| 601 |
<pre><code class="language-bash">#!/bin/bash
|
| 602 |
# translate.sh - Script de traduction en ligne de commande
|
| 603 |
|
| 604 |
-
BASE_URL="https://VOTRE_HOTE:
|
| 605 |
SOURCE="auto"
|
| 606 |
TARGET="fr"
|
| 607 |
|
|
@@ -639,7 +639,7 @@ echo "$translated"</code></pre>
|
|
| 639 |
<h3>Test de charge simple</h3>
|
| 640 |
<pre><code class="language-bash"># Tester le rate limit (30 req/min)
|
| 641 |
for i in {1..35}; do
|
| 642 |
-
curl -s -X POST https://VOTRE_HOTE:
|
| 643 |
-H "Content-Type: application/json" \
|
| 644 |
-d "{\"text\": \"Test $i\", \"target\": \"en\"}" \
|
| 645 |
-w " HTTP %{http_code}\\n" \
|
|
|
|
| 87 |
|
| 88 |
<h3>URL de base</h3>
|
| 89 |
<p>Remplacez <code>VOTRE_HOTE</code> par l'adresse de votre instance :</p>
|
| 90 |
+
<pre><code>https://VOTRE_HOTE:7860</code></pre>
|
| 91 |
|
| 92 |
<h3>Test rapide</h3>
|
| 93 |
+
<pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7860/translate \
|
| 94 |
-H "Content-Type: application/json" \
|
| 95 |
-d '{"text": "Bonjour le monde", "source": "fr", "target": "en"}'</code></pre>
|
| 96 |
|
|
|
|
| 172 |
</div>
|
| 173 |
|
| 174 |
<pre class="code-block active" id="js-fetch"><code class="language-javascript">async function translate(text, source = 'auto', target = 'fr') {
|
| 175 |
+
const response = await fetch('https://VOTRE_HOTE:7860/translate', {
|
| 176 |
method: 'POST',
|
| 177 |
headers: { 'Content-Type': 'application/json' },
|
| 178 |
body: JSON.stringify({ text, source, target })
|
|
|
|
| 195 |
|
| 196 |
async function translate(text, source = 'auto', target = 'fr') {
|
| 197 |
try {
|
| 198 |
+
const { data } = await axios.post('https://VOTRE_HOTE:7860/translate', {
|
| 199 |
text, source, target
|
| 200 |
});
|
| 201 |
return data;
|
|
|
|
| 216 |
|
| 217 |
const options = {
|
| 218 |
hostname: 'VOTRE_HOTE',
|
| 219 |
+
port: 7860,
|
| 220 |
path: '/translate',
|
| 221 |
method: 'POST',
|
| 222 |
headers: {
|
|
|
|
| 247 |
|
| 248 |
<h3>Classe réutilisable (ES6)</h3>
|
| 249 |
<pre><code class="language-javascript">class TranslatorAPI {
|
| 250 |
+
constructor(baseUrl = 'https://VOTRE_HOTE:7860') {
|
| 251 |
this.baseUrl = baseUrl;
|
| 252 |
}
|
| 253 |
|
|
|
|
| 298 |
|
| 299 |
<pre class="code-block active" id="py-requests"><code class="language-python">import requests
|
| 300 |
|
| 301 |
+
def translate(text, source='auto', target='fr', base_url='https://VOTRE_HOTE:7860'):
|
| 302 |
\"\"\"Traduit un texte via l'API Translator.\"\"\"
|
| 303 |
response = requests.post(
|
| 304 |
f'{base_url}/translate',
|
|
|
|
| 326 |
import asyncio
|
| 327 |
|
| 328 |
class TranslatorClient:
|
| 329 |
+
def __init__(self, base_url='https://VOTRE_HOTE:7860'):
|
| 330 |
self.base_url = base_url
|
| 331 |
self.client = httpx.AsyncClient(timeout=30.0)
|
| 332 |
|
|
|
|
| 372 |
<pre class="code-block" id="py-stdlib"><code class="language-python">import urllib.request
|
| 373 |
import json
|
| 374 |
|
| 375 |
+
def translate(text, source='auto', target='fr', base_url='https://VOTRE_HOTE:7860'):
|
| 376 |
data = json.dumps({'text': text, 'source': source, 'target': target}).encode('utf-8')
|
| 377 |
|
| 378 |
req = urllib.request.Request(
|
|
|
|
| 459 |
|
| 460 |
// Usage
|
| 461 |
public static void main(String[] args) throws Exception {
|
| 462 |
+
TranslatorClient translator = new TranslatorClient("https://VOTRE_HOTE:7860");
|
| 463 |
TranslationResult result = translator.translate("Bonjour", "fr", "en");
|
| 464 |
System.out.println(result.translatedText); // "Hello"
|
| 465 |
}
|
|
|
|
| 588 |
<h2>🌐 cURL / Ligne de commande</h2>
|
| 589 |
|
| 590 |
<h3>Traduction simple</h3>
|
| 591 |
+
<pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7860/translate \
|
| 592 |
-H "Content-Type: application/json" \
|
| 593 |
-d '{"text": "Bonjour le monde", "source": "fr", "target": "en"}'</code></pre>
|
| 594 |
|
| 595 |
<h3>Avec détection automatique</h3>
|
| 596 |
+
<pre><code class="language-bash">curl -X POST https://VOTRE_HOTE:7860/translate \
|
| 597 |
-H "Content-Type: application/json" \
|
| 598 |
-d '{"text": "Hello world", "target": "fr"}'</code></pre>
|
| 599 |
|
|
|
|
| 601 |
<pre><code class="language-bash">#!/bin/bash
|
| 602 |
# translate.sh - Script de traduction en ligne de commande
|
| 603 |
|
| 604 |
+
BASE_URL="https://VOTRE_HOTE:7860"
|
| 605 |
SOURCE="auto"
|
| 606 |
TARGET="fr"
|
| 607 |
|
|
|
|
| 639 |
<h3>Test de charge simple</h3>
|
| 640 |
<pre><code class="language-bash"># Tester le rate limit (30 req/min)
|
| 641 |
for i in {1..35}; do
|
| 642 |
+
curl -s -X POST https://VOTRE_HOTE:7860/translate \
|
| 643 |
-H "Content-Type: application/json" \
|
| 644 |
-d "{\"text\": \"Test $i\", \"target\": \"en\"}" \
|
| 645 |
-w " HTTP %{http_code}\\n" \
|
worker/index.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require('dotenv').config();
|
| 2 |
+
const WebSocket = require('ws');
|
| 3 |
+
const translate = require('@vitalets/google-translate-api');
|
| 4 |
+
const fs = require('fs');
|
| 5 |
+
const path = require('path');
|
| 6 |
+
const os = require('os');
|
| 7 |
+
const winston = require('winston');
|
| 8 |
+
|
| 9 |
+
const settings = JSON.parse(fs.readFileSync(path.join(__dirname, '../settings.json'), 'utf8'));
|
| 10 |
+
|
| 11 |
+
// Logger
|
| 12 |
+
const logger = winston.createLogger({
|
| 13 |
+
level: 'info',
|
| 14 |
+
format: winston.format.combine(
|
| 15 |
+
winston.format.timestamp(),
|
| 16 |
+
winston.format.json()
|
| 17 |
+
),
|
| 18 |
+
transports: [
|
| 19 |
+
new winston.transports.Console(),
|
| 20 |
+
new winston.transports.File({ filename: path.join(__dirname, '../../logs/worker.log') })
|
| 21 |
+
]
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
// Worker state
|
| 25 |
+
let workerId = null;
|
| 26 |
+
let ws = null;
|
| 27 |
+
let maxConcurrentJobs = settings.maxConcurrentJobs;
|
| 28 |
+
let activeJobs = 0;
|
| 29 |
+
let reconnectAttempts = 0;
|
| 30 |
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
| 31 |
+
const RECONNECT_DELAY = 5000;
|
| 32 |
+
|
| 33 |
+
// Connect to central server
|
| 34 |
+
function connect() {
|
| 35 |
+
const url = `ws://${settings.centralHost}:${settings.centralPort}`;
|
| 36 |
+
logger.info(`Connecting to central server at ${url}`);
|
| 37 |
+
|
| 38 |
+
ws = new WebSocket(url);
|
| 39 |
+
|
| 40 |
+
ws.on('open', () => {
|
| 41 |
+
logger.info('Connected to central server');
|
| 42 |
+
reconnectAttempts = 0;
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
ws.on('message', (data) => {
|
| 46 |
+
try {
|
| 47 |
+
const msg = JSON.parse(data);
|
| 48 |
+
handleMessage(msg);
|
| 49 |
+
} catch (e) {
|
| 50 |
+
logger.error('Invalid message from central', e);
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
ws.on('close', () => {
|
| 55 |
+
logger.warn('Disconnected from central server');
|
| 56 |
+
scheduleReconnect();
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
ws.on('error', (err) => {
|
| 60 |
+
logger.error('WebSocket error', err);
|
| 61 |
+
});
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
function scheduleReconnect() {
|
| 65 |
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
| 66 |
+
logger.error('Max reconnect attempts reached, exiting');
|
| 67 |
+
process.exit(1);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
reconnectAttempts++;
|
| 71 |
+
const delay = RECONNECT_DELAY * reconnectAttempts;
|
| 72 |
+
logger.info(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
| 73 |
+
|
| 74 |
+
setTimeout(connect, delay);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
function handleMessage(msg) {
|
| 78 |
+
switch (msg.type) {
|
| 79 |
+
case 'welcome':
|
| 80 |
+
workerId = msg.workerId;
|
| 81 |
+
maxConcurrentJobs = msg.maxConcurrentJobs || maxConcurrentJobs;
|
| 82 |
+
logger.info(`Worker registered with ID: ${workerId}, max jobs: ${maxConcurrentJobs}`);
|
| 83 |
+
startHeartbeat();
|
| 84 |
+
break;
|
| 85 |
+
|
| 86 |
+
case 'translate':
|
| 87 |
+
handleTranslateJob(msg);
|
| 88 |
+
break;
|
| 89 |
+
|
| 90 |
+
case 'ping':
|
| 91 |
+
// Keep alive
|
| 92 |
+
ws.send(JSON.stringify({ type: 'pong' }));
|
| 93 |
+
break;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
async function handleTranslateJob(msg) {
|
| 98 |
+
const { jobId, text, source, target } = msg;
|
| 99 |
+
|
| 100 |
+
if (activeJobs >= maxConcurrentJobs) {
|
| 101 |
+
logger.warn(`Job ${jobId} rejected: at capacity (${activeJobs}/${maxConcurrentJobs})`);
|
| 102 |
+
sendResult(jobId, { error: 'Worker at capacity' });
|
| 103 |
+
return;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
activeJobs++;
|
| 107 |
+
logger.info(`Starting job ${jobId} (${activeJobs}/${maxConcurrentJobs})`);
|
| 108 |
+
|
| 109 |
+
try {
|
| 110 |
+
const result = await translate(text, { from: source, to: target });
|
| 111 |
+
sendResult(jobId, { translatedText: result.text });
|
| 112 |
+
} catch (error) {
|
| 113 |
+
logger.error(`Job ${jobId} failed`, error);
|
| 114 |
+
sendResult(jobId, { error: error.message });
|
| 115 |
+
} finally {
|
| 116 |
+
activeJobs--;
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
function sendResult(jobId, result) {
|
| 121 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 122 |
+
ws.send(JSON.stringify({
|
| 123 |
+
type: 'result',
|
| 124 |
+
jobId,
|
| 125 |
+
result
|
| 126 |
+
}));
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// Heartbeat with metrics
|
| 131 |
+
function startHeartbeat() {
|
| 132 |
+
setInterval(() => {
|
| 133 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 134 |
+
const cpuUsage = process.cpuUsage();
|
| 135 |
+
const memUsage = process.memoryUsage();
|
| 136 |
+
|
| 137 |
+
ws.send(JSON.stringify({
|
| 138 |
+
type: 'heartbeat',
|
| 139 |
+
cpu: (cpuUsage.user + cpuUsage.system) / 10000, // Convert to percentage-ish
|
| 140 |
+
ram: memUsage.heapUsed,
|
| 141 |
+
jobsActive: activeJobs
|
| 142 |
+
}));
|
| 143 |
+
}
|
| 144 |
+
}, 5000);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Handle graceful shutdown
|
| 148 |
+
process.on('SIGTERM', () => {
|
| 149 |
+
logger.info('SIGTERM received, shutting down gracefully');
|
| 150 |
+
if (ws) ws.close();
|
| 151 |
+
process.exit(0);
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
process.on('SIGINT', () => {
|
| 155 |
+
logger.info('SIGINT received, shutting down gracefully');
|
| 156 |
+
if (ws) ws.close();
|
| 157 |
+
process.exit(0);
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
// Start
|
| 161 |
+
logger.info('Starting Translator Worker');
|
| 162 |
+
connect();
|
worker/settings.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
"maxConcurrentJobs": 2,
|
| 3 |
"centralHost": "localhost",
|
| 4 |
-
"centralPort":
|
| 5 |
-
}
|
|
|
|
| 1 |
{
|
| 2 |
"maxConcurrentJobs": 2,
|
| 3 |
"centralHost": "localhost",
|
| 4 |
+
"centralPort": 7860
|
| 5 |
+
}
|