Files
ethereum-rpc-docker/split-proxy/index.js
2025-06-01 22:00:33 +07:00

151 lines
3.7 KiB
JavaScript

const express = require('express');
const config = require('./config');
const logger = require('./logger');
const RPCProxy = require('./proxy');
const app = express();
const proxy = new RPCProxy();
// Middleware to parse JSON bodies
app.use(express.json({
limit: '50mb',
type: 'application/json'
}));
// Request timeout middleware
app.use((req, res, next) => {
// Set request timeout
const timeoutHandle = setTimeout(() => {
logger.error({
method: req.method,
url: req.url,
timeout: config.requestTimeout,
headersSent: res.headersSent,
finished: res.finished,
}, 'Request timeout');
if (!res.headersSent && !res.finished) {
res.status(504).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Request timeout',
},
id: req.body?.id,
});
}
}, config.requestTimeout);
// Clear timeout when response finishes
res.on('finish', () => {
clearTimeout(timeoutHandle);
});
res.on('close', () => {
clearTimeout(timeoutHandle);
});
next();
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
primaryEndpoint: config.primaryRpc,
secondaryEndpoint: config.secondaryRpc,
primaryRole: config.primaryRole,
currentStreamEndpoint: proxy.streamEndpoint,
currentCompareEndpoint: proxy.compareEndpoint,
});
});
// Main JSON-RPC endpoint
app.post('/', async (req, res) => {
await proxy.handleRequest(req, res);
});
// Error handling middleware
app.use((err, req, res, next) => {
logger.error({
error: err.message,
stack: err.stack,
}, 'Unhandled error');
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal error',
},
id: req.body?.id,
});
}
});
// Start server
const server = app.listen(config.port, () => {
logger.info({
port: config.port,
primaryRpc: config.primaryRpc,
secondaryRpc: config.secondaryRpc,
primaryRole: config.primaryRole,
latencyThreshold: config.latencyThresholdMs,
sizeDiffThreshold: config.sizeDiffThreshold,
}, 'ETH JSON-RPC proxy started');
});
// Set server timeouts to be longer than request timeout
// This prevents the server from closing connections while requests are in flight
server.timeout = config.requestTimeout + 5000; // Add 5 seconds buffer
server.keepAliveTimeout = config.requestTimeout + 5000;
server.headersTimeout = config.requestTimeout + 6000; // Should be > keepAliveTimeout
logger.info({
serverTimeout: server.timeout,
keepAliveTimeout: server.keepAliveTimeout,
headersTimeout: server.headersTimeout,
requestTimeout: config.requestTimeout,
}, 'Server timeout configuration');
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('SIGTERM received, shutting down gracefully');
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
logger.info('SIGINT received, shutting down gracefully');
server.close(() => {
logger.info('Server closed');
process.exit(0);
});
});
// Dynamic role switching with SIGUSR1
process.on('SIGUSR1', () => {
logger.info('SIGUSR1 received, switching primary/secondary roles');
try {
const newConfig = proxy.switchRoles();
logger.info({
...newConfig,
signal: 'SIGUSR1',
}, 'Successfully switched roles');
} catch (error) {
logger.error({
error: error.message,
signal: 'SIGUSR1',
}, 'Failed to switch roles');
}
});
// Log instructions for role switching on startup
logger.info('To switch primary/secondary roles at runtime, send SIGUSR1 signal:');
logger.info(' kill -USR1 <pid> OR docker kill -s USR1 <container>');
module.exports = app;