2533 lines
94 KiB
TypeScript
2533 lines
94 KiB
TypeScript
import {ConfigurationModule} from "./config";
|
|
import {ConnectionManager} from "../connections/manager.class";
|
|
import {JsonRpc} from "eosjs/dist";
|
|
import {ApiResponse, Client} from "@elastic/elasticsearch";
|
|
import {HyperionModuleLoader} from "./loader";
|
|
|
|
import {
|
|
debugLog,
|
|
getLastIndexedABI,
|
|
getLastIndexedBlock,
|
|
getLastIndexedBlockByDelta,
|
|
getLastIndexedBlockByDeltaFromRange,
|
|
getLastIndexedBlockFromRange,
|
|
hLog,
|
|
messageAllWorkers
|
|
} from "../helpers/common_functions";
|
|
|
|
import {GetInfoResult} from "eosjs/dist/eosjs-rpc-interfaces";
|
|
import pm2io from '@pm2/io';
|
|
|
|
import {
|
|
createWriteStream,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
symlinkSync,
|
|
unlinkSync,
|
|
writeFileSync,
|
|
WriteStream
|
|
} from "fs";
|
|
|
|
import path from "path";
|
|
import cluster, {Worker} from "cluster";
|
|
import {io, Socket} from 'socket.io-client';
|
|
import {HyperionWorkerDef} from "../interfaces/hyperionWorkerDef";
|
|
import {HyperionConfig} from "../interfaces/hyperionConfig";
|
|
|
|
import {queue, QueueObject} from "async";
|
|
import {convertLegacyPublicKey} from "eosjs/dist/eosjs-numeric";
|
|
import AlertsManager from "./alertsManager";
|
|
import IORedis from "ioredis";
|
|
import {IOConfig} from "@pm2/io/build/main/pmx";
|
|
import Gauge from "@pm2/io/build/main/utils/metrics/gauge";
|
|
|
|
import {bootstrap} from 'global-agent';
|
|
import moment = require("moment");
|
|
import Timeout = NodeJS.Timeout;
|
|
import {App, TemplatedApp, WebSocket} from "uWebSockets.js";
|
|
|
|
interface RevBlock {
|
|
num: number;
|
|
id: string;
|
|
tx: string[];
|
|
}
|
|
|
|
export class HyperionMaster {
|
|
|
|
// global configuration
|
|
conf: HyperionConfig;
|
|
|
|
// connection manager
|
|
manager: ConnectionManager;
|
|
|
|
// eosjs rpc
|
|
rpc: JsonRpc;
|
|
|
|
// live producer schedule
|
|
private currentSchedule: any;
|
|
|
|
// elasticsearch client
|
|
private client: Client;
|
|
|
|
// hyperion module loader
|
|
mLoader: HyperionModuleLoader;
|
|
|
|
// Chain/Queue Prefix
|
|
chain: string;
|
|
|
|
// Chain API Info
|
|
private chain_data: GetInfoResult;
|
|
|
|
// Main workers
|
|
private workerMap: HyperionWorkerDef[];
|
|
private worker_index: number;
|
|
|
|
// Scaling params
|
|
private max_readers: number;
|
|
private IndexingQueues: any;
|
|
private maxBatchSize: number;
|
|
private dsErrorStream: WriteStream;
|
|
|
|
// mem-optimized deserialization pool
|
|
private dsPoolMap: Map<number, Worker> = new Map();
|
|
private globalUsageMap = {};
|
|
private totalContractHits = 0;
|
|
|
|
// producer monitoring
|
|
private lastProducedBlockNum = 0;
|
|
private handoffCounter: number = 0;
|
|
private lastProducer: string = null;
|
|
private producedBlocks: object = {};
|
|
private missedRounds: object = {};
|
|
|
|
// IPC Messaging
|
|
private totalMessages = 0;
|
|
|
|
// Repair
|
|
private doctorId = 0;
|
|
private missingRanges = [];
|
|
private doctorIdle = true;
|
|
|
|
// Indexer Monitoring
|
|
private lastProcessedBlockNum = 0;
|
|
private allowMoreReaders = true;
|
|
private allowShutdown = false;
|
|
private readonly log_interval = 5000;
|
|
private consumedBlocks = 0;
|
|
private deserializedActions = 0;
|
|
private indexedObjects = 0;
|
|
private deserializedDeltas = 0;
|
|
private liveConsumedBlocks = 0;
|
|
private livePushedBlocks = 0;
|
|
private pushedBlocks = 0;
|
|
private total_read = 0;
|
|
private total_blocks = 0;
|
|
private total_actions = 0;
|
|
private total_deltas = 0;
|
|
private total_abis = 0;
|
|
private consume_rates: number[] = [];
|
|
private total_range = 0;
|
|
private range_completed = false;
|
|
private head: number;
|
|
private starting_block: number;
|
|
private idle_count = 0;
|
|
private auto_stop = 0;
|
|
|
|
// IPC Messages Handling
|
|
private msgHandlerMap: any;
|
|
|
|
private cachedInitABI = null;
|
|
private activeReadersCount = 0;
|
|
private lastAssignedBlock: number;
|
|
private lastIndexedABI: number;
|
|
private activeSchedule: any;
|
|
private pendingSchedule: any;
|
|
private proposedSchedule: any;
|
|
private wsRouterWorker: Worker;
|
|
private liveBlockQueue: QueueObject<any>;
|
|
private readingPaused = false;
|
|
private readingLimited = false;
|
|
|
|
// Hyperion Hub Socket
|
|
private hub: Socket;
|
|
|
|
// Timers
|
|
private contractMonitoringInterval: Timeout;
|
|
private queueMonitoringInterval: Timeout;
|
|
private shutdownTimer: Timeout;
|
|
|
|
private mode_transition = false;
|
|
|
|
private readonly cm: ConfigurationModule;
|
|
private alerts: AlertsManager;
|
|
private ioRedisClient: IORedis.Redis;
|
|
|
|
// Reversible Blocks
|
|
private revBlockArray: RevBlock[] = [];
|
|
private shutdownStarted = false;
|
|
|
|
// pm2 custom metrics/gauges
|
|
private metrics: Record<string, Gauge> = {};
|
|
private shouldCountIPCMessages = false;
|
|
|
|
// local websocket server
|
|
private localController: TemplatedApp;
|
|
private liveReader?: HyperionWorkerDef;
|
|
private repairReader?: HyperionWorkerDef;
|
|
private pendingRepairRanges: {
|
|
start: number,
|
|
end: number,
|
|
size?: number
|
|
}[] = [];
|
|
private connectedController?: WebSocket<any>;
|
|
private lastIrreversibleBlock: number = 0;
|
|
|
|
constructor() {
|
|
this.cm = new ConfigurationModule();
|
|
this.conf = this.cm.config;
|
|
|
|
if (this.conf.settings.use_global_agent) {
|
|
bootstrap();
|
|
}
|
|
|
|
this.alerts = new AlertsManager(this.conf.alerts);
|
|
this.manager = new ConnectionManager(this.cm);
|
|
this.mLoader = new HyperionModuleLoader(this.cm);
|
|
this.mLoader.init().then(() => {
|
|
|
|
this.mLoader.plugins.forEach(value => {
|
|
value.initOnce();
|
|
hLog(`Plugin loaded: ${value.internalPluginName}`);
|
|
})
|
|
|
|
this.chain = this.conf.settings.chain;
|
|
this.alerts.chainName = this.chain;
|
|
this.initHandlerMap();
|
|
|
|
this.mLoader.plugins.forEach(value => {
|
|
const pluginHandlerMap = value.initHandlerMap();
|
|
hLog(`Plugin IPC handlers loaded: ${Object.keys(pluginHandlerMap)}`);
|
|
for (const handler in pluginHandlerMap) {
|
|
if (Object.keys(this.msgHandlerMap).includes(handler)) {
|
|
hLog(`Handler already mapped, cannot load ${handler} from plugin ${value.internalPluginName}`);
|
|
return;
|
|
}
|
|
|
|
this.msgHandlerMap[handler] = pluginHandlerMap[handler];
|
|
}
|
|
})
|
|
|
|
this.liveBlockQueue = queue((task, callback) => {
|
|
this.onLiveBlock(task);
|
|
callback();
|
|
}, 1);
|
|
});
|
|
}
|
|
|
|
initHandlerMap() {
|
|
this.msgHandlerMap = {
|
|
'skipped_block': (msg: any) => {
|
|
this.consumedBlocks++;
|
|
if (msg.block_num > this.lastProcessedBlockNum) {
|
|
this.lastProcessedBlockNum = msg.block_num;
|
|
}
|
|
},
|
|
'consumed_block': (msg: any) => {
|
|
if (msg.live === 'false') {
|
|
this.consumedBlocks++;
|
|
if (msg.block_num > this.lastProcessedBlockNum) {
|
|
this.lastProcessedBlockNum = msg.block_num;
|
|
}
|
|
} else {
|
|
|
|
// LIVE READER
|
|
this.liveConsumedBlocks++;
|
|
|
|
// cache the last block number for quick api access
|
|
this.ioRedisClient.set(`${this.chain}:last_idx_block`, `${msg.block_num}@${msg.block_ts}`).catch(console.log);
|
|
|
|
if (this.revBlockArray.length > 0) {
|
|
if (this.revBlockArray[this.revBlockArray.length - 1].num === msg.block_num) {
|
|
hLog('WARNING, same block number detected!');
|
|
hLog(this.revBlockArray[this.revBlockArray.length - 1].num, msg.block_num);
|
|
}
|
|
}
|
|
|
|
this.revBlockArray.push({
|
|
num: msg.block_num,
|
|
id: msg.block_id,
|
|
tx: msg.trx_ids
|
|
});
|
|
|
|
this.lastProducedBlockNum = msg.block_num;
|
|
|
|
if (this.conf.settings.bp_monitoring && !this.conf.indexer.abi_scan_mode) {
|
|
this.liveBlockQueue.push(msg).catch(reason => {
|
|
hLog('Error handling consumed_block:', reason);
|
|
});
|
|
if (this.liveBlockQueue.length() > 1) {
|
|
hLog("Live Block Queue:", this.liveBlockQueue.length(), 'blocks');
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'new_schedule': (msg: any) => {
|
|
this.onScheduleUpdate(msg);
|
|
},
|
|
'init_abi': (msg: any) => {
|
|
if (!this.cachedInitABI) {
|
|
this.cachedInitABI = msg.data;
|
|
hLog('received ship abi for distribution');
|
|
messageAllWorkers(cluster, {
|
|
event: 'initialize_abi',
|
|
data: msg.data
|
|
});
|
|
this.monitorIndexingQueues();
|
|
this.startContractMonitoring();
|
|
}
|
|
},
|
|
'router_ready': () => {
|
|
messageAllWorkers(cluster, {
|
|
event: 'connect_ws'
|
|
});
|
|
},
|
|
'ingestor_block_report': () => {
|
|
// console.log(data);
|
|
},
|
|
'save_abi': (msg: any) => {
|
|
this.total_abis++;
|
|
if (msg.live_mode === 'true') {
|
|
hLog(`deserializer ${msg.worker_id} received new abi! propagating changes to other workers...`);
|
|
for (const worker of this.workerMap) {
|
|
if (worker.worker_role === 'deserializer' && worker.worker_id !== parseInt(msg.worker_id)) {
|
|
worker.wref.send({
|
|
event: 'update_abi',
|
|
abi: msg.data
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'update_init_block': (msg: any) => {
|
|
if (msg.block_num) {
|
|
this.lastAssignedBlock = msg.block_num;
|
|
this.total_range = this.head - msg.block_num;
|
|
}
|
|
},
|
|
'repair_reader_ready': () => {
|
|
this.sendPendingRepairRanges();
|
|
},
|
|
'kill_worker': (msg: any) => {
|
|
for (let workersKey in cluster.workers) {
|
|
const w = cluster.workers[workersKey];
|
|
if (w.id === parseInt(msg.id)) {
|
|
const idx = this.workerMap.findIndex(value => value.worker_id === w.id);
|
|
this.workerMap.splice(idx, 1);
|
|
w.kill();
|
|
}
|
|
}
|
|
},
|
|
'completed': (msg: any) => {
|
|
if (this.repairReader && msg.id === this.repairReader.worker_id.toString()) {
|
|
console.log('Repair completed!', msg);
|
|
this.sendPendingRepairRanges();
|
|
} else {
|
|
this.activeReadersCount--;
|
|
if (this.activeReadersCount < this.max_readers && this.lastAssignedBlock < this.head && this.allowMoreReaders) {
|
|
// Assign next range
|
|
const start = this.lastAssignedBlock;
|
|
let end = this.lastAssignedBlock + this.maxBatchSize;
|
|
// Check if we are not exceeding the head block
|
|
if (end > this.head) {
|
|
end = this.head;
|
|
}
|
|
this.lastAssignedBlock = end;
|
|
this.activeReadersCount++;
|
|
messageAllWorkers(cluster, {
|
|
event: 'new_range',
|
|
target: msg.id,
|
|
data: {
|
|
first_block: start,
|
|
last_block: end
|
|
}
|
|
});
|
|
} else {
|
|
if (this.lastAssignedBlock >= this.head) {
|
|
hLog(`Parallel readers finished the requested range`);
|
|
const readers = this.workerMap.filter(value => value.worker_role === 'reader');
|
|
this.workerMap = this.workerMap.filter(value => value.worker_role !== 'reader');
|
|
readers.forEach(value => value.wref.kill());
|
|
// for (let hyperionWorkerDef of this.workerMap) {
|
|
// if (hyperionWorkerDef.worker_role === 'reader') {
|
|
// hyperionWorkerDef.wref.kill();
|
|
// }
|
|
// }
|
|
// for (let workersKey in cluster.workers) {
|
|
// const w = cluster.workers[workersKey];
|
|
// console.log(w);
|
|
// if (w.id === parseInt(msg.id)) {
|
|
// const idx = this.workerMap.findIndex(value => value.worker_id === w.id);
|
|
// this.workerMap.splice(idx, 1);
|
|
// w.kill();
|
|
// }
|
|
// }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'add_index': (msg: any) => {
|
|
this.indexedObjects += msg.size;
|
|
},
|
|
'ds_report': (msg: any) => {
|
|
if (msg.actions) {
|
|
this.deserializedActions += msg.actions;
|
|
}
|
|
if (msg.deltas) {
|
|
this.deserializedDeltas += msg.deltas;
|
|
}
|
|
},
|
|
'ds_error': (msg: any) => {
|
|
const str = JSON.stringify(msg.data);
|
|
this.dsErrorStream.write(str + '\n');
|
|
},
|
|
'read_block': (msg: any) => {
|
|
if (!msg.live) {
|
|
this.pushedBlocks++;
|
|
} else {
|
|
this.livePushedBlocks++;
|
|
}
|
|
},
|
|
// 'ds_ready': (msg: any) => {
|
|
// hLog(msg);
|
|
// },
|
|
'contract_usage_report': (msg: any) => {
|
|
if (msg.data) {
|
|
this.totalContractHits += msg.total_hits;
|
|
for (const contract in msg.data) {
|
|
if (msg.data.hasOwnProperty(contract)) {
|
|
if (this.globalUsageMap[contract]) {
|
|
this.globalUsageMap[contract][0] += msg.data[contract];
|
|
} else {
|
|
this.globalUsageMap[contract] = [msg.data[contract], 0, []];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'lib_update': (msg: any) => {
|
|
// publish LIB to hub
|
|
if (msg.data) {
|
|
|
|
// save local lib
|
|
this.lastIrreversibleBlock = msg.data.block_num;
|
|
|
|
this.revBlockArray = this.revBlockArray.filter(item => item.num > msg.data.block_num);
|
|
if (this.conf.hub && this.conf.hub.inform_url) {
|
|
this.hub.emit('hyp_ev', {e: 'lib', d: msg.data});
|
|
}
|
|
// forward LIB to streaming router
|
|
if (this.conf.features.streaming.enable) {
|
|
debugLog(`Live Reader reported LIB update: ${msg.data.block_num} | ${msg.data.block_id}`);
|
|
this.wsRouterWorker.send(msg);
|
|
}
|
|
}
|
|
},
|
|
'included_trx': (msg: any) => {
|
|
if (this.ioRedisClient) {
|
|
this.ioRedisClient.set(msg.trx_id, JSON.stringify({
|
|
status: 'executed',
|
|
b: msg.block_num,
|
|
s: msg.signatures,
|
|
a: msg.root_act
|
|
}), "EX", 3600).catch(console.log);
|
|
}
|
|
},
|
|
'fork_event': async (msg: any) => {
|
|
const forkedBlocks = this.revBlockArray.filter(item => item.num >= msg.data.starting_block && item.id !== msg.data.new_id);
|
|
this.revBlockArray = this.revBlockArray.filter(item => item.num < msg.data.starting_block);
|
|
if (this.ioRedisClient) {
|
|
for (const block of forkedBlocks) {
|
|
if (block.tx.length > 0) {
|
|
for (const trxId of block.tx) {
|
|
await this.ioRedisClient.set(trxId, JSON.stringify({status: 'forked'}), "EX", 600);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.emitAlert('fork', msg);
|
|
if (msg.data && this.conf.features.streaming.enable) {
|
|
msg.data.forkedBlocks = forkedBlocks;
|
|
this.wsRouterWorker.send(msg);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
printMode() {
|
|
const package_json = JSON.parse(readFileSync('./package.json').toString());
|
|
hLog(`--------- Hyperion Indexer ${package_json.version} ---------`);
|
|
hLog(`Using parser version ${this.conf.settings.parser}`);
|
|
hLog(`Chain: ${this.conf.settings.chain}`);
|
|
if (this.conf.indexer.abi_scan_mode) {
|
|
hLog('\n-------------------\n ABI SCAN MODE \n-------------------');
|
|
} else {
|
|
hLog('\n---------------\n INDEXING MODE \n---------------');
|
|
}
|
|
}
|
|
|
|
private async purgeQueues() {
|
|
if (this.conf.indexer.purge_queues) {
|
|
if (this.conf.indexer.disable_reading) {
|
|
hLog('Cannot purge queue with disabled reading! Exiting now!');
|
|
process.exit(1);
|
|
} else {
|
|
await this.manager.purgeQueues();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async verifyIngestClients() {
|
|
for (const ingestClient of this.manager.ingestClients) {
|
|
try {
|
|
const ping_response: ApiResponse = await ingestClient.ping();
|
|
if (ping_response.body) {
|
|
hLog(`Ingest client ready at ${ping_response.meta.connection.id}`);
|
|
}
|
|
} catch (e) {
|
|
hLog(e.message);
|
|
hLog('Failed to connect to one of the ingestion nodes. Please verify the connections.json file');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
private addStateTables(indicesList, index_queues) {
|
|
const queue_prefix = this.conf.settings.chain;
|
|
const index_queue_prefix = queue_prefix + ':index';
|
|
const table_feats = this.conf.features.tables;
|
|
|
|
if (table_feats.proposals) {
|
|
indicesList.push({name: 'tableProposals', type: 'table-proposals'});
|
|
index_queues.push({type: 'table-proposals', name: index_queue_prefix + "_table_proposals"});
|
|
}
|
|
|
|
if (table_feats.accounts) {
|
|
indicesList.push({name: 'tableAccounts', type: 'table-accounts'});
|
|
index_queues.push({type: 'table-accounts', name: index_queue_prefix + "_table_accounts"});
|
|
}
|
|
|
|
if (table_feats.voters) {
|
|
indicesList.push({name: 'tableVoters', type: 'table-voters'});
|
|
index_queues.push({type: 'table-voters', name: index_queue_prefix + "_table_voters"});
|
|
}
|
|
|
|
if (table_feats.delband) {
|
|
indicesList.push({name: 'tableDelband', type: 'table-delband'});
|
|
index_queues.push({type: 'table-delband', name: index_queue_prefix + "_table_delband"});
|
|
}
|
|
|
|
// special dynamic table mapper
|
|
// indicesList.push({name: 'dynamicTable', type: 'dynamic-table'});
|
|
index_queues.push({type: 'dynamic-table', name: index_queue_prefix + "_dynamic"});
|
|
|
|
}
|
|
|
|
private async getCurrentSchedule() {
|
|
try {
|
|
this.currentSchedule = await this.rpc.get_producer_schedule();
|
|
if (!this.currentSchedule) {
|
|
console.error('empty producer schedule, something went wrong!');
|
|
process.exit(1);
|
|
} else {
|
|
if (this.currentSchedule.active) {
|
|
this.activeSchedule = this.currentSchedule.active;
|
|
}
|
|
if (this.currentSchedule.pending) {
|
|
this.pendingSchedule = this.currentSchedule.pending;
|
|
}
|
|
if (this.currentSchedule.proposed) {
|
|
this.proposedSchedule = this.currentSchedule.proposed;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('failed to connect to api');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
private async applyUpdateScript() {
|
|
const script_status = await this.client.putScript({
|
|
id: "updateByBlock",
|
|
body: {
|
|
script: {
|
|
lang: "painless",
|
|
source: `
|
|
boolean valid = false;
|
|
if(ctx._source.block_num != null) {
|
|
if(params.block_num < ctx._source.block_num) {
|
|
ctx['op'] = 'none';
|
|
valid = false;
|
|
} else {
|
|
valid = true;
|
|
}
|
|
} else {
|
|
valid = true;
|
|
}
|
|
if(valid == true) {
|
|
for (entry in params.entrySet()) {
|
|
if(entry.getValue() != null) {
|
|
ctx._source[entry.getKey()] = entry.getValue();
|
|
} else {
|
|
ctx._source.remove(entry.getKey());
|
|
}
|
|
}
|
|
}
|
|
`
|
|
}
|
|
}
|
|
});
|
|
if (!script_status.body['acknowledged']) {
|
|
hLog('Failed to load script updateByBlock. Aborting!');
|
|
process.exit(1);
|
|
} else {
|
|
hLog('Painless Update Script loaded!');
|
|
}
|
|
}
|
|
|
|
private async addLifecyclePolicies(indexConfig) {
|
|
if (indexConfig.ILPs) {
|
|
for (const ILP of indexConfig.ILPs) {
|
|
try {
|
|
await this.client.ilm.getLifecycle({
|
|
policy: ILP.policy
|
|
});
|
|
} catch {
|
|
try {
|
|
const ilm_status: ApiResponse = await this.client.ilm.putLifecycle(ILP);
|
|
if (!ilm_status.body['acknowledged']) {
|
|
hLog(`Failed to create ILM Policy`);
|
|
}
|
|
} catch (e) {
|
|
hLog(`[FATAL] :: Failed to create ILM Policy`);
|
|
hLog(e);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async appendExtraMappings(indexConfig) {
|
|
// Modify mappings
|
|
for (const exM of this.mLoader.extraMappings) {
|
|
if (exM['action']) {
|
|
for (const key in exM['action']) {
|
|
if (exM['action'].hasOwnProperty(key)) {
|
|
indexConfig['action']['mappings']['properties'][key] = exM['action'][key];
|
|
hLog(`Action Mapping added for ${key}`);
|
|
}
|
|
}
|
|
}
|
|
if (exM['delta']) {
|
|
for (const key in exM['delta']) {
|
|
if (exM['delta'].hasOwnProperty(key)) {
|
|
indexConfig['delta']['mappings']['properties'][key] = exM['delta'][key];
|
|
hLog(`Delta Mapping added for ${key}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async updateIndexTemplates(indicesList: { name: string, type: string }[], indexConfig) {
|
|
hLog(`Updating index templates for ${this.conf.settings.chain}...`);
|
|
let updateCounter = 0;
|
|
for (const index of indicesList) {
|
|
try {
|
|
if (indexConfig[index.name]) {
|
|
const creation_status: ApiResponse = await this.client['indices'].putTemplate({
|
|
name: `${this.conf.settings.chain}-${index.type}`,
|
|
body: indexConfig[index.name]
|
|
});
|
|
if (!creation_status || !creation_status['body']['acknowledged']) {
|
|
hLog(`Failed to create template: ${this.conf.settings.chain}-${index}`);
|
|
} else {
|
|
updateCounter++;
|
|
debugLog(`${this.conf.settings.chain}-${index.type} template updated!`);
|
|
}
|
|
} else {
|
|
hLog(`${index.name} template not found!`);
|
|
}
|
|
} catch (e) {
|
|
hLog(`[FATAL] ${e.message}`);
|
|
if (e.meta) {
|
|
hLog(e.meta.body);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
hLog(`${updateCounter} index templates updated`);
|
|
}
|
|
|
|
private async createIndices(indicesList: { name: string, type: string }[]) {
|
|
// Create indices
|
|
const queue_prefix = this.conf.settings.chain;
|
|
if (this.conf.settings.index_version) {
|
|
|
|
// Start with version=1 if none is defined
|
|
let version = 'v1';
|
|
if (this.conf.settings.index_version) {
|
|
version = this.conf.settings.index_version;
|
|
}
|
|
|
|
for (const index of indicesList) {
|
|
|
|
// check for existing first index in the sequence
|
|
const new_index = `${queue_prefix}-${index.type}-${version}-000001`;
|
|
const exists = await this.client.indices.exists({index: new_index});
|
|
|
|
if (!exists.body) {
|
|
|
|
// create index
|
|
try {
|
|
hLog(`Creating index ${new_index}...`);
|
|
await this.client.indices.create({
|
|
index: new_index
|
|
});
|
|
} catch (e) {
|
|
hLog(e);
|
|
process.exit(1);
|
|
}
|
|
|
|
// create alias pointing to the new index
|
|
try {
|
|
hLog(`Creating alias ${queue_prefix}-${index.type} >> ${new_index}`);
|
|
await this.client.indices.putAlias({
|
|
index: new_index,
|
|
name: `${queue_prefix}-${index.type}`
|
|
});
|
|
} catch (e) {
|
|
hLog(e);
|
|
process.exit(1);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for indexes
|
|
for (const index of indicesList) {
|
|
const status = await this.client.indices.existsAlias({
|
|
name: `${queue_prefix}-${index.type}`
|
|
});
|
|
if (!status) {
|
|
hLog('Alias ' + `${queue_prefix}-${index.type}` + ' not found! Aborting!');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async defineBlockRange() {
|
|
// Define block range
|
|
if (this.conf.indexer.start_on !== 0) {
|
|
this.starting_block = this.conf.indexer.start_on;
|
|
// Check last indexed block again
|
|
if (!this.conf.indexer.rewrite) {
|
|
let lastIndexedBlockOnRange;
|
|
if (this.conf.indexer.abi_scan_mode) {
|
|
hLog(`Last indexed ABI: ${this.lastIndexedABI}`);
|
|
this.starting_block = this.lastIndexedABI;
|
|
} else {
|
|
if (this.conf.features.index_deltas) {
|
|
lastIndexedBlockOnRange = await getLastIndexedBlockByDeltaFromRange(this.client, this.chain, this.starting_block, this.head);
|
|
} else {
|
|
lastIndexedBlockOnRange = await getLastIndexedBlockFromRange(this.client, this.chain, this.starting_block, this.head);
|
|
}
|
|
if (lastIndexedBlockOnRange > this.starting_block) {
|
|
hLog('WARNING! Data present on target range!');
|
|
hLog('Changing initial block num. Use REWRITE = true to bypass.');
|
|
this.starting_block = lastIndexedBlockOnRange;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (this.conf.indexer.rewrite) {
|
|
hLog(`WARNING! "indexer.rewrite: true" If you want to reindex from a specific block, "indexer.start_on" must be greater than zero!`);
|
|
hLog(`The indexer will start from ${this.starting_block}`);
|
|
}
|
|
// Auto Mode
|
|
if (this.conf.indexer.abi_scan_mode) {
|
|
hLog(`Last indexed ABI: ${this.lastIndexedABI}`);
|
|
this.starting_block = this.lastIndexedABI;
|
|
}
|
|
}
|
|
// print results
|
|
hLog(' |>> First Block: ' + this.starting_block);
|
|
hLog(' >>| Last Block: ' + this.head);
|
|
}
|
|
|
|
private static printWorkerMap(wmp) {
|
|
hLog('---------------- PROPOSED WORKER LIST ----------------------');
|
|
for (const w of wmp) {
|
|
const str = [];
|
|
for (const key in w) {
|
|
if (w.hasOwnProperty(key) && key !== 'worker_id') {
|
|
switch (key) {
|
|
case 'worker_role': {
|
|
str.push(`Role: ${w[key]}`);
|
|
break;
|
|
}
|
|
case 'worker_queue': {
|
|
str.push(`Queue Name: ${w[key]}`);
|
|
break;
|
|
}
|
|
case 'first_block': {
|
|
str.push(`First Block: ${w[key]}`);
|
|
break;
|
|
}
|
|
case 'last_block': {
|
|
str.push(`Last Block: ${w[key]}`);
|
|
break;
|
|
}
|
|
case 'live_mode': {
|
|
str.push(`Live Mode: ${w[key]}`);
|
|
break;
|
|
}
|
|
case 'type': {
|
|
str.push(`Index Type: ${w[key]}`);
|
|
break;
|
|
}
|
|
case 'worker_last_processed_block': {
|
|
str.push(`Last Processed Block: ${w[key]}`);
|
|
break;
|
|
}
|
|
case 'queue': {
|
|
str.push(`Indexing Queue: ${w[key]}`);
|
|
break;
|
|
}
|
|
case 'distribution': {
|
|
if (!!w[key]) {
|
|
str.push(`Dist Map: ${Object.keys(w[key]).length} indices`);
|
|
} else {
|
|
str.push(`Dist Map: N/A`);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
str.push(`${key}: ${w[key]}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
hLog(`Worker ID: ${w.worker_id} \t ${str.join(" | ")}`)
|
|
}
|
|
hLog('--------------------------------------------------');
|
|
}
|
|
|
|
private async setupDeserializers() {
|
|
for (let i = 0; i < this.conf.scaling.ds_queues; i++) {
|
|
for (let j = 0; j < this.conf.scaling.ds_threads; j++) {
|
|
this.addWorker({
|
|
worker_role: 'deserializer',
|
|
worker_queue: this.chain + ':blocks' + ":" + (i + 1),
|
|
live_mode: 'false'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async getLastBlock(index_name) {
|
|
const lastBlockSearch = await this.manager.elasticsearchClient.search({
|
|
index: index_name,
|
|
size: 1,
|
|
_source: "block_num",
|
|
body: {
|
|
"query": {"match_all": {}},
|
|
sort: [{"block_num": {"order": "desc"}}]
|
|
}
|
|
});
|
|
if (lastBlockSearch.body.hits.hits[0]) {
|
|
return lastBlockSearch.body.hits.hits[0]._source.block_num;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// async getFirstBlock(index_name) {
|
|
// const firstBlockSearch = await this.manager.elasticsearchClient.search({
|
|
// index: index_name,
|
|
// size: 1,
|
|
// _source: "block_num",
|
|
// body: {
|
|
// "query": {"match_all": {}},
|
|
// sort: [{"block_num": {"order": "asc"}}]
|
|
// }
|
|
// });
|
|
// if (firstBlockSearch.body.hits.hits[0]) {
|
|
// return firstBlockSearch.body.hits.hits[0]._source.block_num;
|
|
// } else {
|
|
// return null;
|
|
// }
|
|
// }
|
|
|
|
private async setupIndexers() {
|
|
|
|
// const indexDist = {
|
|
// action: [],
|
|
// delta: []
|
|
// };
|
|
|
|
// hLog(`Loading indices...`);
|
|
|
|
// if (!this.conf.settings.bypass_index_map) {
|
|
// const getIndicesResponse = await this.manager.elasticsearchClient.cat.indices({
|
|
// index: this.chain + "-*",
|
|
// format: 'json'
|
|
// });
|
|
//
|
|
// if (getIndicesResponse && getIndicesResponse.statusCode === 200 && getIndicesResponse.body) {
|
|
// const indices = getIndicesResponse.body;
|
|
// const actionIndices = indices.filter(k => k.index.startsWith(this.chain + "-action"));
|
|
// for (const actionIndex of actionIndices) {
|
|
// const last_block = await this.getLastBlock(actionIndex.index);
|
|
// const first_block = await this.getFirstBlock(actionIndex.index);
|
|
// hLog(`ActionIndex: ${actionIndex.index} | First: ${first_block} | Last: ${last_block}`);
|
|
// indexDist.action.push({
|
|
// index: actionIndex.index,
|
|
// first_block,
|
|
// last_block
|
|
// });
|
|
// }
|
|
//
|
|
// const deltaIndices = indices.filter(k => k.index.startsWith(this.chain + "-delta"));
|
|
// for (const deltaIndex of deltaIndices) {
|
|
// const last_block = await this.getLastBlock(deltaIndex.index);
|
|
// const first_block = await this.getFirstBlock(deltaIndex.index);
|
|
// hLog(`DeltaIndex: ${deltaIndex.index} | First: ${first_block} | Last: ${last_block}`);
|
|
// indexDist.delta.push({
|
|
// index: deltaIndex.index,
|
|
// first_block,
|
|
// last_block
|
|
// });
|
|
// }
|
|
// } else {
|
|
// console.log(JSON.stringify(getIndicesResponse, null, 2));
|
|
// hLog(`Failed to load all indices!`);
|
|
// process.exit();
|
|
// }
|
|
// }
|
|
|
|
|
|
if (this.conf.indexer.rewrite || this.conf.settings.preview) {
|
|
hLog(`Indexer rewrite enabled (${this.conf.indexer.start_on} - ${this.conf.indexer.stop_on})`);
|
|
}
|
|
|
|
|
|
let qIdx = 0;
|
|
this.IndexingQueues.forEach((q) => {
|
|
let n = this.conf.scaling.indexing_queues;
|
|
qIdx = 0;
|
|
if (q.type === 'action' || q.type === 'delta') {
|
|
n = this.conf.scaling.ad_idx_queues;
|
|
} else if (q.type === 'dynamic-table') {
|
|
n = this.conf.scaling.dyn_idx_queues;
|
|
} else if (q.type === 'abi') {
|
|
n = 1;
|
|
}
|
|
for (let i = 0; i < n; i++) {
|
|
this.addWorker({
|
|
worker_role: 'ingestor',
|
|
queue: q.name + ":" + (qIdx + 1),
|
|
type: q.type
|
|
});
|
|
qIdx++;
|
|
}
|
|
});
|
|
}
|
|
|
|
private async setupStreaming() {
|
|
const _streaming = this.conf.features.streaming;
|
|
if (_streaming.enable) {
|
|
this.addWorker({worker_role: 'router'});
|
|
if (_streaming.deltas) hLog('Delta streaming enabled!');
|
|
if (_streaming.traces) hLog('Action trace streaming enabled!');
|
|
if (!_streaming.deltas && !_streaming.traces) {
|
|
hLog('WARNING! Streaming is enabled without any datatype,' +
|
|
'please enable STREAM_TRACES and/or STREAM_DELTAS');
|
|
}
|
|
}
|
|
}
|
|
|
|
private addWorker(def: HyperionWorkerDef): HyperionWorkerDef {
|
|
this.worker_index++;
|
|
def.worker_id = this.worker_index;
|
|
def.failures = 0;
|
|
this.workerMap.push(def);
|
|
return def;
|
|
}
|
|
|
|
private async setupDSPool() {
|
|
for (let i = 0; i < this.conf.scaling.ds_pool_size; i++) {
|
|
this.addWorker({
|
|
worker_role: 'ds_pool_worker',
|
|
local_id: i + 1
|
|
});
|
|
}
|
|
}
|
|
|
|
private async waitForLaunch(): Promise<void> {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
hLog(`Use "pm2 trigger ${pm2io.getConfig()['module_name']} start" to start the indexer now or restart without preview mode.`);
|
|
|
|
const idleTimeout = setTimeout(() => {
|
|
hLog('No command received after 10 minutes.');
|
|
hLog('Exiting now! Disable the PREVIEW mode to continue.');
|
|
process.exit(1);
|
|
}, 60000 * 10);
|
|
|
|
pm2io.action('start', (reply) => {
|
|
resolve();
|
|
reply({ack: true});
|
|
clearTimeout(idleTimeout);
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
setupDSElogs() {
|
|
const logPath = path.join(path.resolve(), 'logs', this.chain);
|
|
if (!existsSync(logPath)) mkdirSync(logPath, {recursive: true});
|
|
const dsLogFileName = (new Date().toISOString()) + "_ds_err_" + this.starting_block + "_" + this.head + ".log";
|
|
const dsErrorsLog = logPath + '/' + dsLogFileName;
|
|
if (existsSync(dsErrorsLog)) unlinkSync(dsErrorsLog);
|
|
const symbolicLink = logPath + '/deserialization_errors.log';
|
|
if (existsSync(symbolicLink)) unlinkSync(symbolicLink);
|
|
symlinkSync(dsLogFileName, symbolicLink);
|
|
this.dsErrorStream = createWriteStream(dsErrorsLog, {flags: 'a'});
|
|
hLog(`📣️ Deserialization errors are being logged in:\n[${symbolicLink}]`);
|
|
this.dsErrorStream.write(`begin ${this.chain} error logs\n`);
|
|
}
|
|
|
|
onLiveBlock(msg) {
|
|
if (this.proposedSchedule && this.proposedSchedule.version) {
|
|
if (msg.schedule_version >= this.proposedSchedule.version) {
|
|
hLog(`Active producers changed!`);
|
|
this.printActiveProds();
|
|
this.activeSchedule = this.proposedSchedule;
|
|
this.printActiveProds();
|
|
this.proposedSchedule = null;
|
|
this.producedBlocks = {};
|
|
}
|
|
}
|
|
if ((msg.block_num === this.lastProducedBlockNum + 1) || this.lastProducedBlockNum === 0) {
|
|
const prod = msg.producer;
|
|
if (this.producedBlocks[prod]) {
|
|
this.producedBlocks[prod]++;
|
|
} else {
|
|
this.producedBlocks[prod] = 1;
|
|
}
|
|
if (this.lastProducer !== prod) {
|
|
this.handoffCounter++;
|
|
if (this.lastProducer && this.handoffCounter > 2) {
|
|
const activeProds = this.activeSchedule.producers;
|
|
const newIdx = activeProds.findIndex(p => p['producer_name'] === prod) + 1;
|
|
const oldIdx = activeProds.findIndex(p => p['producer_name'] === this.lastProducer) + 1;
|
|
if ((newIdx === oldIdx + 1) || (newIdx === 1 && oldIdx === activeProds.length)) {
|
|
if (this.conf.settings.bp_logs) {
|
|
hLog(`[${msg.block_num}] producer handoff: ${this.lastProducer} [${oldIdx}] -> ${prod} [${newIdx}]`);
|
|
}
|
|
} else {
|
|
let cIdx = oldIdx + 1;
|
|
while (cIdx !== newIdx) {
|
|
try {
|
|
if (activeProds[cIdx - 1]) {
|
|
const missingProd = activeProds[cIdx - 1]['producer_name'];
|
|
this.reportMissedBlocks(missingProd, this.lastProducedBlockNum, 12);
|
|
if (this.missedRounds[missingProd]) {
|
|
this.missedRounds[missingProd]++;
|
|
} else {
|
|
this.missedRounds[missingProd] = 1;
|
|
}
|
|
hLog(`${missingProd} missed a round [${this.missedRounds[missingProd]}]`);
|
|
}
|
|
} catch (e) {
|
|
hLog(activeProds);
|
|
hLog(e);
|
|
}
|
|
cIdx++;
|
|
if (cIdx === activeProds.length) {
|
|
cIdx = 0;
|
|
}
|
|
}
|
|
}
|
|
if (this.producedBlocks[this.lastProducer]) {
|
|
if (this.producedBlocks[this.lastProducer] < 12) {
|
|
const _size = 12 - this.producedBlocks[this.lastProducer];
|
|
this.reportMissedBlocks(this.lastProducer, this.lastProducedBlockNum, _size);
|
|
}
|
|
}
|
|
this.producedBlocks[this.lastProducer] = 0;
|
|
}
|
|
this.lastProducer = prod;
|
|
}
|
|
if (this.conf.settings.bp_logs) {
|
|
if (this.proposedSchedule) {
|
|
hLog(`received block ${msg.block_num} from ${prod} [${this.activeSchedule.version} >> ${this.proposedSchedule.version}]`);
|
|
} else {
|
|
hLog(`received block ${msg.block_num} from ${prod} [${this.activeSchedule.version}]`);
|
|
}
|
|
}
|
|
}
|
|
this.lastProducedBlockNum = msg.block_num;
|
|
}
|
|
|
|
handleMessage(msg) {
|
|
if (this.shouldCountIPCMessages) {
|
|
this.totalMessages++;
|
|
}
|
|
|
|
if (this.msgHandlerMap[msg.event]) {
|
|
this.msgHandlerMap[msg.event](msg);
|
|
}
|
|
}
|
|
|
|
private async setupReaders() {
|
|
// Setup Readers
|
|
|
|
this.lastAssignedBlock = this.starting_block;
|
|
|
|
this.activeReadersCount = 0;
|
|
|
|
// Setup parallel readers unless explicitly disabled
|
|
if (!this.conf.indexer.live_only_mode) {
|
|
// console.log(this.activeReadersCount, this.max_readers, this.lastAssignedBlock, this.head)
|
|
while (this.activeReadersCount < this.max_readers && this.lastAssignedBlock < this.head) {
|
|
const start = this.lastAssignedBlock;
|
|
let end = this.lastAssignedBlock + this.maxBatchSize;
|
|
if (end > this.head) {
|
|
end = this.head;
|
|
}
|
|
this.lastAssignedBlock += this.maxBatchSize;
|
|
this.addWorker({
|
|
worker_role: 'reader',
|
|
first_block: start,
|
|
last_block: end
|
|
});
|
|
this.activeReadersCount++;
|
|
hLog(`Setting parallel reader [${this.worker_index}] from block ${start} to ${end}`);
|
|
}
|
|
}
|
|
|
|
// Setup live workers
|
|
if (this.conf.indexer.live_reader) {
|
|
const _head = this.chain_data.head_block_num;
|
|
hLog(`Setting live reader at head = ${_head}`);
|
|
|
|
// live block reader
|
|
this.liveReader = this.addWorker({
|
|
worker_role: 'continuous_reader',
|
|
worker_last_processed_block: _head,
|
|
ws_router: ''
|
|
});
|
|
|
|
// live deserializers
|
|
for (let j = 0; j < this.conf.scaling.ds_threads; j++) {
|
|
this.addWorker({
|
|
worker_role: 'deserializer',
|
|
worker_queue: this.chain + ':live_blocks',
|
|
live_mode: 'true'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private reportMissedBlocks(missingProd: any, lastProducedBlockNum: number, size: number) {
|
|
hLog(`${missingProd} missed ${size} ${size === 1 ? "block" : "blocks"} after ${lastProducedBlockNum}`);
|
|
const _body = {
|
|
type: 'missed_blocks',
|
|
'@timestamp': new Date().toISOString(),
|
|
'missed_blocks': {
|
|
'producer': missingProd,
|
|
'last_block': lastProducedBlockNum,
|
|
'size': size,
|
|
'schedule_version': this.activeSchedule.version
|
|
}
|
|
};
|
|
this.client.index({
|
|
index: this.chain + '-logs-' + this.conf.settings.index_version,
|
|
body: _body
|
|
}).catch(hLog);
|
|
}
|
|
|
|
// private startRepairMode() {
|
|
// let doctorStarted = false;
|
|
// let doctorId = 0;
|
|
// doctor.run(this.missingRanges as any).then(() => {
|
|
// hLog('repair completed!');
|
|
// });
|
|
// setInterval(() => {
|
|
// if (this.missingRanges.length > 0 && !doctorStarted) {
|
|
// doctorStarted = true;
|
|
// hLog('repair worker launched');
|
|
// const range_data = this.missingRanges.shift();
|
|
// this.worker_index++;
|
|
// const def = {
|
|
// worker_id: this.worker_index,
|
|
// worker_role: 'reader',
|
|
// first_block: range_data.start,
|
|
// last_block: range_data.end
|
|
// };
|
|
// const self = cluster.fork(def);
|
|
// doctorId = def.worker_id;
|
|
// hLog('repair id =', doctorId);
|
|
// self.on('message', (msg) => {
|
|
// this.handleMessage(msg);
|
|
// });
|
|
// } else {
|
|
// if (this.missingRanges.length > 0 && this.doctorIdle) {
|
|
// const range_data = this.missingRanges.shift();
|
|
// messageAllWorkers(cluster, {
|
|
// event: 'new_range',
|
|
// target: doctorId.toString(),
|
|
// data: {
|
|
// first_block: range_data.start,
|
|
// last_block: range_data.end
|
|
// }
|
|
// });
|
|
// }
|
|
// }
|
|
// }, 1000);
|
|
// }
|
|
|
|
updateWorkerAssignments() {
|
|
const pool_size = this.conf.scaling.ds_pool_size;
|
|
const worker_max_pct = 1 / pool_size;
|
|
|
|
const worker_shares = {};
|
|
for (let i = 0; i < pool_size; i++) {
|
|
worker_shares[i] = 0.0;
|
|
}
|
|
|
|
const worker_counters = {};
|
|
for (let i = 0; i < pool_size; i++) {
|
|
worker_counters[i] = 0;
|
|
}
|
|
|
|
const propWorkerMap = {};
|
|
|
|
// phase 1 - calculate usage
|
|
for (const code in this.globalUsageMap) {
|
|
const _pct = this.globalUsageMap[code][0] / this.totalContractHits;
|
|
let used_pct = 0;
|
|
const proposedWorkers = [];
|
|
for (let i = 0; i < pool_size; i++) {
|
|
if (worker_shares[i] < worker_max_pct) {
|
|
const rem_pct = (_pct - used_pct);
|
|
if (rem_pct === 0) {
|
|
break;
|
|
}
|
|
if (rem_pct > worker_max_pct) {
|
|
used_pct += (worker_max_pct - worker_shares[i]);
|
|
worker_shares[i] = worker_max_pct;
|
|
} else {
|
|
if (worker_shares[i] + rem_pct > worker_max_pct) {
|
|
used_pct += (worker_max_pct - worker_shares[i]);
|
|
worker_shares[i] = worker_max_pct;
|
|
} else {
|
|
used_pct += rem_pct;
|
|
worker_shares[i] += rem_pct;
|
|
}
|
|
}
|
|
proposedWorkers.push(i);
|
|
worker_counters[i]++;
|
|
}
|
|
}
|
|
propWorkerMap[code] = proposedWorkers;
|
|
this.globalUsageMap[code][1] = _pct;
|
|
}
|
|
|
|
// phase 2 - re-balance
|
|
const totalContracts = Object.keys(this.globalUsageMap).length;
|
|
const desiredCodeCount = totalContracts / pool_size;
|
|
const ignoreList = [];
|
|
for (let i = 0; i < pool_size; i++) {
|
|
if (worker_counters[i] > desiredCodeCount * 1.1) {
|
|
ignoreList.push(i);
|
|
}
|
|
}
|
|
|
|
for (const code in propWorkerMap) {
|
|
if (propWorkerMap[code].length === 1) {
|
|
const oldVal = propWorkerMap[code][0];
|
|
if (ignoreList.includes(oldVal)) {
|
|
for (let i = 0; i < pool_size; i++) {
|
|
if (worker_counters[i] < desiredCodeCount) {
|
|
propWorkerMap[code] = [i];
|
|
worker_counters[i]++;
|
|
worker_counters[oldVal]--;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// phase 3 - assign
|
|
for (const code in this.globalUsageMap) {
|
|
if (JSON.stringify(this.globalUsageMap[code][2]) !== JSON.stringify(propWorkerMap[code])) {
|
|
// hLog(this.globalUsageMap[code][2], ">>", proposedWorkers);
|
|
propWorkerMap[code].forEach(w => {
|
|
const idx = this.globalUsageMap[code][2].indexOf(w);
|
|
if (idx !== -1) {
|
|
this.globalUsageMap[code][2].splice(idx, 1);
|
|
} else {
|
|
// hLog(`Worker ${w} assigned to ${code}`);
|
|
}
|
|
});
|
|
this.globalUsageMap[code][2].forEach(w_id => {
|
|
// hLog(`>>>> Worker ${this.globalUsageMap[code][2]} removed from ${code}!`);
|
|
if (this.dsPoolMap.has(w_id)) {
|
|
this.dsPoolMap.get(w_id).send({
|
|
event: "remove_contract",
|
|
contract: code
|
|
});
|
|
}
|
|
});
|
|
this.globalUsageMap[code][2] = propWorkerMap[code];
|
|
}
|
|
}
|
|
}
|
|
|
|
private startContractMonitoring() {
|
|
if (!this.contractMonitoringInterval) {
|
|
// Monitor Global Contract Usage
|
|
this.contractMonitoringInterval = setInterval(() => {
|
|
|
|
// const t0 = process.hrtime.bigint();
|
|
this.updateWorkerAssignments();
|
|
// const t1 = process.hrtime.bigint();
|
|
|
|
// hLog('----------- Usage Report ----------');
|
|
// hLog(`Total Hits: ${this.totalContractHits}`);
|
|
// hLog(`Update time: ${parseInt((t1 - t0).toString()) / 1000000} ms`);
|
|
// hLog(this.globalUsageMap);
|
|
// hLog('-----------------------------------');
|
|
|
|
// update on deserializers
|
|
for (const w of this.workerMap) {
|
|
if (w.worker_role === 'deserializer') {
|
|
if (w.wref && w.wref.isConnected()) {
|
|
w.wref.send({
|
|
event: 'update_pool_map',
|
|
data: this.globalUsageMap
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// clearUsageMap();
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
private sendToRole(role: string, payload: any) {
|
|
this.workerMap.filter(value => value.worker_role === role).forEach(value => value.wref.send(payload));
|
|
}
|
|
|
|
private pauseReaders() {
|
|
if (!this.readingPaused) {
|
|
this.sendToRole('reader', {event: 'pause'});
|
|
this.readingPaused = true;
|
|
}
|
|
}
|
|
|
|
private resumeReaders() {
|
|
if (this.readingPaused) {
|
|
this.sendToRole('reader', {event: 'resume'});
|
|
this.readingPaused = false;
|
|
}
|
|
}
|
|
|
|
private async checkQueues(autoscaleConsumers, limit) {
|
|
const testedQueues = new Set();
|
|
let aboveLimit = false;
|
|
let canResume = true;
|
|
let queuedMessages = 0;
|
|
for (const worker of this.workerMap) {
|
|
let queue = worker.queue;
|
|
if (!queue) {
|
|
queue = worker.worker_queue;
|
|
}
|
|
|
|
if (worker.worker_role === 'ds_pool_worker') {
|
|
queue = `${this.chain}:ds_pool:${worker.local_id}`;
|
|
}
|
|
|
|
if (queue && !testedQueues.has(queue)) {
|
|
|
|
const size = await this.manager.checkQueueSize(queue);
|
|
queuedMessages += size;
|
|
let qlimit = this.conf.scaling.max_queue_limit;
|
|
if (worker.worker_role === 'deserializer') {
|
|
qlimit = this.conf.scaling.block_queue_limit;
|
|
}
|
|
|
|
// if (size / qlimit > 0.5) {
|
|
// hLog(queue, size, ((size / qlimit) * 100).toFixed(2) + "%");
|
|
// }
|
|
|
|
// pause readers if queues are above the max_limit
|
|
if (size >= qlimit) {
|
|
aboveLimit = true;
|
|
}
|
|
|
|
// check if any queue is above resume point
|
|
if (size >= this.conf.scaling.resume_trigger) {
|
|
canResume = false;
|
|
}
|
|
|
|
// check indexing queues
|
|
if (worker.worker_role === 'ingestor') {
|
|
if (size > limit) {
|
|
if (!autoscaleConsumers[queue]) {
|
|
autoscaleConsumers[queue] = 0;
|
|
}
|
|
if (autoscaleConsumers[queue] < this.conf.scaling.max_autoscale) {
|
|
hLog(`${queue} is above the limit (${size}/${limit}). Launching consumer...`);
|
|
// TODO: scale down automatically
|
|
this.addWorker({queue: queue, type: worker.type, worker_role: 'ingestor'});
|
|
this.launchWorkers();
|
|
autoscaleConsumers[queue]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// // resume readers if the queues are below the trigger point
|
|
// if (size <= this.conf.scaling.resume_trigger) {
|
|
//
|
|
// // remove flow limiter
|
|
// if (this.readingLimited) {
|
|
// this.readingLimited = false;
|
|
// hLog('removing flow limiters');
|
|
// for (const worker of this.workerMap) {
|
|
// if (worker.worker_role === 'reader') {
|
|
// worker.wref.send({event: 'set_delay', data: {state: false, delay: 0}});
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// // fully unpause
|
|
// if (this.readingPaused) {
|
|
// canResume = true;
|
|
// this.readingPaused = false;
|
|
// hLog('resuming readers');
|
|
// for (const worker of this.workerMap) {
|
|
// if (worker.worker_role === 'reader') {
|
|
// worker.wref.send({event: 'pause'});
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// // apply block processing delay on 80% usage
|
|
// if (size >= this.conf.scaling.max_queue_limit * 0.8) {
|
|
// this.readingLimited = true;
|
|
// hLog('applying flow limiters');
|
|
// for (const worker of this.workerMap) {
|
|
// if (worker.worker_role === 'reader') {
|
|
// worker.wref.send({event: 'set_delay', data: {state: true, delay: 250}});
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
testedQueues.add(queue);
|
|
}
|
|
}
|
|
if (aboveLimit) {
|
|
this.pauseReaders();
|
|
} else {
|
|
if (canResume) {
|
|
this.resumeReaders();
|
|
}
|
|
}
|
|
this.metrics.queuedMessages?.set(queuedMessages);
|
|
}
|
|
|
|
private monitorIndexingQueues() {
|
|
if (!this.queueMonitoringInterval) {
|
|
const limit = this.conf.scaling.auto_scale_trigger;
|
|
const autoscaleConsumers = {};
|
|
setTimeout(() => {
|
|
this.checkQueues(autoscaleConsumers, limit).catch(console.log);
|
|
}, 3000);
|
|
if (!this.conf.scaling.polling_interval) {
|
|
this.conf.scaling.polling_interval = 20000;
|
|
}
|
|
this.queueMonitoringInterval = setInterval(async () => {
|
|
await this.checkQueues(autoscaleConsumers, limit);
|
|
}, this.conf.scaling.polling_interval);
|
|
}
|
|
}
|
|
|
|
private startIndexMonitoring() {
|
|
let reference_time = Date.now();
|
|
|
|
setInterval(() => {
|
|
|
|
// console.log(this.producedBlocks);
|
|
// console.log(this.missedRounds);
|
|
|
|
const _workers = Object.keys(cluster.workers).length;
|
|
const tScale = (this.log_interval / 1000);
|
|
this.total_read += this.pushedBlocks;
|
|
this.total_blocks += this.consumedBlocks;
|
|
this.total_actions += this.deserializedActions;
|
|
this.total_deltas += this.deserializedDeltas;
|
|
const consume_rate = this.consumedBlocks / tScale;
|
|
this.consume_rates.push(consume_rate);
|
|
if (this.consume_rates.length > 20) {
|
|
this.consume_rates.splice(0, 1);
|
|
}
|
|
let avg_consume_rate = 0;
|
|
if (this.consume_rates.length > 0) {
|
|
for (const r of this.consume_rates) {
|
|
avg_consume_rate += r;
|
|
}
|
|
avg_consume_rate = avg_consume_rate / this.consume_rates.length;
|
|
} else {
|
|
avg_consume_rate = consume_rate;
|
|
}
|
|
const log_msg = [];
|
|
|
|
// print current head for live reading
|
|
if (this.lastProducedBlockNum > 0 && this.lastIrreversibleBlock > 0) {
|
|
log_msg.push(`H:${this.lastProducedBlockNum} L:${this.lastIrreversibleBlock}`);
|
|
}
|
|
|
|
log_msg.push(`W:${_workers}`);
|
|
|
|
const _r = (this.pushedBlocks + this.livePushedBlocks) / tScale;
|
|
log_msg.push(`R:${_r}`);
|
|
this.metrics.readingRate?.set(_r);
|
|
|
|
const _c = (this.liveConsumedBlocks + this.consumedBlocks) / tScale;
|
|
log_msg.push(`C:${_c}`);
|
|
this.metrics.consumeRate?.set(_c);
|
|
|
|
const _a = (this.deserializedActions) / tScale;
|
|
log_msg.push(`A:${_a}`);
|
|
this.metrics.actionDSRate?.set(_a);
|
|
|
|
const _dds = (this.deserializedDeltas) / tScale
|
|
log_msg.push(`D:${_dds}`);
|
|
this.metrics.deltaDSRate?.set(_dds);
|
|
|
|
const _ir = (this.indexedObjects) / tScale
|
|
log_msg.push(`I:${_ir}`);
|
|
this.metrics.indexingRate?.set(_ir);
|
|
|
|
if (this.total_blocks < this.total_range && !this.conf.indexer.live_only_mode) {
|
|
let time_string = 'waiting for indexer';
|
|
if (avg_consume_rate > 0) {
|
|
const remaining = this.total_range - this.total_blocks;
|
|
const estimated_time = Math.round(remaining / avg_consume_rate);
|
|
time_string = moment().add(estimated_time, 'seconds').fromNow(false);
|
|
}
|
|
const pct_parsed = ((this.total_blocks / this.total_range) * 100).toFixed(1);
|
|
const pct_read = ((this.total_read / this.total_range) * 100).toFixed(1);
|
|
log_msg.push(`${this.total_blocks}/${this.total_read}/${this.total_range}`);
|
|
log_msg.push(`syncs ${time_string} (${pct_parsed}% ${pct_read}%)`);
|
|
this.metrics.syncEta.set(time_string);
|
|
}
|
|
|
|
// publish last processed block to pm2
|
|
this.metrics.lastProcessedBlockNum.set(this.lastProcessedBlockNum);
|
|
|
|
// publish log to hub
|
|
if (this.conf.hub && this.conf.hub.inform_url) {
|
|
this.hub.emit('hyp_ev', {
|
|
e: 'rates',
|
|
d: {r: _r, c: _c, a: _a}
|
|
});
|
|
}
|
|
|
|
// print monitoring log
|
|
if (this.conf.settings.rate_monitoring && !this.mode_transition) {
|
|
hLog(log_msg.join(' | '));
|
|
}
|
|
|
|
if (this.liveConsumedBlocks > 0 && this.consumedBlocks === 0 && this.conf.indexer.abi_scan_mode) {
|
|
hLog('Warning: Live reading on ABI SCAN mode')
|
|
}
|
|
|
|
if (this.liveConsumedBlocks + this.indexedObjects + this.deserializedActions + this.consumedBlocks === 0 && !this.mode_transition) {
|
|
|
|
// Report completed range (parallel reading)
|
|
if (this.total_blocks === this.total_range && !this.range_completed) {
|
|
const ttime = (Date.now() - reference_time) / 1000;
|
|
hLog(`\n
|
|
-------- BLOCK RANGE COMPLETED -------------
|
|
| Range: ${this.starting_block} >> ${this.head}
|
|
| Total time: ${ttime} seconds
|
|
| Blocks: ${this.total_range}
|
|
| Actions: ${this.total_actions}
|
|
| Deltas: ${this.total_deltas}
|
|
| ABIs: ${this.total_abis}
|
|
--------------------------------------------\n`);
|
|
this.range_completed = true;
|
|
if (this.conf.indexer.abi_scan_mode) {
|
|
if (this.conf.settings.auto_mode_switch) {
|
|
this.mode_transition = true;
|
|
hLog('Auto switching to full indexing mode in 10 seconds...');
|
|
setTimeout(() => {
|
|
reference_time = Date.now();
|
|
this.startFullIndexing().catch(console.log);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (!this.mode_transition) {
|
|
|
|
// Allow 10s threshold before shutting down the process
|
|
this.shutdownTimer = setTimeout(() => {
|
|
this.allowShutdown = true;
|
|
}, 10000);
|
|
|
|
// Auto-Stop
|
|
if (this.pushedBlocks === 0) {
|
|
this.idle_count++;
|
|
if (this.auto_stop > 0) {
|
|
if ((tScale * this.idle_count) >= this.auto_stop) {
|
|
hLog("Reached limit for no blocks processed, stopping now...");
|
|
process.exit(1);
|
|
} else {
|
|
const idleMsg = `No blocks processed! Indexer will stop in ${this.auto_stop - (tScale * this.idle_count)} seconds!`;
|
|
if (this.idle_count === 1) {
|
|
this.emitAlert('warning', idleMsg);
|
|
}
|
|
hLog(idleMsg);
|
|
}
|
|
} else {
|
|
if (!this.shutdownStarted) {
|
|
const readers = this.workerMap.filter(value => {
|
|
return value.worker_role === 'reader' || value.worker_role === 'continuous_reader';
|
|
});
|
|
if (readers.length === 0) {
|
|
hLog(`No more active workers, stopping now...`);
|
|
process.exit();
|
|
}
|
|
const idleMsg = 'No blocks are being processed, please check your state-history node!';
|
|
if (this.idle_count === 2) {
|
|
this.emitAlert('warning', idleMsg);
|
|
}
|
|
hLog(idleMsg);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
} else {
|
|
if (this.idle_count > 1) {
|
|
this.emitAlert('info', '✅ Data processing resumed!');
|
|
hLog('Processing resumed!');
|
|
}
|
|
this.idle_count = 0;
|
|
if (this.shutdownTimer) {
|
|
clearTimeout(this.shutdownTimer);
|
|
this.shutdownTimer = null;
|
|
}
|
|
}
|
|
|
|
// reset counters
|
|
this.resetMonitoringCounters();
|
|
|
|
|
|
if (_workers === 0 && !this.mode_transition) {
|
|
hLog('FATAL ERROR - All Workers have stopped!');
|
|
process.exit(1);
|
|
}
|
|
|
|
}, this.log_interval);
|
|
}
|
|
|
|
resetMonitoringCounters() {
|
|
this.pushedBlocks = 0;
|
|
this.livePushedBlocks = 0;
|
|
this.consumedBlocks = 0;
|
|
this.liveConsumedBlocks = 0;
|
|
this.deserializedActions = 0;
|
|
this.deserializedDeltas = 0;
|
|
this.indexedObjects = 0;
|
|
}
|
|
|
|
private onScheduleUpdate(msg: any) {
|
|
if (msg.live === 'true') {
|
|
hLog(`Producer schedule updated at block ${msg.block_num}. Waiting version update...`);
|
|
this.proposedSchedule = msg.new_producers;
|
|
}
|
|
}
|
|
|
|
private launchWorkers() {
|
|
let launchedCount = 0;
|
|
this.workerMap.forEach((wrk: HyperionWorkerDef) => {
|
|
if (!wrk.wref) {
|
|
wrk.wref = cluster.fork(wrk);
|
|
wrk.wref.id = wrk.worker_id;
|
|
wrk.wref.on('message', (msg) => {
|
|
this.handleMessage(msg);
|
|
});
|
|
if (wrk.worker_role === 'ds_pool_worker') {
|
|
this.dsPoolMap.set(wrk.local_id, wrk.wref);
|
|
}
|
|
if (wrk.worker_role === 'router') {
|
|
this.wsRouterWorker = wrk.wref;
|
|
}
|
|
launchedCount++;
|
|
}
|
|
});
|
|
hLog(`${launchedCount} workers launched`);
|
|
}
|
|
|
|
printActiveProds() {
|
|
if (this.activeSchedule && this.activeSchedule.producers) {
|
|
const arr = this.activeSchedule.producers.map((p, i) => {
|
|
const pos = (i < 9 ? "0" + (i + 1) : i + 1);
|
|
return "│ " + pos + " " + p.producer_name + "\t│";
|
|
});
|
|
const div = '──────────────────';
|
|
console.log(`\n ⛏ Active Producers\n┌${div}┐\n${arr.join('\n')}\n└${div}┘`);
|
|
}
|
|
}
|
|
|
|
private emitHubUpdate() {
|
|
this.hub.emit('hyp_info', {
|
|
type: 'indexer',
|
|
production: this.conf.hub.production,
|
|
location: this.conf.hub.location,
|
|
chainId: this.chain_data.chain_id,
|
|
chainLogo: this.conf.api.chain_logo_url,
|
|
providerName: this.conf.api.provider_name,
|
|
providerUrl: this.conf.api.provider_url,
|
|
providerLogo: this.conf.api.provider_logo,
|
|
explorerEnabled: this.conf.plugins.explorer?.enabled,
|
|
chainCodename: this.chain,
|
|
chainName: this.conf.api.chain_name,
|
|
endpoint: this.conf.api.server_name,
|
|
features: this.conf.features,
|
|
filters: {
|
|
blacklists: this.conf.blacklists,
|
|
whitelists: this.conf.whitelists
|
|
}
|
|
});
|
|
}
|
|
|
|
startHyperionHub() {
|
|
if (this.conf.hub) {
|
|
const url = this.conf.hub.inform_url;
|
|
hLog(`Connecting to Hyperion Hub...`);
|
|
this.hub = io(url, {
|
|
query: {
|
|
key: this.conf.hub.publisher_key,
|
|
client_mode: 'false'
|
|
}
|
|
});
|
|
this.hub.on('connect', () => {
|
|
hLog(`Hyperion Hub connected!`);
|
|
this.emitHubUpdate();
|
|
});
|
|
// this.hub.on('reconnect', () => {
|
|
// this.emitHubUpdate();
|
|
// });
|
|
}
|
|
}
|
|
|
|
async fillMissingBlocks(data: any, ws: WebSocket<any>) {
|
|
this.pendingRepairRanges = data.filter((range: any) => {
|
|
return range.end - range.start >= 0;
|
|
});
|
|
this.pendingRepairRanges.reverse();
|
|
let totalBlocks = 0;
|
|
this.pendingRepairRanges.forEach(value => {
|
|
value["size"] = value.end - value.start + 1;
|
|
totalBlocks += value.size;
|
|
});
|
|
hLog(`Filling ${totalBlocks} missing blocks...`);
|
|
this.repairReader = this.addWorker({
|
|
worker_role: 'repair_reader'
|
|
});
|
|
this.launchWorkers();
|
|
this.connectedController = ws;
|
|
|
|
// for (const blockRange of data) {
|
|
// console.log(`Filling missing blocks ${blockRange.start} - ${blockRange.end}`);
|
|
// ws.send(JSON.stringify({
|
|
// event: 'received_range',
|
|
// data: blockRange
|
|
// }));
|
|
// }
|
|
}
|
|
|
|
private createLocalController(controlPort: number) {
|
|
this.localController = App().ws('/local', {
|
|
open: (ws) => {
|
|
hLog(`Local controller connected!`);
|
|
},
|
|
message: (ws, msg) => {
|
|
const buffer = Buffer.from(msg);
|
|
const message = JSON.parse(buffer.toString());
|
|
if (message.event === 'fill_missing_blocks') {
|
|
this.fillMissingBlocks(message.data, ws).catch(console.log);
|
|
}
|
|
},
|
|
close: (ws) => {
|
|
hLog(`Local controller disconnected!`);
|
|
}
|
|
});
|
|
this.localController.listen(controlPort, (token) => {
|
|
if (token) {
|
|
hLog(`Local controller listening on port ${controlPort}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
async runMaster() {
|
|
|
|
// config checks
|
|
if (!this.conf.scaling.max_queue_limit) {
|
|
hLog(`scaling.max_queue_limit is not defined!`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!this.conf.scaling.resume_trigger) {
|
|
hLog(`scaling.resume_trigger is not defined!`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!this.conf.scaling.block_queue_limit) {
|
|
hLog(`scaling.block_queue_limit is not defined!`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!this.conf.settings.index_partition_size) {
|
|
hLog(`[FATAL ERROR] settings.index_partition_size is not defined!`);
|
|
process.exit(1);
|
|
}
|
|
|
|
this.printMode();
|
|
this.createLocalController(this.manager.conn.chains[this.conf.settings.chain].control_port);
|
|
|
|
// Preview mode - prints only the proposed worker map
|
|
let preview = this.conf.settings.preview;
|
|
|
|
await this.purgeQueues();
|
|
|
|
// Chain API
|
|
this.rpc = this.manager.nodeosJsonRPC;
|
|
if (this.conf.settings.bp_monitoring) {
|
|
await this.getCurrentSchedule();
|
|
this.printActiveProds();
|
|
}
|
|
|
|
// Redis
|
|
this.ioRedisClient = new IORedis(this.manager.conn.redis);
|
|
|
|
// Remove first indexed block from cache (v2/health)
|
|
await this.ioRedisClient.del(`${this.manager.chain}::fib`);
|
|
|
|
// check nodeos
|
|
try {
|
|
const info = await this.rpc.get_info();
|
|
hLog(`Nodeos version: ${info.server_version_string}`);
|
|
} catch (e) {
|
|
hLog(`Chain API Error: ${e.message}`);
|
|
process.exit();
|
|
}
|
|
|
|
// Elasticsearch
|
|
this.client = this.manager.elasticsearchClient;
|
|
try {
|
|
const esInfo = await this.client.info();
|
|
hLog(`Elasticsearch: ${esInfo.body.version.number} | Lucene: ${esInfo.body.version.lucene_version}`);
|
|
this.emitAlert('info', `Indexer started using ES v${esInfo.body.version.number}`);
|
|
} catch (e) {
|
|
console.log(e.message);
|
|
hLog('Failed to check elasticsearch version!');
|
|
process.exit();
|
|
}
|
|
await this.verifyIngestClients();
|
|
this.max_readers = this.conf.scaling.readers;
|
|
if (this.conf.indexer.disable_reading) {
|
|
this.max_readers = 1;
|
|
}
|
|
|
|
const prefix = this.chain + ':index';
|
|
this.IndexingQueues = [
|
|
{
|
|
type: 'action',
|
|
name: prefix + "_actions"
|
|
},
|
|
{
|
|
type: 'block',
|
|
name: prefix + "_blocks"
|
|
},
|
|
{
|
|
type: 'delta',
|
|
name: prefix + "_deltas"
|
|
},
|
|
{
|
|
type: 'abi',
|
|
name: prefix + "_abis"
|
|
},
|
|
{
|
|
type: 'generic',
|
|
name: prefix + "_generic"
|
|
}
|
|
];
|
|
|
|
const indexConfig = await import('../definitions/index-templates');
|
|
|
|
const indicesList = [
|
|
{name: "action", type: "action"},
|
|
{name: "block", type: "block"},
|
|
{name: "abi", type: "abi"},
|
|
{name: "delta", type: "delta"},
|
|
{name: "logs", type: "logs"},
|
|
{name: 'permissionLink', type: 'link'},
|
|
{name: 'permission', type: 'perm'},
|
|
{name: 'resourceLimits', type: 'reslimits'},
|
|
{name: 'resourceUsage', type: 'userres'},
|
|
{name: 'generatedTransaction', type: 'gentrx'},
|
|
{name: 'failedTransaction', type: 'trxerr'}
|
|
];
|
|
|
|
this.addStateTables(indicesList, this.IndexingQueues);
|
|
await this.applyUpdateScript();
|
|
await this.addLifecyclePolicies(indexConfig);
|
|
await this.appendExtraMappings(indexConfig);
|
|
await this.updateIndexTemplates(indicesList, indexConfig);
|
|
|
|
// await this.createIndices(indicesList);
|
|
|
|
if (this.conf.indexer.fill_state) {
|
|
await this.fillCurrentStateTables();
|
|
}
|
|
|
|
// Prepare Workers
|
|
this.workerMap = [];
|
|
this.worker_index = 0;
|
|
this.maxBatchSize = this.conf.scaling.batch_size;
|
|
|
|
await this.findRange();
|
|
|
|
this.emitAlert(
|
|
'info',
|
|
`Range defined from ${this.starting_block} to ${this.head}` +
|
|
(this.conf.indexer.live_reader ? ' with ' : ' without ') +
|
|
'live reading active');
|
|
|
|
await this.setupWorkers();
|
|
|
|
// TODO: fix repair mode
|
|
// Run and wait for repair
|
|
if (this.conf.indexer.repair_mode) {
|
|
await this.startRepairMode();
|
|
}
|
|
|
|
// Quit App if on preview mode
|
|
if (preview) {
|
|
HyperionMaster.printWorkerMap(this.workerMap);
|
|
await this.waitForLaunch();
|
|
}
|
|
|
|
// Setup Error Logging
|
|
this.setupDSElogs();
|
|
|
|
// Start Monitoring
|
|
this.startIndexMonitoring();
|
|
|
|
// handle worker disconnection events
|
|
cluster.on('disconnect', (worker) => {
|
|
if (!this.mode_transition && !this.shutdownStarted) {
|
|
const workerReference = this.workerMap.find(value => value.worker_id === worker.id);
|
|
if (workerReference) {
|
|
hLog(`The worker #${worker.id} has disconnected, attempting to re-launch in 5 seconds...`);
|
|
workerReference.wref = null;
|
|
workerReference.failures++;
|
|
hLog(`New worker defined: ${workerReference.worker_role} for ${workerReference.worker_queue}`);
|
|
setTimeout(() => {
|
|
this.launchWorkers();
|
|
setTimeout(() => {
|
|
if (workerReference.worker_role === 'deserializer' && workerReference.wref) {
|
|
workerReference.wref.send({
|
|
event: 'initialize_abi',
|
|
data: this.cachedInitABI
|
|
});
|
|
}
|
|
}, 1000);
|
|
}, 5000);
|
|
} else {
|
|
hLog(`The worker #${worker.id} has disconnected`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Launch all workers
|
|
this.launchWorkers();
|
|
|
|
// enable ipc msg rate monitoring
|
|
let ipcDebugRate = this.conf.settings.ipc_debug_rate;
|
|
if (ipcDebugRate > 0 && ipcDebugRate < 1000) {
|
|
hLog(`settings.ipc_debug_rate was set too low (${this.conf.settings.ipc_debug_rate}) using 1000 instead!`);
|
|
ipcDebugRate = 1000;
|
|
}
|
|
this.shouldCountIPCMessages = ipcDebugRate && ipcDebugRate >= 1000;
|
|
if (this.conf.settings.debug) {
|
|
if (this.shouldCountIPCMessages) {
|
|
const rate = ipcDebugRate;
|
|
this.totalMessages = 0;
|
|
setInterval(() => {
|
|
hLog(`IPC Messaging Rate: ${(this.totalMessages / (rate / 1000)).toFixed(2)} msg/s`);
|
|
this.totalMessages = 0;
|
|
}, ipcDebugRate);
|
|
}
|
|
}
|
|
|
|
// load hyperion hub connection
|
|
if (this.conf.hub) {
|
|
try {
|
|
this.startHyperionHub();
|
|
} catch (e) {
|
|
hLog(e);
|
|
}
|
|
}
|
|
|
|
// Actions that can be triggered via pm2 trigger <action> <process> or on the pm2 dashboard
|
|
this.addPm2Actions();
|
|
|
|
// Add custom metrics
|
|
this.addPm2Metrics();
|
|
}
|
|
|
|
async streamBlockHeaders(start_on, stop_on, func: (data) => void) {
|
|
const responseQueue = [];
|
|
const init_response = await this.client.search({
|
|
index: this.chain + '-block-*',
|
|
scroll: '30s',
|
|
size: 1000,
|
|
track_total_hits: true,
|
|
body: {
|
|
query: {
|
|
bool: {
|
|
must: [
|
|
{
|
|
range: {
|
|
block_num: {
|
|
gte: start_on,
|
|
lte: stop_on
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
sort: [{block_num: {order: "asc"}}],
|
|
_source: {
|
|
includes: [
|
|
"block_num",
|
|
"block_id",
|
|
"prev_id"
|
|
]
|
|
}
|
|
},
|
|
});
|
|
const totalHits = init_response.body.hits.total.value;
|
|
hLog(`Total hits in range: ${totalHits}`);
|
|
responseQueue.push(init_response);
|
|
while (responseQueue.length) {
|
|
const {body} = responseQueue.shift();
|
|
for (const hit of body['hits']['hits']) {
|
|
func(hit._source);
|
|
}
|
|
const next_response = await this.client.scroll({scroll_id: body['_scroll_id'], scroll: '30s'});
|
|
if (next_response.body['hits']['hits'].length > 0) {
|
|
responseQueue.push(next_response);
|
|
}
|
|
}
|
|
}
|
|
|
|
async collectBlockHistogram() {
|
|
const min_interval = 25000;
|
|
const missed_ranges = [];
|
|
const response = await this.client.search({
|
|
index: this.chain + '-block-*',
|
|
track_total_hits: true,
|
|
body: {
|
|
aggs: {
|
|
"blocks": {
|
|
"histogram": {
|
|
"field": "block_num",
|
|
"interval": min_interval,
|
|
"min_doc_count": 1
|
|
}
|
|
}
|
|
},
|
|
size: 0,
|
|
_source: {
|
|
excludes: []
|
|
},
|
|
query: {
|
|
match_all: {}
|
|
}
|
|
}
|
|
});
|
|
const totalBlockCount = response.body.hits.total.value;
|
|
hLog('Total indexed blocks: ' + totalBlockCount);
|
|
hLog('Last indexed block: ' + this.head);
|
|
hLog(`Total missing blocks: ${this.head - totalBlockCount}`);
|
|
let lastKey = 0;
|
|
for (const bucket of response.body.aggregations.blocks.buckets) {
|
|
if (lastKey !== 0 && bucket.key !== lastKey + min_interval) {
|
|
hLog(`All blocks missing from ${lastKey + min_interval} to ${bucket.key}`);
|
|
missed_ranges.push([lastKey + min_interval, bucket.key]);
|
|
}
|
|
if (bucket.doc_count < min_interval) {
|
|
const missed_on_range = min_interval - bucket.doc_count;
|
|
hLog(`${missed_on_range} blocks missing from ${bucket.key} to ${bucket.key + min_interval}`);
|
|
let lastBlock = bucket.key;
|
|
await this.streamBlockHeaders(bucket.key, bucket.key + min_interval, (data) => {
|
|
if (data.block_num !== lastBlock + 1) {
|
|
hLog(`Missing range from ${lastBlock} to ${data.block_num}`);
|
|
missed_ranges.push([lastBlock, data.block_num]);
|
|
}
|
|
lastBlock = data.block_num;
|
|
});
|
|
}
|
|
lastKey = bucket.key;
|
|
}
|
|
hLog(missed_ranges);
|
|
}
|
|
|
|
async startRepairMode() {
|
|
hLog("-------->> REPAIR MODE ACTIVATED <<------------");
|
|
const t0 = Date.now();
|
|
await this.collectBlockHistogram();
|
|
hLog(`Block index analysis completed in ${(Date.now() - t0) / 1000} seconds`);
|
|
}
|
|
|
|
async processAccount(accountName: string) {
|
|
const acc = await this.manager.nodeosJsonRPC.get_account(accountName);
|
|
|
|
// table-accounts
|
|
if (acc.core_liquid_balance) {
|
|
const arr = acc.core_liquid_balance.split(' ');
|
|
const payload = {
|
|
block_num: acc.head_block_num,
|
|
symbol: arr[1],
|
|
amount: parseFloat(arr[0]),
|
|
code: this.conf.settings.eosio_alias + '.token',
|
|
scope: accountName,
|
|
present: 2
|
|
};
|
|
try {
|
|
await this.manager.elasticsearchClient.index({
|
|
index: this.chain + '-table-accounts',
|
|
id: `${payload.code}-${payload.scope}-${payload.symbol}`,
|
|
type: '_doc',
|
|
body: payload
|
|
});
|
|
// console.log(`${payload.scope}: ${payload.amount} ${payload.symbol}`);
|
|
} catch (e) {
|
|
console.log(`Failed to index account: ${payload.code}-${payload.scope}-${payload.symbol}`);
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
// table-voters
|
|
if (acc.voter_info) {
|
|
const payload = {
|
|
block_num: acc.head_block_num,
|
|
last_vote_weight: parseFloat(acc.voter_info.last_vote_weight),
|
|
proxied_vote_weight: parseFloat(acc.voter_info.proxied_vote_weight),
|
|
staked: parseFloat(acc.voter_info.staked),
|
|
voter: acc.voter_info.owner,
|
|
is_proxy: acc.voter_info.is_proxy === 1,
|
|
proxy: acc.voter_info.proxy,
|
|
producers: acc.voter_info.producers
|
|
}
|
|
|
|
if (payload.producers.length === 0) {
|
|
delete payload.producers;
|
|
}
|
|
|
|
if (payload.proxy === '') {
|
|
delete payload.proxy;
|
|
}
|
|
|
|
try {
|
|
await this.manager.elasticsearchClient.index({
|
|
index: this.chain + '-table-voters',
|
|
id: `${payload.voter}`,
|
|
type: '_doc',
|
|
body: payload
|
|
});
|
|
// console.log(`${payload.scope}: ${payload.amount} ${payload.symbol}`);
|
|
} catch (e) {
|
|
console.log(`Failed to index voter: ${payload.voter}`);
|
|
console.log(e);
|
|
}
|
|
}
|
|
|
|
|
|
if (acc.permissions) {
|
|
for (const perm of acc.permissions) {
|
|
const payload = {
|
|
owner: accountName,
|
|
block_num: acc.head_block_num,
|
|
parent: perm.parent,
|
|
name: perm.perm_name,
|
|
auth: perm.required_auth,
|
|
present: 2
|
|
};
|
|
|
|
if (payload.auth.accounts.length === 0) {
|
|
delete payload.auth.accounts;
|
|
}
|
|
|
|
if (payload.auth.keys.length === 0) {
|
|
delete payload.auth.keys;
|
|
} else {
|
|
for (const key of payload.auth.keys) {
|
|
key.key = convertLegacyPublicKey(key.key);
|
|
}
|
|
}
|
|
|
|
if (payload.auth.waits.length === 0) {
|
|
delete payload.auth.waits;
|
|
}
|
|
|
|
try {
|
|
await this.manager.elasticsearchClient.index({
|
|
index: this.chain + '-perm',
|
|
id: `${payload.owner}-${payload.name}`,
|
|
type: '_doc',
|
|
body: payload
|
|
});
|
|
// console.log(`${payload.scope}: ${payload.amount} ${payload.symbol}`);
|
|
} catch (e) {
|
|
console.log(`Failed to index permission: ${payload.owner}-${payload.name}`);
|
|
console.log(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async getRows(start_at?) {
|
|
const data = await this.manager.nodeosJsonRPC.get_table_rows({
|
|
code: this.conf.settings.eosio_alias,
|
|
table: 'voters',
|
|
scope: this.conf.settings.eosio_alias,
|
|
key_type: "name",
|
|
lower_bound: start_at,
|
|
upper_bound: "",
|
|
limit: 10,
|
|
json: true
|
|
});
|
|
return data.rows;
|
|
}
|
|
|
|
async fillCurrentStateTables() {
|
|
hLog(`Filling system state tables with current data...`);
|
|
const tRef = Date.now();
|
|
|
|
let processedAccounts = 0;
|
|
let totalAccounts = 0;
|
|
let lastAccount = null;
|
|
|
|
setInterval(() => {
|
|
if (processedAccounts > 0) {
|
|
hLog(`[Account Filler] processing ${processedAccounts} accounts/s | ${lastAccount} | ${totalAccounts}`);
|
|
processedAccounts = 0;
|
|
}
|
|
}, 1000);
|
|
|
|
const queue = [];
|
|
const rows = await this.getRows(null);
|
|
if (rows.length > 0) {
|
|
lastAccount = rows[rows.length - 1].owner
|
|
}
|
|
queue.push(...rows);
|
|
while (queue.length > 0) {
|
|
const acc = queue.shift();
|
|
await this.processAccount(acc.owner);
|
|
processedAccounts++;
|
|
totalAccounts++;
|
|
if (queue.length === 0) {
|
|
const nextBatch = await this.getRows(acc.owner);
|
|
if (nextBatch.length > 1) {
|
|
lastAccount = (nextBatch.shift()).owner;
|
|
queue.push(...nextBatch);
|
|
}
|
|
}
|
|
}
|
|
|
|
hLog(`Filling took ${Date.now() - tRef}ms | ${totalAccounts} accounts`);
|
|
process.exit();
|
|
}
|
|
|
|
async startFullIndexing() {
|
|
|
|
// emit kill signal
|
|
hLog(`Killing ${this.workerMap.length} workers`);
|
|
let disconnect_counter = 0;
|
|
for (const w of this.workerMap) {
|
|
if (w.wref && w.wref.isConnected()) {
|
|
w.wref.on("disconnect", () => {
|
|
disconnect_counter++;
|
|
});
|
|
w.wref.kill();
|
|
}
|
|
}
|
|
|
|
// wait for all disconnect events
|
|
await new Promise<void>((resolve) => {
|
|
let _loop = setInterval(() => {
|
|
if (disconnect_counter === this.workerMap.length) {
|
|
clearInterval(_loop);
|
|
resolve();
|
|
}
|
|
}, 100);
|
|
setTimeout(() => {
|
|
if (_loop) {
|
|
clearInterval(_loop);
|
|
resolve();
|
|
}
|
|
}, 10000);
|
|
});
|
|
|
|
// clear worker references
|
|
this.workerMap = [];
|
|
|
|
// disable abi scan mode and rewrite config file
|
|
this.cm.setAbiScanMode(false);
|
|
hLog('indexer.abi_scan_mode set to false');
|
|
this.conf.indexer.abi_scan_mode = false;
|
|
|
|
// reset global counters
|
|
this.total_abis = 0;
|
|
this.total_actions = 0;
|
|
this.total_blocks = 0;
|
|
this.total_deltas = 0;
|
|
this.total_read = 0;
|
|
this.range_completed = false;
|
|
this.lastProcessedBlockNum = 0;
|
|
this.cachedInitABI = null;
|
|
|
|
|
|
await this.findRange();
|
|
await this.setupWorkers();
|
|
|
|
// launch new workers
|
|
this.launchWorkers();
|
|
|
|
// exit transition
|
|
this.mode_transition = false;
|
|
}
|
|
|
|
private async setupWorkers() {
|
|
this.lastIndexedABI = await getLastIndexedABI(this.client, this.chain);
|
|
await this.defineBlockRange();
|
|
this.total_range = this.head - this.starting_block;
|
|
await this.setupReaders();
|
|
await this.setupDeserializers();
|
|
await this.setupIndexers();
|
|
await this.setupStreaming();
|
|
await this.setupDSPool();
|
|
this.addWorker({worker_role: "delta_updater"});
|
|
}
|
|
|
|
private async findRange() {
|
|
// Auto-stop
|
|
if (this.conf.settings.auto_stop) {
|
|
this.auto_stop = this.conf.settings.auto_stop;
|
|
}
|
|
|
|
// Find last indexed block
|
|
let lastIndexedBlock;
|
|
if (this.conf.features.index_deltas) {
|
|
lastIndexedBlock = await getLastIndexedBlockByDelta(this.client, this.chain);
|
|
hLog(`Last indexed block (deltas): ${lastIndexedBlock}`);
|
|
} else {
|
|
lastIndexedBlock = await getLastIndexedBlock(this.client, this.chain);
|
|
hLog(`Last indexed block (blocks): ${lastIndexedBlock}`);
|
|
}
|
|
|
|
// Start from the last indexed block
|
|
this.starting_block = 1;
|
|
|
|
// Fecth chain lib
|
|
try {
|
|
this.chain_data = await this.rpc.get_info();
|
|
} catch (e) {
|
|
hLog('Failed to connect to chain api: ' + e.message);
|
|
process.exit(1);
|
|
}
|
|
this.head = this.chain_data.head_block_num;
|
|
|
|
if (lastIndexedBlock > 0) {
|
|
this.starting_block = lastIndexedBlock;
|
|
}
|
|
|
|
if (this.conf.indexer.stop_on !== 0) {
|
|
this.head = this.conf.indexer.stop_on;
|
|
}
|
|
}
|
|
|
|
private emitAlert(msgType: string, msgContent: string) {
|
|
this.alerts.emitAlert({type: msgType, process: process.title, content: msgContent});
|
|
}
|
|
|
|
private addPm2Metrics() {
|
|
this.metrics.readingRate = pm2io.metric({name: 'Block Reading (blocks/s)'});
|
|
this.metrics.consumeRate = pm2io.metric({name: 'Block Processing (blocks/s)'});
|
|
this.metrics.actionDSRate = pm2io.metric({name: 'Action Deserialization (actions/s)'});
|
|
this.metrics.deltaDSRate = pm2io.metric({name: 'Delta Deserialization (deltas/s)'});
|
|
this.metrics.indexingRate = pm2io.metric({name: 'Indexing Rate (docs/s)'});
|
|
this.metrics.queuedMessages = pm2io.metric({name: 'Queued Messages'});
|
|
this.metrics.lastProcessedBlockNum = pm2io.metric({name: 'Last Block'});
|
|
this.metrics.syncEta = pm2io.metric({name: 'Time to sync'});
|
|
}
|
|
|
|
private addPm2Actions() {
|
|
|
|
const ioConfig: IOConfig = {
|
|
apmOptions: {
|
|
appName: this.chain + " indexer",
|
|
publicKey: undefined,
|
|
secretKey: undefined,
|
|
sendLogs: true
|
|
},
|
|
metrics: {
|
|
v8: true,
|
|
http: false,
|
|
network: false,
|
|
runtime: true,
|
|
eventLoop: true
|
|
}
|
|
};
|
|
pm2io.init(ioConfig);
|
|
|
|
|
|
pm2io.action('stop', (reply) => {
|
|
try {
|
|
this.shutdownStarted = true;
|
|
this.allowMoreReaders = false;
|
|
const stopMsg = 'Stop signal received. Shutting down readers immediately!';
|
|
hLog(stopMsg);
|
|
this.emitAlert('warning', stopMsg);
|
|
messageAllWorkers(cluster, {
|
|
event: 'stop'
|
|
});
|
|
setInterval(() => {
|
|
if (this.allowShutdown) {
|
|
getLastIndexedBlockFromRange(this.client, this.chain, this.starting_block, this.head).then((value: number) => {
|
|
hLog(`Last Indexed Block: ${value}`);
|
|
writeFileSync(`./chains/.${this.chain}_lastblock.txt`, value.toString());
|
|
hLog('Shutting down master...');
|
|
reply({ack: true});
|
|
process.exit(1);
|
|
});
|
|
}
|
|
}, 100);
|
|
} catch (e) {
|
|
reply({ack: false, error: e.message});
|
|
}
|
|
});
|
|
|
|
pm2io.action('get_usage_map', (reply) => {
|
|
reply(this.globalUsageMap);
|
|
});
|
|
|
|
pm2io.action('get_memory_usage', (reply) => {
|
|
this.requestDataFromWorkers(
|
|
{
|
|
event: 'request_memory_usage'
|
|
},
|
|
'memory_report'
|
|
).then(value => {
|
|
reply(value);
|
|
});
|
|
});
|
|
|
|
pm2io.action('get_heap', (reply) => {
|
|
this.requestDataFromWorkers(
|
|
{
|
|
event: 'request_v8_heap_stats'
|
|
},
|
|
'v8_heap_report'
|
|
).then(value => {
|
|
reply(value);
|
|
});
|
|
});
|
|
}
|
|
|
|
async requestDataFromWorkers(requestEvent: {
|
|
event: string,
|
|
data?: any
|
|
}, responseEventType: string, timeoutSec: number = 1000) {
|
|
const requests = [];
|
|
for (const id in cluster.workers) {
|
|
if (cluster.workers.hasOwnProperty(id)) {
|
|
const worker: Worker = cluster.workers[id];
|
|
requests.push(new Promise((resolve) => {
|
|
const _timeout = setTimeout(() => {
|
|
worker.removeListener('message', _listener);
|
|
resolve([id, null]);
|
|
}, timeoutSec);
|
|
const _listener = (msg) => {
|
|
if (msg.event === responseEventType) {
|
|
clearTimeout(_timeout);
|
|
worker.removeListener('message', _listener);
|
|
resolve([msg.id, msg.data]);
|
|
}
|
|
};
|
|
worker.on('message', _listener);
|
|
worker.send(requestEvent);
|
|
}));
|
|
}
|
|
}
|
|
const results = await Promise.all(requests);
|
|
const responses = {};
|
|
for (const result of results) {
|
|
if (result[1]) {
|
|
responses[result[0]] = result[1];
|
|
}
|
|
}
|
|
return responses;
|
|
}
|
|
|
|
private sendPendingRepairRanges() {
|
|
if (this.pendingRepairRanges.length > 0) {
|
|
const nextRange = this.pendingRepairRanges.shift();
|
|
console.log('NEXT RANGE FOR REPAIR: ', nextRange);
|
|
this.repairReader.wref.send({
|
|
event: 'new_range',
|
|
target: this.repairReader.worker_id.toString(),
|
|
data: {
|
|
first_block: nextRange.start,
|
|
last_block: nextRange.end + 1
|
|
}
|
|
});
|
|
} else {
|
|
console.log('Repair completed!');
|
|
this.connectedController.send(JSON.stringify({
|
|
event: 'repair_completed'
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|