mempool/backend/src/index.ts

343 lines
12 KiB
TypeScript
Raw Normal View History

2022-08-23 20:25:29 +04:00
import express from 'express';
import { Application, Request, Response, NextFunction } from 'express';
2019-07-21 17:59:47 +03:00
import * as http from 'http';
import * as WebSocket from 'ws';
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
import cluster from 'cluster';
import DB from './database';
import config from './config';
2019-07-21 17:59:47 +03:00
import blocks from './api/blocks';
import memPool from './api/mempool';
import diskCache from './api/disk-cache';
2022-07-11 19:15:28 +02:00
import statistics from './api/statistics/statistics';
import websocketHandler from './api/websocket-handler';
2020-09-10 14:46:23 +07:00
import bisq from './api/bisq/bisq';
import bisqMarkets from './api/bisq/markets';
import logger from './logger';
import backendInfo from './api/backend-info';
import loadingIndicators from './api/loading-indicators';
import mempool from './api/mempool';
import elementsParser from './api/liquid/elements-parser';
import databaseMigration from './api/database-migration';
2021-12-21 02:00:50 +04:00
import syncAssets from './sync-assets';
import icons from './api/liquid/icons';
import { Common } from './api/common';
import poolsUpdater from './tasks/pools-updater';
import indexer from './indexer';
import nodesRoutes from './api/explorer/nodes.routes';
import channelsRoutes from './api/explorer/channels.routes';
import generalLightningRoutes from './api/explorer/general.routes';
import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
import networkSyncService from './tasks/lightning/network-sync.service';
import statisticsRoutes from './api/statistics/statistics.routes';
2023-07-29 14:23:06 +09:00
import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes';
import bisqRoutes from './api/bisq/bisq.routes';
import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
2022-08-23 20:25:29 +04:00
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater';
import chainTips from './api/chain-tips';
import { AxiosError } from 'axios';
2023-03-04 23:13:55 -06:00
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
import redisCache from './api/redis-cache';
import accelerationApi from './api/services/acceleration';
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
2019-07-21 17:59:47 +03:00
class Server {
2020-09-22 03:52:54 +07:00
private wss: WebSocket.Server | undefined;
private server: http.Server | undefined;
private app: Application;
2023-05-03 10:11:44 +04:00
private currentBackendRetryInterval = 1;
private backendRetryCount = 0;
2019-07-21 17:59:47 +03:00
2023-03-04 23:13:55 -06:00
private maxHeapSize: number = 0;
private heapLogInterval: number = 60;
private warnedHeapCritical: boolean = false;
private lastHeapLogTime: number | null = null;
2019-07-21 17:59:47 +03:00
constructor() {
this.app = express();
if (!config.MEMPOOL.SPAWN_CLUSTER_PROCS) {
2020-09-22 03:52:54 +07:00
this.startServer();
return;
}
if (cluster.isPrimary) {
logger.notice(`Mempool Server (Master) is running on port ${config.MEMPOOL.HTTP_PORT} (${backendInfo.getShortCommitHash()})`);
2020-09-22 03:52:54 +07:00
const numCPUs = config.MEMPOOL.SPAWN_CLUSTER_PROCS;
2020-09-22 03:52:54 +07:00
for (let i = 0; i < numCPUs; i++) {
2020-09-30 00:25:43 +07:00
const env = { workerId: i };
const worker = cluster.fork(env);
worker.process['env'] = env;
2020-09-22 03:52:54 +07:00
}
cluster.on('exit', (worker, code, signal) => {
2020-09-30 00:25:43 +07:00
const workerId = worker.process['env'].workerId;
logger.warn(`Mempool Worker PID #${worker.process.pid} workerId: ${workerId} died. Restarting in 10 seconds... ${signal || code}`);
2020-09-30 00:25:43 +07:00
setTimeout(() => {
const env = { workerId: workerId };
const newWorker = cluster.fork(env);
newWorker.process['env'] = env;
}, 10000);
2020-09-22 03:52:54 +07:00
});
} else {
this.startServer(true);
}
}
2022-08-23 20:25:29 +04:00
async startServer(worker = false): Promise<void> {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
2023-08-28 19:20:58 +09:00
// Register cleanup listeners for exit events
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(event => {
2023-08-28 19:20:58 +09:00
process.on(event, () => { this.onExit(event); });
});
process.on('uncaughtException', (error) => {
this.onUnhandledException('uncaughtException', error);
});
process.on('unhandledRejection', (reason, promise) => {
this.onUnhandledException('unhandledRejection', reason);
});
2023-08-28 19:20:58 +09:00
if (config.MEMPOOL.BACKEND === 'esplora') {
bitcoinApi.startHealthChecks();
}
if (config.DATABASE.ENABLED) {
2023-08-28 19:20:58 +09:00
DB.getPidLock();
await DB.checkDbConnection();
try {
if (process.env.npm_config_reindex_blocks === 'true') { // Re-index requests
await databaseMigration.$blocksReindexingTruncate();
}
await databaseMigration.$initializeOrMigrateDatabase();
} catch (e) {
throw new Error(e instanceof Error ? e.message : 'Error');
}
}
2019-07-21 17:59:47 +03:00
this.app
2020-06-07 17:30:32 +07:00
.use((req: Request, res: Response, next: NextFunction) => {
2019-07-21 17:59:47 +03:00
res.setHeader('Access-Control-Allow-Origin', '*');
next();
})
.use(express.urlencoded({ extended: true }))
.use(express.text({ type: ['text/plain', 'application/base64'] }))
;
if (config.DATABASE.ENABLED) {
await priceUpdater.$initializeLatestPriceWithDb();
}
this.server = http.createServer(this.app);
this.wss = new WebSocket.Server({ server: this.server });
2019-07-21 17:59:47 +03:00
this.setUpWebsocketHandling();
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
2022-08-23 20:25:29 +04:00
if (config.MEMPOOL.ENABLED) {
if (config.MEMPOOL.CACHE_ENABLED) {
await diskCache.$loadMempoolCache();
} else if (config.REDIS.ENABLED) {
await redisCache.$loadCache();
}
2022-08-23 20:25:29 +04:00
}
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
2020-07-23 00:04:29 +07:00
statistics.startStatistics();
}
if (Common.isLiquid()) {
2022-04-15 13:20:54 +09:00
try {
icons.loadIcons();
} catch (e) {
logger.err('Cannot load liquid icons. Ignoring. Reason: ' + (e instanceof Error ? e.message : e));
}
2021-12-21 02:00:50 +04:00
}
priceUpdater.$run();
await chainTips.updateOrphanedBlocks();
this.setUpHttpApiRoutes();
2022-08-23 20:25:29 +04:00
if (config.MEMPOOL.ENABLED) {
this.runMainUpdateLoop();
}
2019-07-21 17:59:47 +03:00
2023-03-04 23:13:55 -06:00
setInterval(() => { this.healthCheck(); }, 2500);
2021-04-25 02:38:46 +04:00
if (config.BISQ.ENABLED) {
bisq.startBisqService();
2023-06-12 15:31:47 -04:00
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitData('bsq-price', price));
blocks.setNewBlockCallback(bisq.handleNewBitcoinBlock.bind(bisq));
2020-09-10 14:46:23 +07:00
bisqMarkets.startBisqService();
}
if (config.LIGHTNING.ENABLED) {
this.$runLightningBackend();
}
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
2020-09-22 03:52:54 +07:00
if (worker) {
logger.info(`Mempool Server worker #${process.pid} started`);
2020-09-22 03:52:54 +07:00
} else {
logger.notice(`Mempool Server is running on port ${config.MEMPOOL.HTTP_PORT}`);
2020-09-22 03:52:54 +07:00
}
2019-07-21 17:59:47 +03:00
});
}
2022-08-23 20:25:29 +04:00
async runMainUpdateLoop(): Promise<void> {
2023-07-17 18:21:44 +09:00
const start = Date.now();
try {
try {
await memPool.$updateMemPoolInfo();
} catch (e) {
const msg = `updateMempoolInfo: ${(e instanceof Error ? e.message : e)}`;
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
logger.warn(msg);
} else {
logger.debug(msg);
}
}
const newMempool = await bitcoinApi.$getRawMempool();
const newAccelerations = await accelerationApi.$fetchAccelerations();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, newAccelerations, pollRate);
}
indexer.$run();
priceUpdater.$run();
// rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS
2023-07-17 18:21:44 +09:00
const elapsed = Date.now() - start;
const remainingTime = Math.max(0, pollRate - elapsed);
2023-07-17 18:21:44 +09:00
setTimeout(this.runMainUpdateLoop.bind(this), numHandledBlocks > 0 ? 0 : remainingTime);
2023-05-03 10:11:44 +04:00
this.backendRetryCount = 0;
} catch (e: any) {
2023-05-03 10:11:44 +04:00
this.backendRetryCount++;
let loggerMsg = `Exception in runMainUpdateLoop() (count: ${this.backendRetryCount}). Retrying in ${this.currentBackendRetryInterval} sec.`;
loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`;
if (e?.stack) {
loggerMsg += ` Stack trace: ${e.stack}`;
}
// When we get a first Exception, only `logger.debug` it and retry after 5 seconds
// From the second Exception, `logger.warn` the Exception and increase the retry delay
2023-05-03 10:11:44 +04:00
if (this.backendRetryCount >= 5) {
logger.warn(loggerMsg);
mempool.setOutOfSync();
} else {
logger.debug(loggerMsg);
}
if (e instanceof AxiosError) {
logger.debug(`AxiosError: ${e?.message}`);
}
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
} finally {
diskCache.unlock();
}
2019-07-21 17:59:47 +03:00
}
2022-08-23 20:25:29 +04:00
async $runLightningBackend(): Promise<void> {
try {
await fundingTxFetcher.$init();
await networkSyncService.$startService();
await lightningStatsUpdater.$startService();
2023-03-01 16:52:24 +09:00
await forensicsService.$startService();
} catch(e) {
logger.err(`Exception in $runLightningBackend. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
await Common.sleep$(1000 * 60);
this.$runLightningBackend();
};
}
2022-08-23 20:25:29 +04:00
setUpWebsocketHandling(): void {
2020-09-22 03:52:54 +07:00
if (this.wss) {
websocketHandler.setWebsocketServer(this.wss);
}
if (Common.isLiquid() && config.DATABASE.ENABLED) {
blocks.setNewBlockCallback(async () => {
try {
await elementsParser.$parse();
await elementsParser.$updateFederationUtxos();
} catch (e) {
logger.warn('Elements parsing error: ' + (e instanceof Error ? e.message : e));
}
});
}
websocketHandler.setupConnectionHandling();
2022-08-23 20:25:29 +04:00
if (config.MEMPOOL.ENABLED) {
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
memPool.setAsyncMempoolChangedCallback(websocketHandler.$handleMempoolChange.bind(websocketHandler));
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
2022-08-23 20:25:29 +04:00
}
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
2019-07-21 17:59:47 +03:00
}
2022-08-23 20:25:29 +04:00
setUpHttpApiRoutes(): void {
2022-07-11 19:15:28 +02:00
bitcoinRoutes.initRoutes(this.app);
bitcoinCoreRoutes.initRoutes(this.app);
2023-07-29 14:23:06 +09:00
pricesRoutes.initRoutes(this.app);
2022-08-23 20:25:29 +04:00
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
2022-07-11 19:15:28 +02:00
statisticsRoutes.initRoutes(this.app);
}
2022-08-23 20:25:29 +04:00
if (Common.indexingEnabled() && config.MEMPOOL.ENABLED) {
2022-07-11 19:15:28 +02:00
miningRoutes.initRoutes(this.app);
2020-10-19 18:47:10 +07:00
}
2021-04-25 02:38:46 +04:00
if (config.BISQ.ENABLED) {
2022-07-11 19:15:28 +02:00
bisqRoutes.initRoutes(this.app);
2020-09-10 14:46:23 +07:00
}
if (Common.isLiquid()) {
2022-07-11 19:15:28 +02:00
liquidRoutes.initRoutes(this.app);
2021-12-14 16:06:03 +04:00
}
if (config.LIGHTNING.ENABLED) {
generalLightningRoutes.initRoutes(this.app);
nodesRoutes.initRoutes(this.app);
channelsRoutes.initRoutes(this.app);
}
}
2023-03-04 23:13:55 -06:00
healthCheck(): void {
const now = Date.now();
const stats = v8.getHeapStatistics();
this.maxHeapSize = Math.max(stats.used_heap_size, this.maxHeapSize);
const warnThreshold = 0.8 * stats.heap_size_limit;
2023-03-04 23:13:55 -06:00
const byteUnits = getBytesUnit(Math.max(this.maxHeapSize, stats.heap_size_limit));
if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) {
this.warnedHeapCritical = true;
2023-03-16 14:35:09 +09:00
logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit * 100).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`);
2023-03-04 23:13:55 -06:00
}
if (this.lastHeapLogTime === null || (now - this.lastHeapLogTime) > (this.heapLogInterval * 1000)) {
logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`);
this.warnedHeapCritical = false;
this.maxHeapSize = 0;
this.lastHeapLogTime = now;
}
}
2023-08-28 19:20:58 +09:00
onExit(exitEvent, code = 0): void {
logger.debug(`onExit for signal: ${exitEvent}`);
if (config.DATABASE.ENABLED) {
DB.releasePidLock();
}
process.exit(code);
2023-08-28 19:20:58 +09:00
}
onUnhandledException(type, error): void {
console.error(`${type}:`, error);
this.onExit(type, 1);
}
}
2023-08-28 19:20:58 +09:00
2022-08-23 20:25:29 +04:00
((): Server => new Server())();