Websocket fix 1
This commit is contained in:
@@ -13,7 +13,7 @@ TEST_PASSWORD="TestPassword123!"
|
||||
TEST_NAME="Test Bakery Owner"
|
||||
REAL_CSV_FILE="bakery_sales_2023_2024.csv"
|
||||
WS_BASE="ws://localhost:8002/api/v1/ws"
|
||||
WS_TEST_DURATION=200 # seconds to listen for WebSocket messages
|
||||
WS_TEST_DURATION=2000 # seconds to listen for WebSocket messages
|
||||
WS_PID=""
|
||||
|
||||
|
||||
@@ -158,20 +158,429 @@ check_timezone_error() {
|
||||
return 1 # No timezone error
|
||||
}
|
||||
|
||||
# Function to test WebSocket connection using websocat (if available) or Node.js
|
||||
test_websocket_with_nodejs_builtin() {
|
||||
local tenant_id="$1"
|
||||
local job_id="$2"
|
||||
local max_duration="$3" # Maximum time to wait (fallback)
|
||||
|
||||
echo "Using Node.js with built-in modules for WebSocket testing..."
|
||||
echo "Will monitor until job completion or ${max_duration}s timeout"
|
||||
|
||||
# Create ENHANCED Node.js WebSocket test script
|
||||
local ws_test_script="/tmp/websocket_test_$job_id.js"
|
||||
cat > "$ws_test_script" << 'EOF'
|
||||
// ENHANCED WebSocket test - waits for job completion
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const tenantId = process.argv[2];
|
||||
const jobId = process.argv[3];
|
||||
const maxDuration = parseInt(process.argv[4]) * 1000; // Convert to milliseconds
|
||||
const accessToken = process.argv[5];
|
||||
const wsUrl = process.argv[6];
|
||||
|
||||
console.log(`🚀 Starting enhanced WebSocket monitoring`);
|
||||
console.log(`Connecting to: ${wsUrl}`);
|
||||
console.log(`Will wait for job completion (max ${maxDuration/1000}s)`);
|
||||
|
||||
// Parse WebSocket URL
|
||||
const url = new URL(wsUrl);
|
||||
const isSecure = url.protocol === 'wss:';
|
||||
const port = url.port || (isSecure ? 443 : 80);
|
||||
|
||||
// Create WebSocket key
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
|
||||
// WebSocket handshake headers
|
||||
const headers = {
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'Upgrade',
|
||||
'Sec-WebSocket-Key': key,
|
||||
'Sec-WebSocket-Version': '13',
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
};
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: port,
|
||||
path: url.pathname,
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
};
|
||||
|
||||
console.log(`Attempting WebSocket handshake to ${url.hostname}:${port}${url.pathname}`);
|
||||
|
||||
const client = isSecure ? https : http;
|
||||
let messageCount = 0;
|
||||
let jobCompleted = false;
|
||||
let lastProgressUpdate = Date.now();
|
||||
let highestProgress = 0;
|
||||
|
||||
// Enhanced job tracking
|
||||
const jobStats = {
|
||||
startTime: Date.now(),
|
||||
progressUpdates: 0,
|
||||
stepsCompleted: [],
|
||||
productsProcessed: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
const req = client.request(options);
|
||||
|
||||
req.on('upgrade', (res, socket, head) => {
|
||||
console.log('✅ WebSocket handshake successful');
|
||||
console.log('📡 Monitoring training progress...\n');
|
||||
|
||||
let buffer = Buffer.alloc(0);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
buffer = Buffer.concat([buffer, data]);
|
||||
|
||||
// WebSocket frame parsing
|
||||
while (buffer.length >= 2) {
|
||||
const firstByte = buffer[0];
|
||||
const secondByte = buffer[1];
|
||||
|
||||
const fin = (firstByte & 0x80) === 0x80;
|
||||
const opcode = firstByte & 0x0F;
|
||||
const masked = (secondByte & 0x80) === 0x80;
|
||||
let payloadLength = secondByte & 0x7F;
|
||||
|
||||
let offset = 2;
|
||||
|
||||
// Handle extended payload length
|
||||
if (payloadLength === 126) {
|
||||
if (buffer.length < offset + 2) break;
|
||||
payloadLength = buffer.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
} else if (payloadLength === 127) {
|
||||
if (buffer.length < offset + 8) break;
|
||||
const high = buffer.readUInt32BE(offset);
|
||||
const low = buffer.readUInt32BE(offset + 4);
|
||||
if (high !== 0) {
|
||||
console.log('⚠️ Large payload detected, skipping...');
|
||||
buffer = buffer.slice(offset + 8);
|
||||
continue;
|
||||
}
|
||||
payloadLength = low;
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
// Check if we have the complete frame
|
||||
if (buffer.length < offset + payloadLength) {
|
||||
break; // Wait for more data
|
||||
}
|
||||
|
||||
// Extract payload
|
||||
const payload = buffer.slice(offset, offset + payloadLength);
|
||||
buffer = buffer.slice(offset + payloadLength);
|
||||
|
||||
// Handle different frame types
|
||||
if (opcode === 1 && fin) { // Text frame
|
||||
messageCount++;
|
||||
lastProgressUpdate = Date.now();
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
try {
|
||||
const messageText = payload.toString('utf8');
|
||||
const message = JSON.parse(messageText);
|
||||
|
||||
// Enhanced message processing
|
||||
processTrainingMessage(message, timestamp);
|
||||
|
||||
} catch (e) {
|
||||
const rawText = payload.toString('utf8');
|
||||
console.log(`[${timestamp}] ⚠️ Raw message: ${rawText.substring(0, 200)}${rawText.length > 200 ? '...' : ''}`);
|
||||
}
|
||||
|
||||
} else if (opcode === 8) { // Close frame
|
||||
console.log('🔌 WebSocket closed by server');
|
||||
socket.end();
|
||||
return;
|
||||
|
||||
} else if (opcode === 9) { // Ping frame
|
||||
// Send pong response
|
||||
const pongFrame = Buffer.concat([
|
||||
Buffer.from([0x8A, payload.length]),
|
||||
payload
|
||||
]);
|
||||
socket.write(pongFrame);
|
||||
|
||||
} else if (opcode === 10) { // Pong frame
|
||||
// Ignore pong responses
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Enhanced message processing function
|
||||
function processTrainingMessage(message, timestamp) {
|
||||
const messageType = message.type || 'unknown';
|
||||
const data = message.data || {};
|
||||
|
||||
console.log(`[${timestamp}] 📨 Message ${messageCount}: ${messageType.toUpperCase()}`);
|
||||
|
||||
// Track job statistics
|
||||
if (messageType === 'progress') {
|
||||
jobStats.progressUpdates++;
|
||||
const progress = data.progress || 0;
|
||||
const step = data.current_step || 'Unknown step';
|
||||
const product = data.current_product;
|
||||
|
||||
// Update highest progress
|
||||
if (progress > highestProgress) {
|
||||
highestProgress = progress;
|
||||
}
|
||||
|
||||
// Track steps
|
||||
if (step && !jobStats.stepsCompleted.includes(step)) {
|
||||
jobStats.stepsCompleted.push(step);
|
||||
}
|
||||
|
||||
// Track products
|
||||
if (product && !jobStats.productsProcessed.includes(product)) {
|
||||
jobStats.productsProcessed.push(product);
|
||||
}
|
||||
|
||||
// Display progress with enhanced formatting
|
||||
console.log(` 📊 Progress: ${progress}% (${step})`);
|
||||
if (product) {
|
||||
console.log(` 🍞 Product: ${product}`);
|
||||
}
|
||||
if (data.products_completed && data.products_total) {
|
||||
console.log(` 📦 Products: ${data.products_completed}/${data.products_total} completed`);
|
||||
}
|
||||
if (data.estimated_time_remaining_minutes) {
|
||||
console.log(` ⏱️ ETA: ${data.estimated_time_remaining_minutes} minutes`);
|
||||
}
|
||||
|
||||
} else if (messageType === 'completed') {
|
||||
jobCompleted = true;
|
||||
const duration = Math.round((Date.now() - jobStats.startTime) / 1000);
|
||||
|
||||
console.log(`\n🎉 TRAINING COMPLETED SUCCESSFULLY!`);
|
||||
console.log(` ⏱️ Total Duration: ${duration}s`);
|
||||
|
||||
if (data.results) {
|
||||
const results = data.results;
|
||||
if (results.successful_trainings !== undefined) {
|
||||
console.log(` ✅ Models Trained: ${results.successful_trainings}`);
|
||||
}
|
||||
if (results.total_products !== undefined) {
|
||||
console.log(` 📦 Total Products: ${results.total_products}`);
|
||||
}
|
||||
if (results.success_rate !== undefined) {
|
||||
console.log(` 📈 Success Rate: ${results.success_rate}%`);
|
||||
}
|
||||
}
|
||||
|
||||
// Close connection after completion
|
||||
setTimeout(() => {
|
||||
console.log('\n📊 Training job completed - closing WebSocket connection');
|
||||
socket.end();
|
||||
}, 2000); // Wait 2 seconds to ensure all final messages are received
|
||||
|
||||
} else if (messageType === 'failed') {
|
||||
jobCompleted = true;
|
||||
jobStats.errors.push(data);
|
||||
|
||||
console.log(`\n❌ TRAINING FAILED!`);
|
||||
if (data.error) {
|
||||
console.log(` 💥 Error: ${data.error}`);
|
||||
}
|
||||
if (data.error_details) {
|
||||
console.log(` 📝 Details: ${JSON.stringify(data.error_details, null, 2)}`);
|
||||
}
|
||||
|
||||
// Close connection after failure
|
||||
setTimeout(() => {
|
||||
console.log('\n📊 Training job failed - closing WebSocket connection');
|
||||
socket.end();
|
||||
}, 2000);
|
||||
|
||||
} else if (messageType === 'step_completed') {
|
||||
console.log(` ✅ Step completed: ${data.step_name || 'Unknown'}`);
|
||||
|
||||
} else if (messageType === 'product_started') {
|
||||
console.log(` 🚀 Started training: ${data.product_name || 'Unknown product'}`);
|
||||
|
||||
} else if (messageType === 'product_completed') {
|
||||
console.log(` ✅ Product completed: ${data.product_name || 'Unknown product'}`);
|
||||
if (data.metrics) {
|
||||
console.log(` 📊 Metrics: ${JSON.stringify(data.metrics, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(''); // Add spacing between messages
|
||||
}
|
||||
|
||||
socket.on('end', () => {
|
||||
const duration = Math.round((Date.now() - jobStats.startTime) / 1000);
|
||||
|
||||
console.log(`\n📊 WebSocket connection ended`);
|
||||
console.log(`📨 Total messages received: ${messageCount}`);
|
||||
console.log(`⏱️ Connection duration: ${duration}s`);
|
||||
console.log(`📈 Highest progress reached: ${highestProgress}%`);
|
||||
|
||||
if (jobCompleted) {
|
||||
console.log('✅ Job completed successfully - connection closed normally');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('⚠️ Connection ended before job completion');
|
||||
console.log(`📊 Progress reached: ${highestProgress}%`);
|
||||
console.log(`📋 Steps completed: ${jobStats.stepsCompleted.length}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.log(`❌ WebSocket error: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Enhanced ping mechanism - send pings more frequently
|
||||
const pingInterval = setInterval(() => {
|
||||
if (socket.writable && !jobCompleted) {
|
||||
try {
|
||||
const pingFrame = Buffer.from([0x89, 0x00]);
|
||||
socket.write(pingFrame);
|
||||
} catch (e) {
|
||||
// Ignore ping errors
|
||||
}
|
||||
}
|
||||
}, 5000); // Ping every 5 seconds
|
||||
|
||||
// Heartbeat check - ensure we're still receiving messages
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
if (!jobCompleted) {
|
||||
const timeSinceLastMessage = Date.now() - lastProgressUpdate;
|
||||
|
||||
if (timeSinceLastMessage > 60000) { // 60 seconds without messages
|
||||
console.log('\n⚠️ No messages received for 60 seconds');
|
||||
console.log(' This could indicate the training is stuck or connection issues');
|
||||
console.log(` Last progress: ${highestProgress}%`);
|
||||
} else if (timeSinceLastMessage > 30000) { // 30 seconds warning
|
||||
console.log(`\n💤 Quiet period: ${Math.round(timeSinceLastMessage/1000)}s since last update`);
|
||||
console.log(' (This is normal during intensive training phases)');
|
||||
}
|
||||
}
|
||||
}, 15000); // Check every 15 seconds
|
||||
|
||||
// Safety timeout - close connection if max duration exceeded
|
||||
const safetyTimeout = setTimeout(() => {
|
||||
if (!jobCompleted) {
|
||||
clearInterval(pingInterval);
|
||||
clearInterval(heartbeatInterval);
|
||||
|
||||
console.log(`\n⏰ Maximum duration (${maxDuration/1000}s) reached`);
|
||||
console.log(`📊 Final status:`);
|
||||
console.log(` 📨 Messages received: ${messageCount}`);
|
||||
console.log(` 📈 Progress reached: ${highestProgress}%`);
|
||||
console.log(` 📋 Steps completed: ${jobStats.stepsCompleted.length}`);
|
||||
console.log(` 🍞 Products processed: ${jobStats.productsProcessed.length}`);
|
||||
|
||||
if (messageCount > 0) {
|
||||
console.log('\n✅ WebSocket communication was successful!');
|
||||
console.log(' Training may still be running - check server logs for completion');
|
||||
} else {
|
||||
console.log('\n⚠️ No messages received during monitoring period');
|
||||
}
|
||||
|
||||
socket.end();
|
||||
}
|
||||
}, maxDuration);
|
||||
|
||||
// Clean up intervals when job completes
|
||||
socket.on('end', () => {
|
||||
clearInterval(pingInterval);
|
||||
clearInterval(heartbeatInterval);
|
||||
clearTimeout(safetyTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('response', (res) => {
|
||||
console.log(`❌ HTTP response instead of WebSocket upgrade: ${res.statusCode}`);
|
||||
console.log('Response headers:', res.headers);
|
||||
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (body) console.log('Response body:', body);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.log(`❌ Connection error: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.end();
|
||||
EOF
|
||||
|
||||
# Run the ENHANCED Node.js WebSocket test
|
||||
local ws_url="$WS_BASE/tenants/$tenant_id/training/jobs/$job_id/live"
|
||||
echo "Starting enhanced WebSocket monitoring..."
|
||||
node "$ws_test_script" "$tenant_id" "$job_id" "$max_duration" "$ACCESS_TOKEN" "$ws_url"
|
||||
local exit_code=$?
|
||||
|
||||
# Clean up
|
||||
rm -f "$ws_test_script"
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
log_success "Training job completed successfully!"
|
||||
echo " 📡 WebSocket monitoring detected job completion"
|
||||
echo " 🎉 Real-time progress tracking worked perfectly"
|
||||
else
|
||||
log_warning "WebSocket monitoring ended before job completion"
|
||||
echo " 📊 Check the progress logs above for details"
|
||||
fi
|
||||
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
|
||||
install_websocat_if_needed() {
|
||||
if ! command -v websocat >/dev/null 2>&1; then
|
||||
echo "📦 Installing websocat for better WebSocket testing..."
|
||||
|
||||
# Try to install websocat (works on most Linux systems)
|
||||
if command -v cargo >/dev/null 2>&1; then
|
||||
cargo install websocat 2>/dev/null || true
|
||||
elif [ -x "$(command -v wget)" ]; then
|
||||
wget -q -O /tmp/websocat "https://github.com/vi/websocat/releases/latest/download/websocat.x86_64-unknown-linux-musl" 2>/dev/null || true
|
||||
if [ -f /tmp/websocat ]; then
|
||||
chmod +x /tmp/websocat
|
||||
sudo mv /tmp/websocat /usr/local/bin/ 2>/dev/null || mv /tmp/websocat ~/bin/ 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v websocat >/dev/null 2>&1; then
|
||||
log_success "websocat installed successfully"
|
||||
return 0
|
||||
else
|
||||
log_warning "websocat installation failed, using Node.js fallback"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# IMPROVED: WebSocket connection function with better tool selection
|
||||
test_websocket_connection() {
|
||||
local tenant_id="$1"
|
||||
local job_id="$2"
|
||||
local duration="$3"
|
||||
|
||||
log_step "4.2. Testing WebSocket connection for real-time training progress"
|
||||
log_step "4.2. Connecting to WebSocket for real-time progress monitoring"
|
||||
|
||||
echo "WebSocket URL: $WS_BASE/tenants/$tenant_id/training/jobs/$job_id/live"
|
||||
echo "Test duration: ${duration}s"
|
||||
echo ""
|
||||
|
||||
# Check if websocat is available
|
||||
if command -v websocat >/dev/null 2>&1; then
|
||||
# Try to install websocat if not available
|
||||
if install_websocat_if_needed; then
|
||||
test_websocket_with_websocat "$tenant_id" "$job_id" "$duration"
|
||||
elif command -v node >/dev/null 2>&1; then
|
||||
test_websocket_with_nodejs_builtin "$tenant_id" "$job_id" "$duration"
|
||||
@@ -221,382 +630,6 @@ test_websocket_with_websocat() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Test WebSocket using Node.js
|
||||
test_websocket_with_nodejs() {
|
||||
local tenant_id="$1"
|
||||
local job_id="$2"
|
||||
local duration="$3"
|
||||
|
||||
echo "Using Node.js for WebSocket testing..."
|
||||
|
||||
# Create Node.js WebSocket test script
|
||||
local ws_test_script="/tmp/websocket_test_$job_id.js"
|
||||
cat > "$ws_test_script" << 'EOF'
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const tenantId = process.argv[2];
|
||||
const jobId = process.argv[3];
|
||||
const duration = parseInt(process.argv[4]) * 1000;
|
||||
const accessToken = process.argv[5];
|
||||
const wsUrl = process.argv[6];
|
||||
|
||||
console.log(`Connecting to: ${wsUrl}`);
|
||||
|
||||
const ws = new WebSocket(wsUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
let messageCount = 0;
|
||||
let startTime = Date.now();
|
||||
|
||||
ws.on('open', function() {
|
||||
console.log('✅ WebSocket connected successfully');
|
||||
|
||||
// Send periodic pings
|
||||
const pingInterval = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send('ping');
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Close after duration
|
||||
setTimeout(() => {
|
||||
clearInterval(pingInterval);
|
||||
console.log(`\n📊 WebSocket test completed after ${duration/1000}s`);
|
||||
console.log(`📨 Total messages received: ${messageCount}`);
|
||||
if (messageCount > 0) {
|
||||
console.log('✅ WebSocket communication successful');
|
||||
} else {
|
||||
console.log('⚠️ No training progress messages received');
|
||||
console.log(' This may be normal if training completed quickly');
|
||||
}
|
||||
ws.close();
|
||||
process.exit(0);
|
||||
}, duration);
|
||||
});
|
||||
|
||||
ws.on('message', function(data) {
|
||||
messageCount++;
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
try {
|
||||
const message = JSON.parse(data);
|
||||
console.log(`\n[${timestamp}] 📨 Message ${messageCount}:`);
|
||||
console.log(` Type: ${message.type || 'unknown'}`);
|
||||
console.log(` Job ID: ${message.job_id || 'unknown'}`);
|
||||
|
||||
if (message.data) {
|
||||
if (message.data.progress !== undefined) {
|
||||
console.log(` Progress: ${message.data.progress}%`);
|
||||
}
|
||||
if (message.data.current_step) {
|
||||
console.log(` Step: ${message.data.current_step}`);
|
||||
}
|
||||
if (message.data.current_product) {
|
||||
console.log(` Product: ${message.data.current_product}`);
|
||||
}
|
||||
if (message.data.estimated_time_remaining_minutes) {
|
||||
console.log(` ETA: ${message.data.estimated_time_remaining_minutes} minutes`);
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for completion messages
|
||||
if (message.type === 'completed') {
|
||||
console.log('🎉 Training completed!');
|
||||
} else if (message.type === 'failed') {
|
||||
console.log('❌ Training failed!');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(`[${timestamp}] Raw message: ${data}`);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', function(error) {
|
||||
console.log('❌ WebSocket error:', error.message);
|
||||
});
|
||||
|
||||
ws.on('close', function(code, reason) {
|
||||
console.log(`\n🔌 WebSocket closed (code: ${code}, reason: ${reason || 'normal'})`);
|
||||
process.exit(code === 1000 ? 0 : 1);
|
||||
});
|
||||
EOF
|
||||
|
||||
# Run Node.js WebSocket test
|
||||
local ws_url="$WS_BASE/tenants/$tenant_id/training/jobs/$job_id/live"
|
||||
node "$ws_test_script" "$tenant_id" "$job_id" "$duration" "$ACCESS_TOKEN" "$ws_url" &
|
||||
WS_PID=$!
|
||||
|
||||
# Wait for completion
|
||||
wait $WS_PID
|
||||
local exit_code=$?
|
||||
|
||||
# Clean up
|
||||
rm -f "$ws_test_script"
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
log_success "WebSocket test completed successfully"
|
||||
else
|
||||
log_warning "WebSocket test completed with issues"
|
||||
fi
|
||||
}
|
||||
|
||||
test_websocket_with_nodejs_builtin() {
|
||||
local tenant_id="$1"
|
||||
local job_id="$2"
|
||||
local duration="$3"
|
||||
|
||||
echo "Using Node.js with built-in modules for WebSocket testing..."
|
||||
|
||||
# Create Node.js WebSocket test script using built-in modules only
|
||||
local ws_test_script="/tmp/websocket_test_$job_id.js"
|
||||
cat > "$ws_test_script" << 'EOF'
|
||||
// WebSocket test using only built-in Node.js modules
|
||||
const { WebSocket } = require('node:http');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const tenantId = process.argv[2];
|
||||
const jobId = process.argv[3];
|
||||
const duration = parseInt(process.argv[4]) * 1000;
|
||||
const accessToken = process.argv[5];
|
||||
const wsUrl = process.argv[6];
|
||||
|
||||
console.log(`Connecting to: ${wsUrl}`);
|
||||
console.log(`Duration: ${duration/1000}s`);
|
||||
|
||||
// Parse WebSocket URL
|
||||
const url = new URL(wsUrl);
|
||||
const isSecure = url.protocol === 'wss:';
|
||||
const port = url.port || (isSecure ? 443 : 80);
|
||||
|
||||
// Create WebSocket key (required for WebSocket handshake)
|
||||
const key = crypto.randomBytes(16).toString('base64');
|
||||
|
||||
// WebSocket handshake headers
|
||||
const headers = {
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'Upgrade',
|
||||
'Sec-WebSocket-Key': key,
|
||||
'Sec-WebSocket-Version': '13',
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
};
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: port,
|
||||
path: url.pathname,
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
};
|
||||
|
||||
console.log(`Attempting WebSocket handshake to ${url.hostname}:${port}${url.pathname}`);
|
||||
|
||||
const client = isSecure ? https : http;
|
||||
let messageCount = 0;
|
||||
let startTime = Date.now();
|
||||
|
||||
const req = client.request(options);
|
||||
|
||||
req.on('upgrade', (res, socket, head) => {
|
||||
console.log('✅ WebSocket handshake successful');
|
||||
|
||||
let buffer = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
|
||||
// Process complete WebSocket frames
|
||||
while (buffer.length > 0) {
|
||||
// Simple WebSocket frame parsing (for text frames)
|
||||
if (buffer.length < 2) break;
|
||||
|
||||
const firstByte = buffer.charCodeAt(0);
|
||||
const secondByte = buffer.charCodeAt(1);
|
||||
|
||||
const opcode = firstByte & 0x0F;
|
||||
const masked = (secondByte & 0x80) === 0x80;
|
||||
let payloadLength = secondByte & 0x7F;
|
||||
|
||||
let offset = 2;
|
||||
|
||||
if (payloadLength === 126) {
|
||||
if (buffer.length < offset + 2) break;
|
||||
payloadLength = (buffer.charCodeAt(offset) << 8) | buffer.charCodeAt(offset + 1);
|
||||
offset += 2;
|
||||
} else if (payloadLength === 127) {
|
||||
if (buffer.length < offset + 8) break;
|
||||
// For simplicity, assume payload length fits in 32 bits
|
||||
payloadLength = (buffer.charCodeAt(offset + 4) << 24) |
|
||||
(buffer.charCodeAt(offset + 5) << 16) |
|
||||
(buffer.charCodeAt(offset + 6) << 8) |
|
||||
buffer.charCodeAt(offset + 7);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
if (buffer.length < offset + payloadLength) break;
|
||||
|
||||
// Extract payload
|
||||
let payload = buffer.slice(offset, offset + payloadLength);
|
||||
buffer = buffer.slice(offset + payloadLength);
|
||||
|
||||
if (opcode === 1) { // Text frame
|
||||
messageCount++;
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
try {
|
||||
const message = JSON.parse(payload);
|
||||
console.log(`\n[${timestamp}] 📨 Message ${messageCount}:`);
|
||||
console.log(` Type: ${message.type || 'unknown'}`);
|
||||
console.log(` Job ID: ${message.job_id || 'unknown'}`);
|
||||
|
||||
if (message.data) {
|
||||
if (message.data.progress !== undefined) {
|
||||
console.log(` Progress: ${message.data.progress}%`);
|
||||
}
|
||||
if (message.data.current_step) {
|
||||
console.log(` Step: ${message.data.current_step}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === 'completed') {
|
||||
console.log('🎉 Training completed!');
|
||||
} else if (message.type === 'failed') {
|
||||
console.log('❌ Training failed!');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log(`[${timestamp}] Raw message: ${payload}`);
|
||||
}
|
||||
} else if (opcode === 8) { // Close frame
|
||||
console.log('🔌 WebSocket closed by server');
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('end', () => {
|
||||
console.log(`\n📊 WebSocket test completed`);
|
||||
console.log(`📨 Total messages received: ${messageCount}`);
|
||||
if (messageCount > 0) {
|
||||
console.log('✅ WebSocket communication successful');
|
||||
} else {
|
||||
console.log('⚠️ No messages received during test period');
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.log('❌ WebSocket error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Send periodic pings to keep connection alive
|
||||
const pingInterval = setInterval(() => {
|
||||
if (socket.writable) {
|
||||
// Send ping frame (opcode 9)
|
||||
const pingFrame = Buffer.from([0x89, 0x00]);
|
||||
socket.write(pingFrame);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// Close after duration
|
||||
setTimeout(() => {
|
||||
clearInterval(pingInterval);
|
||||
console.log(`\n⏰ Test duration (${duration/1000}s) completed`);
|
||||
console.log(`📨 Total messages received: ${messageCount}`);
|
||||
|
||||
if (messageCount > 0) {
|
||||
console.log('✅ WebSocket communication successful');
|
||||
} else {
|
||||
console.log('⚠️ No training progress messages received');
|
||||
console.log(' This is normal if training completed before WebSocket connection');
|
||||
}
|
||||
|
||||
socket.end();
|
||||
process.exit(0);
|
||||
}, duration);
|
||||
});
|
||||
|
||||
req.on('response', (res) => {
|
||||
console.log(`❌ HTTP response instead of WebSocket upgrade: ${res.statusCode}`);
|
||||
console.log('Response headers:', res.headers);
|
||||
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (body) console.log('Response body:', body);
|
||||
});
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.log('❌ Connection error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.end();
|
||||
EOF
|
||||
|
||||
# Run the Node.js WebSocket test
|
||||
local ws_url="$WS_BASE/tenants/$tenant_id/training/jobs/$job_id/live"
|
||||
echo "Starting WebSocket test..."
|
||||
node "$ws_test_script" "$tenant_id" "$job_id" "$duration" "$ACCESS_TOKEN" "$ws_url"
|
||||
local exit_code=$?
|
||||
|
||||
# Clean up
|
||||
rm -f "$ws_test_script"
|
||||
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# Fallback: Test WebSocket using curl (limited functionality)
|
||||
test_websocket_with_curl() {
|
||||
local tenant_id="$1"
|
||||
local job_id="$2"
|
||||
local duration="$3"
|
||||
|
||||
log_warning "WebSocket testing tools not available (websocat/node.js)"
|
||||
echo "Falling back to HTTP polling simulation..."
|
||||
|
||||
# Create a simple HTTP-based progress polling simulation
|
||||
local poll_endpoint="$API_BASE/api/v1/tenants/$tenant_id/training/jobs/$job_id/status"
|
||||
local end_time=$(($(date +%s) + duration))
|
||||
local poll_count=0
|
||||
|
||||
echo "Simulating real-time updates by polling: $poll_endpoint"
|
||||
echo "Duration: ${duration}s"
|
||||
|
||||
while [ $(date +%s) -lt $end_time ]; do
|
||||
poll_count=$((poll_count + 1))
|
||||
echo ""
|
||||
echo "[$(date '+%H:%M:%S')] Poll #$poll_count - Checking training status..."
|
||||
|
||||
STATUS_RESPONSE=$(curl -s -X GET "$poll_endpoint" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
-H "X-Tenant-ID: $tenant_id")
|
||||
|
||||
echo "Response:"
|
||||
echo "$STATUS_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$STATUS_RESPONSE"
|
||||
|
||||
# Check if training is complete
|
||||
if echo "$STATUS_RESPONSE" | grep -q '"status".*"completed"\|"status".*"failed"'; then
|
||||
log_success "Training status detected as complete/failed - stopping polling"
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
done
|
||||
|
||||
log_success "HTTP polling simulation completed ($poll_count polls)"
|
||||
echo "💡 For real WebSocket testing, install: npm install -g websocat"
|
||||
}
|
||||
|
||||
# Wait for WebSocket messages and analyze them
|
||||
wait_for_websocket_messages() {
|
||||
local ws_log="$1"
|
||||
@@ -660,8 +693,8 @@ wait_for_websocket_messages() {
|
||||
|
||||
# Enhanced training step with WebSocket testing
|
||||
enhanced_training_step_with_completion_check() {
|
||||
echo -e "${STEP_ICONS[3]} ${PURPLE}STEP 4: MODEL TRAINING WITH WEBSOCKET MONITORING${NC}"
|
||||
echo "Enhanced training step with real-time progress monitoring"
|
||||
echo -e "${STEP_ICONS[3]} ${PURPLE}STEP 4: MODEL TRAINING WITH SMART WEBSOCKET MONITORING${NC}"
|
||||
echo "Enhanced training step with completion-aware progress monitoring"
|
||||
echo ""
|
||||
|
||||
log_step "4.1. Initiating model training with FULL dataset"
|
||||
@@ -694,42 +727,50 @@ enhanced_training_step_with_completion_check() {
|
||||
echo " Job ID: $WEBSOCKET_JOB_ID"
|
||||
echo " Status: $JOB_STATUS"
|
||||
|
||||
# Check if training completed instantly
|
||||
# Determine monitoring strategy based on initial status
|
||||
if [ "$JOB_STATUS" = "completed" ]; then
|
||||
log_warning "Training completed instantly - no real-time progress to monitor"
|
||||
echo " This can happen when:"
|
||||
echo " • Models are already trained and cached"
|
||||
echo " • No valid products found in sales data"
|
||||
echo " • Training data is insufficient"
|
||||
echo " • Models are already trained and cached"
|
||||
echo ""
|
||||
|
||||
# Show training results
|
||||
TOTAL_PRODUCTS=$(extract_json_field "$TRAINING_RESPONSE" "training_results.total_products")
|
||||
SUCCESSFUL_TRAININGS=$(extract_json_field "$TRAINING_RESPONSE" "training_results.successful_trainings")
|
||||
SALES_RECORDS=$(extract_json_field "$TRAINING_RESPONSE" "data_summary.sales_records")
|
||||
|
||||
echo ""
|
||||
echo "📊 Training Summary:"
|
||||
echo " Sales records: $SALES_RECORDS"
|
||||
echo " Products found: $TOTAL_PRODUCTS"
|
||||
echo " Successful trainings: $SUCCESSFUL_TRAININGS"
|
||||
|
||||
if [ "$TOTAL_PRODUCTS" = "0" ]; then
|
||||
log_warning "No products found for training"
|
||||
echo " Possible causes:"
|
||||
echo " • CSV doesn't contain valid product names"
|
||||
echo " • Product column is missing or malformed"
|
||||
echo " • Insufficient sales data per product"
|
||||
fi
|
||||
|
||||
# Still test WebSocket for demonstration
|
||||
# Brief WebSocket connection test
|
||||
log_step "4.2. Testing WebSocket endpoint (demonstration mode)"
|
||||
echo "Even though training is complete, testing WebSocket connection..."
|
||||
echo "Testing WebSocket connection for 10 seconds..."
|
||||
test_websocket_with_nodejs_builtin "$TENANT_ID" "$WEBSOCKET_JOB_ID" "10"
|
||||
|
||||
else
|
||||
# Training is in progress - monitor with WebSocket
|
||||
log_step "4.2. Connecting to WebSocket for real-time progress monitoring"
|
||||
test_websocket_with_nodejs_builtin "$TENANT_ID" "$WEBSOCKET_JOB_ID" "$WS_TEST_DURATION"
|
||||
# Training is in progress - use smart monitoring
|
||||
log_step "4.2. Starting smart WebSocket monitoring"
|
||||
echo " Strategy: Monitor until job completion"
|
||||
echo " Maximum wait time: ${WS_TEST_DURATION}s (safety timeout)"
|
||||
echo " Will automatically close when training completes"
|
||||
echo ""
|
||||
|
||||
# Use enhanced monitoring with longer timeout for real training
|
||||
local SMART_DURATION=$WS_TEST_DURATION
|
||||
|
||||
# Estimate duration based on data size (optional enhancement)
|
||||
if [ -n "$SALES_RECORDS" ] && [ "$SALES_RECORDS" -gt 1000 ]; then
|
||||
# For large datasets, extend timeout
|
||||
SMART_DURATION=$((WS_TEST_DURATION * 2))
|
||||
echo " 📊 Large dataset detected ($SALES_RECORDS records)"
|
||||
echo " 🕐 Extended timeout to ${SMART_DURATION}s for thorough training"
|
||||
fi
|
||||
|
||||
test_websocket_with_nodejs_builtin "$TENANT_ID" "$WEBSOCKET_JOB_ID" "$SMART_DURATION"
|
||||
fi
|
||||
|
||||
else
|
||||
@@ -786,41 +827,6 @@ services_check() {
|
||||
done
|
||||
}
|
||||
|
||||
check_websocket_prerequisites() {
|
||||
echo -e "${PURPLE}🔍 Checking WebSocket testing prerequisites...${NC}"
|
||||
|
||||
# Check for websocat
|
||||
if command -v websocat >/dev/null 2>&1; then
|
||||
log_success "websocat found - will use for WebSocket testing"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check for Node.js
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
local node_version=$(node --version 2>/dev/null || echo "unknown")
|
||||
log_success "Node.js found ($node_version) - will use for WebSocket testing"
|
||||
|
||||
# Check if ws module is available (try to require it)
|
||||
if node -e "require('ws')" 2>/dev/null; then
|
||||
log_success "Node.js 'ws' module available"
|
||||
else
|
||||
log_warning "Node.js 'ws' module not found"
|
||||
echo " Install with: npm install -g ws"
|
||||
echo " Will attempt to use built-in functionality..."
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warning "Neither websocat nor Node.js found"
|
||||
echo " WebSocket testing will use HTTP polling fallback"
|
||||
echo " For better testing, install one of:"
|
||||
echo " • websocat: cargo install websocat"
|
||||
echo " • Node.js: https://nodejs.org/"
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
services_check
|
||||
echo ""
|
||||
|
||||
@@ -1167,7 +1173,7 @@ echo ""
|
||||
# STEP 4: MODEL TRAINING (ONBOARDING PAGE STEP 4)
|
||||
# =================================================================
|
||||
|
||||
check_websocket_prerequisites
|
||||
test_websocket_connection
|
||||
|
||||
enhanced_training_step_with_completion_check
|
||||
|
||||
|
||||
Reference in New Issue
Block a user