Files
2020-04-08 02:15:11 -03:00

1405 lines
53 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 {
getLastIndexedABI,
getLastIndexedBlock,
getLastIndexedBlockByDelta,
getLastIndexedBlockByDeltaFromRange,
getLastIndexedBlockFromRange,
hLog,
messageAllWorkers
} from "../helpers/common_functions";
import {GetInfoResult} from "eosjs/dist/eosjs-rpc-interfaces";
import * as pm2io from '@pm2/io';
import {
createWriteStream,
existsSync,
mkdirSync,
readFileSync,
symlinkSync,
unlinkSync,
writeFileSync,
WriteStream
} from "fs";
import * as path from "path";
import * as cluster from "cluster";
import {Worker} from "cluster";
import {HyperionWorkerDef} from "../interfaces/hyperionWorkerDef";
import {HyperionConfig} from "../interfaces/hyperionConfig";
import moment = require("moment");
import Timeout = NodeJS.Timeout;
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, cluster.Worker> = new Map();
private globalUsageMap = {};
private totalContractHits = 0;
// producer monitoring
private producedBlocks: object = {};
private lastProducedBlockNum = 0;
private lastProducer: string = null;
private handoffCounter: number = 0;
private missedRounds: object = {};
private blockMsgQueue: any[] = [];
// 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 total_indexed_blocks = 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 consume_rates: number[] = [];
private total_range = 0;
private range_completed = false;
private head: number;
private starting_block: number;
private shutdownTimer: Timeout;
private idle_count = 0;
private auto_stop = 0;
// IPC Messages Handling
private msgHandlerMap: any;
private cachedInitABI = false;
private activeReadersCount = 0;
private lastAssignedBlock: number;
private lastIndexedABI: number;
private activeSchedule: any;
private pendingSchedule: any;
private proposedSchedule: any;
constructor() {
const cm = new ConfigurationModule();
this.conf = cm.config;
this.manager = new ConnectionManager(cm);
this.mLoader = new HyperionModuleLoader(cm);
this.chain = this.conf.settings.chain;
this.initHandlerMap();
}
initHandlerMap() {
this.msgHandlerMap = {
'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++;
if (this.conf.settings.bp_monitoring) {
this.onLiveBlock(msg);
}
}
},
'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
});
}
},
'router_ready': () => {
messageAllWorkers(cluster, {
event: 'connect_ws'
});
},
'save_abi': (msg: any) => {
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
});
}
}
}
},
'completed': (msg: any) => {
if (msg.id === this.doctorId.toString()) {
hLog('repair worker completed', msg);
hLog('queue size [before]:', this.missingRanges.length);
if (this.missingRanges.length > 0) {
const range_data = this.missingRanges.shift();
hLog('New repair range', range_data);
hLog('queue size [after]:', this.missingRanges.length);
this.doctorIdle = false;
messageAllWorkers(cluster, {
event: 'new_range',
target: msg.id,
data: {
first_block: range_data.start,
last_block: range_data.end
}
});
} else {
this.doctorIdle = true;
}
} 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;
if (end > this.head) {
end = this.head;
}
this.lastAssignedBlock += this.maxBatchSize;
const def = {
first_block: start,
last_block: end
};
this.activeReadersCount++;
messageAllWorkers(cluster, {
event: 'new_range',
target: msg.id,
data: def
});
}
}
},
'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, []];
}
}
}
}
}
};
}
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"});
}
if (table_feats.userres) {
indicesList.push({name: 'tableUserres', type: 'table-userres'});
index_queues.push({type: 'table-userres', name: index_queue_prefix + "_table_userres"});
}
}
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 (e) {
hLog(e);
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(`Mapping added for ${key}`);
}
}
}
}
}
private async updateIndexTemplates(indicesList: { name: string, type: string }[], indexConfig) {
hLog(`Updating index templates for ${this.conf.settings.chain}`);
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 {
hLog(`${this.conf.settings.chain}-${index.type} template updated!`);
}
} else {
hLog(`${index.name} template not found!`);
}
} catch (e) {
hLog(e);
if (e.meta) {
hLog(e.meta.body);
}
process.exit(1);
}
}
hLog('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) {
// Create indices
let version;
if (this.conf.settings.index_version === 'true') {
version = 'v1';
} else {
version = this.conf.settings.index_version;
}
for (const index of indicesList) {
const new_index = `${queue_prefix}-${index.type}-${version}-000001`;
const exists = await this.client.indices.exists({
index: new_index
});
if (!exists.body) {
hLog(`Creating index ${new_index}...`);
await this.client['indices'].create({
index: new_index
});
hLog(`Creating alias ${queue_prefix}-${index.type} >> ${new_index}`);
await this.client.indices.putAlias({
index: new_index,
name: `${queue_prefix}-${index.type}`
});
}
}
}
// 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 {
// 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;
}
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'
});
}
}
}
private async setupIndexers() {
let qIdx = 0;
this.IndexingQueues.forEach((q) => {
let n = this.conf.scaling.indexing_queues;
if (q.type === 'abi') {
n = 1;
}
qIdx = 0;
for (let i = 0; i < n; i++) {
let m = 1;
if (q.type === 'action' || q.type === 'delta') {
m = this.conf.scaling.ad_idx_queues;
}
for (let j = 0; j < m; j++) {
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: any) {
this.worker_index++;
def.worker_id = this.worker_index;
this.workerMap.push(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
});
}
}
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;
}
}
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;
} else {
this.blockMsgQueue.push(msg);
this.blockMsgQueue.sort((a, b) => a.block_num - b.block_num);
while (this.blockMsgQueue.length > 0) {
if (this.blockMsgQueue[0].block_num === this.lastProducedBlockNum + 1) {
this.onLiveBlock(this.blockMsgQueue.shift());
} else {
break;
}
}
}
}
handleMessage(msg) {
if (this.conf.settings.ipc_debug_rate && this.conf.settings.ipc_debug_rate >= 1000) {
this.totalMessages++;
}
if (this.msgHandlerMap[msg.event]) {
this.msgHandlerMap[msg.event](msg);
} else {
if (msg.type) {
if (msg.type === 'axm:monitor') {
if (process.env['AXM_DEBUG'] === 'true') {
hLog(`----------- axm:monitor ------------`);
for (const key in msg.data) {
if (msg.data.hasOwnProperty(key)) {
hLog(`${key}: ${msg.data[key].value}`);
}
}
}
}
}
}
}
private async setupReaders() {
// Setup Readers
this.lastAssignedBlock = this.starting_block;
this.activeReadersCount = 0;
if (!this.conf.indexer.repair_mode) {
if (!this.conf.indexer.live_only_mode) {
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 Serial reader worker
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.addWorker({
worker_role: 'continuous_reader',
worker_last_processed_block: _head,
ws_router: ''
});
// live deserializer
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',
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;
}
for (const code in this.globalUsageMap) {
if (this.globalUsageMap.hasOwnProperty(code)) {
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);
}
}
this.globalUsageMap[code][1] = _pct;
if (JSON.stringify(this.globalUsageMap[code][2]) !== JSON.stringify(proposedWorkers)) {
// hLog(this.globalUsageMap[code][2], ">>", proposedWorkers);
proposedWorkers.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] = proposedWorkers;
}
}
}
}
private startContractMonitoring() {
// Monitor Global Contract Usage
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') {
w.wref.send({
event: 'update_pool_map',
data: this.globalUsageMap
});
}
}
// clearUsageMap();
}, 5000);
}
private monitorIndexingQueues() {
const limit = this.conf.scaling.auto_scale_trigger;
const autoscaleConsumers = {};
setInterval(async () => {
const testedQueues = new Set();
for (const worker of this.workerMap) {
if (worker.worker_role === 'ingestor') {
const queue = worker.queue;
if (!testedQueues.has(queue)) {
testedQueues.add(queue);
const size = await this.manager.checkQueueSize(queue);
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...`);
this.addWorker({
queue: queue,
type: worker.type,
worker_role: 'ingestor'
});
this.launchWorkers();
autoscaleConsumers[queue]++;
} else {
// hLog(`WARN: Max consumer limit reached on ${queue}!`);
}
}
}
}
}
}, 20000);
}
private onPm2Stop() {
pm2io.action('stop', (reply) => {
this.allowMoreReaders = false;
console.info('Stop signal received. Shutting down readers immediately!');
hLog('Waiting for queues...');
messageAllWorkers(cluster, {
event: 'stop'
});
reply({ack: true});
setInterval(() => {
if (this.allowShutdown) {
// TODO: check last indexed block, print to console and save on a temporary file
getLastIndexedBlockFromRange(this.client, this.chain, this.starting_block, this.head).then((lastblock) => {
hLog(`Last Indexed Block: ${lastblock}`);
writeFileSync(`./chains/.${this.chain}_lastblock.txt`, lastblock.toString());
hLog('Shutting down master...');
process.exit(1);
});
}
}, 500);
});
}
private startIndexMonitoring() {
const reference_time = Date.now();
setInterval(() => {
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;
this.total_indexed_blocks += this.indexedObjects;
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 = [];
log_msg.push(`W:${_workers}`);
log_msg.push(`R:${(this.pushedBlocks + this.livePushedBlocks) / tScale}`);
log_msg.push(`C:${(this.liveConsumedBlocks + this.consumedBlocks) / tScale}`);
log_msg.push(`A:${(this.deserializedActions) / tScale}`);
log_msg.push(`D:${(this.deserializedDeltas) / tScale}`);
log_msg.push(`I:${this.indexedObjects / tScale}`);
if (this.total_blocks < this.total_range && !this.conf.indexer.live_only_mode) {
const remaining = this.total_range - this.total_blocks;
const estimated_time = Math.round(remaining / avg_consume_rate);
const 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}%)`);
}
// 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}
--------------------------------------------\n`);
this.range_completed = true;
}
// print monitoring log
if (this.conf.settings.rate_monitoring) {
hLog(log_msg.join(' | '));
}
if (this.indexedObjects === 0 && this.deserializedActions === 0 && this.consumedBlocks === 0) {
// 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 && (tScale * this.idle_count) >= this.auto_stop) {
hLog("Reached limit for no blocks processed, stopping now...");
process.exit(1);
} else {
hLog(`No blocks processed! Indexer will stop in ${this.auto_stop - (tScale * this.idle_count)} seconds!`);
}
}
} else {
if (this.idle_count > 1) {
hLog('Processing resumed!');
}
this.idle_count = 0;
if (this.shutdownTimer) {
clearTimeout(this.shutdownTimer);
this.shutdownTimer = null;
}
}
// reset counters
this.resetMonitoringCounters();
if (_workers === 0) {
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() {
this.workerMap.forEach((conf) => {
if (!conf.wref) {
conf['wref'] = cluster.fork(conf);
conf['wref'].on('message', (msg) => {
this.handleMessage(msg);
});
if (conf.worker_role === 'ds_pool_worker') {
this.dsPoolMap.set(conf.local_id, conf['wref']);
}
}
});
}
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}`);
}
}
async runMaster() {
this.printMode();
// Preview mode - prints only the proposed worker map
let preview = this.conf.settings.preview;
const queue_prefix = this.conf.settings.chain;
await this.purgeQueues();
// Chain API
this.rpc = this.manager.nodeosJsonRPC;
if (this.conf.settings.bp_monitoring) {
await this.getCurrentSchedule();
this.printActiveProds();
}
// 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}`);
} catch (e) {
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'}
];
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);
// Prepare Workers
this.workerMap = [];
this.worker_index = 0;
this.maxBatchSize = this.conf.scaling.batch_size;
// 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, queue_prefix);
hLog(`Last indexed block (deltas): ${lastIndexedBlock}`);
} else {
lastIndexedBlock = await getLastIndexedBlock(this.client, queue_prefix);
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) {
console.log(e.message);
console.error('failed to connect to chain api');
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;
}
this.lastIndexedABI = await getLastIndexedABI(this.client, queue_prefix);
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();
// Quit App if on preview mode
if (preview) {
HyperionMaster.printWorkerMap(this.workerMap);
await this.waitForLaunch();
}
// Setup Error Logging
this.setupDSElogs();
// Start Monitoring
this.startIndexMonitoring();
cluster.on('disconnect', (worker) => {
hLog(`The worker #${worker.id} has disconnected`);
});
// Launch all workers
this.launchWorkers();
if (this.conf.settings.ipc_debug_rate > 0 && this.conf.settings.ipc_debug_rate < 1000) {
hLog(`settings.ipc_debug_rate was set too low (${this.conf.settings.ipc_debug_rate}) using 1000 instead!`);
this.conf.settings.ipc_debug_rate = 1000;
}
if (this.conf.settings.ipc_debug_rate && this.conf.settings.ipc_debug_rate >= 1000) {
const rate = this.conf.settings.ipc_debug_rate;
this.totalMessages = 0;
setInterval(() => {
hLog(`IPC Messaging Rate: ${(this.totalMessages / (rate / 1000)).toFixed(2)} msg/s`);
this.totalMessages = 0;
}, rate);
}
// TODO: reimplement the indexer repair mode in typescript modules
// if (this.conf.indexer.repair_mode) {
// this.startRepairMode();
// }
this.startContractMonitoring();
this.monitorIndexingQueues();
this.onPm2Stop();
pm2io.action('get_usage_map', (reply) => {
reply(this.globalUsageMap);
});
pm2io.action('get_heap', (reply) => {
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]);
}, 1000);
const _listener = (msg) => {
if (msg.event === 'v8_heap_report') {
clearTimeout(_timeout);
worker.removeListener('message', _listener);
resolve([msg.id, msg.data]);
}
};
worker.on('message', _listener);
worker.send({event: 'request_v8_heap_stats'});
}));
}
}
Promise.all(requests).then((results) => {
const responses = {};
for (const result of results) {
if (result[1]) {
responses[result[0]] = result[1];
}
}
reply(responses);
});
});
}
}