duniter-crawler
Version:
duniter-crawler ===============
868 lines (767 loc) • 30.2 kB
JavaScript
;
const util = require('util');
const stream = require('stream');
const co = require('co');
const _ = require('underscore');
const moment = require('moment');
const multimeter = require('multimeter');
const constants = require('./constants');
const pulling = require('./pulling');
const sandbox = require('./sandbox');
const connect = require('./connect');
const contacter = require('./contacter');
const tx_cleaner = require('./tx_cleaner');
const makeQuerablePromise = require('querablep');
const common = require('duniter-common');
const Peer = common.document.Peer;
const CONST_BLOCKS_CHUNK = 250;
const EVAL_REMAINING_INTERVAL = 1000;
const INITIAL_DOWNLOAD_SLOTS = 1;
module.exports = Synchroniser;
function Synchroniser (server, host, port, conf, interactive, slowOption) {
const that = this;
const logger = server.logger;
const dos2unix = common.dos2unix;
const hashf = common.hashf;
const rawer = common.rawer;
let speed = 0, blocksApplied = 0;
const baseWatcher = interactive ? new MultimeterWatcher() : new LoggerWatcher(logger);
// Wrapper to also push event stream
const watcher = {
writeStatus: baseWatcher.writeStatus,
downloadPercent: (pct) => {
if (pct !== undefined && baseWatcher.downloadPercent() < pct) {
that.push({ download: pct });
}
return baseWatcher.downloadPercent(pct);
},
appliedPercent: (pct) => {
if (pct !== undefined && baseWatcher.appliedPercent() < pct) {
that.push({ applied: pct });
}
return baseWatcher.appliedPercent(pct);
},
end: baseWatcher.end
};
stream.Duplex.call(this, { objectMode: true });
// Unused, but made mandatory by Duplex interface
this._read = () => null;
this._write = () => null;
if (interactive) {
logger.mute();
}
// Services
const PeeringService = server.PeeringService;
const BlockchainService = server.BlockchainService;
const contacterOptions = {
timeout: constants.SYNC_LONG_TIMEOUT
};
const dal = server.dal;
const logRemaining = (to) => co(function*() {
const lCurrent = yield dal.getCurrentBlockOrNull();
const localNumber = lCurrent ? lCurrent.number : -1;
if (to > 1 && speed > 0) {
const remain = (to - (localNumber + 1 + blocksApplied));
const secondsLeft = remain / speed;
const momDuration = moment.duration(secondsLeft * 1000);
watcher.writeStatus('Remaining ' + momDuration.humanize() + '');
}
});
this.test = (to, chunkLen, askedCautious) => co(function*() {
const peering = yield contacter.statics.fetchPeer(host, port, contacterOptions);
const node = yield connect(Peer.fromJSON(peering));
return node.getCurrent();
});
this.sync = (to, chunkLen, askedCautious, nopeers, noShufflePeers) => co(function*() {
try {
const peering = yield contacter.statics.fetchPeer(host, port, contacterOptions);
let peer = Peer.fromJSON(peering);
logger.info("Try with %s %s", peer.getURL(), peer.pubkey.substr(0, 6));
let node = yield connect(peer);
node.pubkey = peer.pubkey;
logger.info('Sync started.');
const fullSync = !to;
//============
// Blockchain headers
//============
logger.info('Getting remote blockchain info...');
watcher.writeStatus('Connecting to ' + host + '...');
const lCurrent = yield dal.getCurrentBlockOrNull();
const localNumber = lCurrent ? lCurrent.number : -1;
let rCurrent;
if (isNaN(to)) {
rCurrent = yield node.getCurrent();
} else {
rCurrent = yield node.getBlock(to);
}
to = rCurrent.number;
//=======
// Peers (just for P2P download)
//=======
let peers = [];
if (!nopeers && (to - localNumber > 1000)) { // P2P download if more than 1000 blocs
watcher.writeStatus('Peers...');
const merkle = yield dal.merkleForPeers();
const getPeers = node.getPeers.bind(node);
const json2 = yield getPeers({});
const rm = new NodesMerkle(json2);
if(rm.root() != merkle.root()){
const leavesToAdd = [];
const json = yield getPeers({ leaves: true });
_(json.leaves).forEach((leaf) => {
if(merkle.leaves().indexOf(leaf) == -1){
leavesToAdd.push(leaf);
}
});
peers = yield leavesToAdd.map((leaf) => co(function*() {
try {
const json3 = yield getPeers({ "leaf": leaf });
const jsonEntry = json3.leaf.value;
const endpoint = jsonEntry.endpoints[0];
watcher.writeStatus('Peer ' + endpoint);
return jsonEntry;
} catch (e) {
logger.warn("Could not get peer of leaf %s, continue...", leaf);
return null;
}
}));
}
else {
watcher.writeStatus('Peers already known');
}
}
if (!peers.length) {
peers.push(peer);
}
peers = peers.filter((p) => p);
//============
// Blockchain
//============
logger.info('Downloading Blockchain...');
// We use cautious mode if it is asked, or not particulary asked but blockchain has been started
const cautious = (askedCautious === true || localNumber >= 0);
const shuffledPeers = noShufflePeers ? peers : _.shuffle(peers);
const downloader = new P2PDownloader(localNumber, to, rCurrent.hash, shuffledPeers, watcher, logger, hashf, rawer, dal, slowOption);
downloader.start();
let lastPullBlock = null;
let dao = pulling.abstractDao({
// Get the local blockchain current block
localCurrent: () => co(function*() {
if (cautious) {
return yield dal.getCurrentBlockOrNull();
} else {
if (lCurrent && !lastPullBlock) {
lastPullBlock = lCurrent;
}
return lastPullBlock;
}
}),
// Get the remote blockchain (bc) current block
remoteCurrent: () => Promise.resolve(rCurrent),
// Get the remote peers to be pulled
remotePeers: () => co(function*() {
return [node];
}),
// Get block of given peer with given block number
getLocalBlock: (number) => dal.getBlock(number),
// Get block of given peer with given block number
getRemoteBlock: (thePeer, number) => co(function *() {
let block = null;
try {
block = yield node.getBlock(number);
tx_cleaner(block.transactions);
} catch (e) {
if (e.httpCode != 404) {
throw e;
}
}
return block;
}),
downloadBlocks: (thePeer, number) => co(function *() {
// Note: we don't care about the particular peer asked by the method. We use the network instead.
const numberOffseted = number - (localNumber + 1);
const targetChunk = Math.floor(numberOffseted / CONST_BLOCKS_CHUNK);
// Return the download promise! Simple.
return downloader.getChunk(targetChunk);
}),
applyBranch: (blocks) => co(function *() {
blocks = _.filter(blocks, (b) => b.number <= to);
if (cautious) {
for (const block of blocks) {
if (block.number == 0) {
yield BlockchainService.saveParametersForRootBlock(block);
}
yield dao.applyMainBranch(block);
}
} else {
yield server.BlockchainService.fastBlockInsertions(blocks, to)
}
lastPullBlock = blocks[blocks.length - 1];
watcher.appliedPercent(Math.floor(blocks[blocks.length - 1].number / to * 100));
return true;
}),
applyMainBranch: (block) => co(function *() {
const addedBlock = yield server.BlockchainService.submitBlock(block, true, constants.FORK_ALLOWED);
server.streamPush(addedBlock);
watcher.appliedPercent(Math.floor(block.number / to * 100));
}),
// Eventually remove forks later on
removeForks: () => co(function*() {}),
// Tells wether given peer is a member peer
isMemberPeer: (thePeer) => co(function *() {
let idty = yield dal.getWrittenIdtyByPubkey(thePeer.pubkey);
return (idty && idty.member) || false;
})
});
const logInterval = setInterval(() => logRemaining(to), EVAL_REMAINING_INTERVAL);
yield pulling.pull(conf, dao, logger);
// Finished blocks
watcher.downloadPercent(100.0);
watcher.appliedPercent(100.0);
if (logInterval) {
clearInterval(logInterval);
}
// Save currency parameters given by root block
const rootBlock = yield server.dal.getBlock(0);
yield BlockchainService.saveParametersForRootBlock(rootBlock);
server.dal.blockDAL.cleanCache();
//=======
// Sandboxes
//=======
watcher.writeStatus('Synchronizing the sandboxes...');
yield sandbox.pullSandboxToLocalServer(conf.currency, node, server, server.logger, watcher)
//=======
// Peers
//=======
if (!nopeers && fullSync) {
watcher.writeStatus('Peers...');
yield syncPeer(node);
const merkle = yield dal.merkleForPeers();
const getPeers = node.getPeers.bind(node);
const json2 = yield getPeers({});
const rm = new NodesMerkle(json2);
if(rm.root() != merkle.root()){
const leavesToAdd = [];
const json = yield getPeers({ leaves: true });
_(json.leaves).forEach((leaf) => {
if(merkle.leaves().indexOf(leaf) == -1){
leavesToAdd.push(leaf);
}
});
for (const leaf of leavesToAdd) {
try {
const json3 = yield getPeers({ "leaf": leaf });
const jsonEntry = json3.leaf.value;
const sign = json3.leaf.value.signature;
const entry = {};
["version", "currency", "pubkey", "endpoints", "block"].forEach((key) => {
entry[key] = jsonEntry[key];
});
entry.signature = sign;
watcher.writeStatus('Peer ' + entry.pubkey);
yield PeeringService.submitP(entry, false, to === undefined);
} catch (e) {
logger.warn(e);
}
}
}
else {
watcher.writeStatus('Peers already known');
}
}
watcher.end();
that.push({ sync: true });
logger.info('Sync finished.');
} catch (err) {
that.push({ sync: false, msg: err });
err && watcher.writeStatus(err.message || (err.uerr && err.uerr.message) || String(err));
watcher.end();
throw err;
}
});
//============
// Peer
//============
function syncPeer (node) {
// Global sync vars
const remotePeer = Peer.fromJSON({});
let remoteJsonPeer = {};
return co(function *() {
const json = yield node.getPeer();
remotePeer.version = json.version
remotePeer.currency = json.currency
remotePeer.pub = json.pub
remotePeer.endpoints = json.endpoints
remotePeer.blockstamp = json.block
remotePeer.signature = json.signature
const entry = remotePeer.getRawUnsigned();
const signature = dos2unix(remotePeer.signature);
// Parameters
if(!(entry && signature)){
throw 'Requires a peering entry + signature';
}
remoteJsonPeer = json;
remoteJsonPeer.pubkey = json.pubkey;
let signatureOK = PeeringService.checkPeerSignature(remoteJsonPeer);
if (!signatureOK) {
watcher.writeStatus('Wrong signature for peer #' + remoteJsonPeer.pubkey);
}
try {
yield PeeringService.submitP(remoteJsonPeer);
} catch (err) {
if (err.indexOf !== undefined && err.indexOf(constants.ERRORS.NEWER_PEER_DOCUMENT_AVAILABLE.uerr.message) !== -1 && err != constants.ERROR.PEER.UNKNOWN_REFERENCE_BLOCK) {
throw err;
}
}
});
}
}
function NodesMerkle (json) {
const that = this;
["depth", "nodesCount", "leavesCount"].forEach(function (key) {
that[key] = json[key];
});
this.merkleRoot = json.root;
// var i = 0;
// this.levels = [];
// while(json && json.levels[i]){
// this.levels.push(json.levels[i]);
// i++;
// }
this.root = function () {
return this.merkleRoot;
};
}
function MultimeterWatcher() {
const multi = multimeter(process);
const charm = multi.charm;
charm.on('^C', process.exit);
charm.reset();
multi.write('Progress:\n\n');
multi.write("Download: \n");
const downloadBar = multi("Download: \n".length, 3, {
width : 20,
solid : {
text : '|',
foreground : 'white',
background : 'blue'
},
empty : { text : ' ' }
});
multi.write("Apply: \n");
const appliedBar = multi("Apply: \n".length, 4, {
width : 20,
solid : {
text : '|',
foreground : 'white',
background : 'blue'
},
empty : { text : ' ' }
});
multi.write('\nStatus: ');
let xPos, yPos;
charm.position( (x, y) => {
xPos = x;
yPos = y;
});
const writtens = [];
this.writeStatus = (str) => {
writtens.push(str);
//require('fs').writeFileSync('writtens.json', JSON.stringify(writtens));
charm
.position(xPos, yPos)
.erase('end')
.write(str)
;
};
this.downloadPercent = (pct) => downloadBar.percent(pct);
this.appliedPercent = (pct) => appliedBar.percent(pct);
this.end = () => {
multi.write('\nAll done.\n');
multi.destroy();
};
downloadBar.percent(0);
appliedBar.percent(0);
}
function LoggerWatcher(logger) {
let downPct = 0, appliedPct = 0, lastMsg;
this.showProgress = () => logger.info('Downloaded %s%, Applied %s%', downPct, appliedPct);
this.writeStatus = (str) => {
if (str != lastMsg) {
lastMsg = str;
logger.info(str);
}
};
this.downloadPercent = (pct) => {
if (pct !== undefined) {
let changed = pct > downPct;
downPct = pct;
if (changed) this.showProgress();
}
return downPct;
};
this.appliedPercent = (pct) => {
if (pct !== undefined) {
let changed = pct > appliedPct;
appliedPct = pct;
if (changed) this.showProgress();
}
return appliedPct;
};
this.end = () => {
};
}
function P2PDownloader(localNumber, to, toHash, peers, watcher, logger, hashf, rawer, dal, slowOption) {
const that = this;
const PARALLEL_PER_CHUNK = 1;
const MAX_DELAY_PER_DOWNLOAD = 15000;
const NO_NODES_AVAILABLE = "No node available for download";
const TOO_LONG_TIME_DOWNLOAD = "No answer after " + MAX_DELAY_PER_DOWNLOAD + "ms, will retry download later.";
const nbBlocksToDownload = Math.max(0, to - localNumber);
const numberOfChunksToDownload = Math.ceil(nbBlocksToDownload / CONST_BLOCKS_CHUNK);
const chunks = Array.from({ length: numberOfChunksToDownload }).map(() => null);
const processing = Array.from({ length: numberOfChunksToDownload }).map(() => false);
const handler = Array.from({ length: numberOfChunksToDownload }).map(() => null);
const resultsDeferers = Array.from({ length: numberOfChunksToDownload }).map(() => null);
const resultsData = Array.from({ length: numberOfChunksToDownload }).map((unused, index) => new Promise((resolve, reject) => {
resultsDeferers[index] = { resolve, reject };
}));
// Create slots of download, in a ready stage
let downloadSlots = slowOption ? 1 : Math.min(INITIAL_DOWNLOAD_SLOTS, peers.length);
let nodes = {};
let nbDownloadsTried = 0, nbDownloading = 0;
let lastAvgDelay = MAX_DELAY_PER_DOWNLOAD;
let aSlotWasAdded = false;
/**
* Get a list of P2P nodes to use for download.
* If a node is not yet correctly initialized (we can test a node before considering it good for downloading), then
* this method would not return it.
*/
const getP2Pcandidates = () => co(function*() {
let promises = peers.reduce((chosens, other, index) => {
if (!nodes[index]) {
// Create the node
let p = Peer.fromJSON(peers[index]);
nodes[index] = makeQuerablePromise(co(function*() {
// We wait for the download process to be triggered
// yield downloadStarter;
// if (nodes[index - 1]) {
// try { yield nodes[index - 1]; } catch (e) {}
// }
const node = yield connect(p);
// We initialize nodes with the near worth possible notation
node.tta = 1;
node.nbSuccess = 0;
return node;
}));
chosens.push(nodes[index]);
} else {
chosens.push(nodes[index]);
}
// Continue
return chosens;
}, []);
let candidates = yield promises;
candidates.forEach((c) => {
c.tta = c.tta || 0; // By default we say a node is super slow to answer
c.ttas = c.ttas || []; // Memorize the answer delays
});
if (candidates.length === 0) {
throw NO_NODES_AVAILABLE;
}
// We remove the nodes impossible to reach (timeout)
let withGoodDelays = _.filter(candidates, (c) => c.tta <= MAX_DELAY_PER_DOWNLOAD);
if (withGoodDelays.length === 0) {
// No node can be reached, we can try to lower the number of nodes on which we download
downloadSlots = Math.floor(downloadSlots / 2);
// We reinitialize the nodes
nodes = {};
// And try it all again
return getP2Pcandidates();
}
const parallelMax = Math.min(PARALLEL_PER_CHUNK, withGoodDelays.length);
withGoodDelays = _.sortBy(withGoodDelays, (c) => c.tta);
withGoodDelays = withGoodDelays.slice(0, parallelMax);
// We temporarily augment the tta to avoid asking several times to the same node in parallel
withGoodDelays.forEach((c) => c.tta = MAX_DELAY_PER_DOWNLOAD);
return withGoodDelays;
});
/**
* Download a chunk of blocks using P2P network through BMA API.
* @param from The starting block to download
* @param count The number of blocks to download.
* @param chunkIndex The # of the chunk in local algorithm (logging purposes only)
*/
const p2pDownload = (from, count, chunkIndex) => co(function*() {
let candidates = yield getP2Pcandidates();
// Book the nodes
return yield raceOrCancelIfTimeout(MAX_DELAY_PER_DOWNLOAD, candidates.map((node) => co(function*() {
try {
const start = Date.now();
handler[chunkIndex] = node;
node.downloading = true;
nbDownloading++;
watcher.writeStatus('Getting chunck #' + chunkIndex + '/' + (numberOfChunksToDownload - 1) + ' from ' + from + ' to ' + (from + count - 1) + ' on peer ' + [node.host, node.port].join(':'));
let blocks = yield node.getBlocks(count, from);
node.ttas.push(Date.now() - start);
// Only keep a flow of 5 ttas for the node
if (node.ttas.length > 5) node.ttas.shift();
// Average time to answer
node.tta = Math.round(node.ttas.reduce((sum, tta) => sum + tta, 0) / node.ttas.length);
watcher.writeStatus('GOT chunck #' + chunkIndex + '/' + (numberOfChunksToDownload - 1) + ' from ' + from + ' to ' + (from + count - 1) + ' on peer ' + [node.host, node.port].join(':'));
node.nbSuccess++;
// Opening/Closing slots depending on the Interne connection
if (slots.length == downloadSlots) {
const peers = yield Object.values(nodes);
const downloading = _.filter(peers, (p) => p.downloading && p.ttas.length);
const currentAvgDelay = downloading.reduce((sum, c) => {
const tta = Math.round(c.ttas.reduce((sum, tta) => sum + tta, 0) / c.ttas.length);
return sum + tta;
}, 0) / downloading.length;
// Opens or close downloading slots
if (!slowOption) {
// Check the impact of an added node (not first time)
if (!aSlotWasAdded) {
// We try to add a node
const newValue = Math.min(peers.length, downloadSlots + 1);
if (newValue !== downloadSlots) {
downloadSlots = newValue;
aSlotWasAdded = true;
logger.info('AUGMENTED DOWNLOAD SLOTS! Now has %s slots', downloadSlots);
}
} else {
aSlotWasAdded = false;
const decelerationPercent = currentAvgDelay / lastAvgDelay - 1;
const addedNodePercent = 1 / nbDownloading;
logger.info('Deceleration = %s (%s/%s), AddedNodePercent = %s', decelerationPercent, currentAvgDelay, lastAvgDelay, addedNodePercent);
if (decelerationPercent > addedNodePercent) {
downloadSlots = Math.max(1, downloadSlots - 1); // We reduce the number of slots, but we keep at least 1 slot
logger.info('REDUCED DOWNLOAD SLOT! Now has %s slots', downloadSlots);
}
}
}
lastAvgDelay = currentAvgDelay;
}
nbDownloadsTried++;
nbDownloading--;
node.downloading = false;
return blocks;
} catch (e) {
nbDownloading--;
node.downloading = false;
nbDownloadsTried++;
node.ttas.push(MAX_DELAY_PER_DOWNLOAD + 1); // No more ask on this node
// Average time to answer
node.tta = Math.round(node.ttas.reduce((sum, tta) => sum + tta, 0) / node.ttas.length);
throw e;
}
})));
});
/**
* Function for downloading a chunk by its number.
* @param index Number of the chunk.
*/
const downloadChunk = (index) => co(function*() {
// The algorithm to download a chunk
const from = localNumber + 1 + index * CONST_BLOCKS_CHUNK;
let count = CONST_BLOCKS_CHUNK;
if (index == numberOfChunksToDownload - 1) {
count = nbBlocksToDownload % CONST_BLOCKS_CHUNK || CONST_BLOCKS_CHUNK;
}
try {
const fileName = "blockchain/chunk_" + index + "-" + CONST_BLOCKS_CHUNK + ".json";
if (localNumber <= 0 && (yield dal.confDAL.coreFS.exists(fileName))) {
handler[index] = {
host: 'filesystem',
port: 'blockchain',
resetFunction: () => dal.confDAL.coreFS.remove(fileName)
};
const chunk = (yield dal.confDAL.coreFS.readJSON(fileName)).blocks;
return chunk;
} else {
const chunk = yield p2pDownload(from, count, index);
// Store the file to avoid re-downloading
if (localNumber <= 0 && chunk.length === CONST_BLOCKS_CHUNK) {
yield dal.confDAL.coreFS.makeTree('blockchain');
yield dal.confDAL.coreFS.writeJSON(fileName, { blocks: chunk });
}
return chunk;
}
} catch (e) {
logger.error(e);
return downloadChunk(index);
}
});
const slots = [];
const downloads = {};
/**
* Utility function that starts a race between promises but cancels it if no answer is found before `timeout`
* @param timeout
* @param races
* @returns {Promise}
*/
const raceOrCancelIfTimeout = (timeout, races) => {
return Promise.race([
// Process the race, but cancel it if we don't get an anwser quickly enough
new Promise((resolve, reject) => {
setTimeout(() => {
reject(TOO_LONG_TIME_DOWNLOAD);
}, MAX_DELAY_PER_DOWNLOAD);
})
].concat(races));
};
/**
* Triggers for starting the download.
*/
let startResolver;
const downloadStarter = new Promise((resolve) => startResolver = resolve);
const chainsCorrectly = (blocks, index) => co(function*() {
if (!blocks.length) {
logger.error('No block was downloaded');
return false;
}
for (let i = blocks.length - 1; i > 0; i--) {
if (blocks[i].number !== blocks[i - 1].number + 1 || blocks[i].previousHash !== blocks[i - 1].hash) {
logger.error("Blocks do not chaing correctly", blocks[i].number);
return false;
}
if (blocks[i].version != blocks[i - 1].version && blocks[i].version != blocks[i - 1].version + 1) {
logger.error("Version cannot be downgraded", blocks[i].number);
return false;
}
}
// Check hashes
for (let i = 0; i < blocks.length; i++) {
// Note: the hash, in Duniter, is made only on the **signing part** of the block: InnerHash + Nonce
if (blocks[i].version >= 6) {
for (const tx of blocks[i].transactions) {
tx.version = constants.TRANSACTION_VERSION;
}
}
if (blocks[i].inner_hash !== hashf(rawer.getBlockInnerPart(blocks[i])).toUpperCase()) {
logger.error("Inner hash of block#%s from %s does not match", blocks[i].number);
return false;
}
if (blocks[i].hash !== hashf(rawer.getBlockInnerHashAndNonceWithSignature(blocks[i])).toUpperCase()) {
logger.error("Hash of block#%s from %s does not match", blocks[i].number);
return false;
}
}
const lastBlockOfChunk = blocks[blocks.length - 1];
if ((lastBlockOfChunk.number == to || blocks.length < CONST_BLOCKS_CHUNK) && lastBlockOfChunk.hash != toHash) {
// Top chunk
logger.error('Top block is not on the right chain');
return false;
} else {
// Chaining between downloads
const previousChunk = yield that.getChunk(index + 1);
const blockN = blocks[blocks.length - 1]; // The block n
const blockNp1 = previousChunk[0]; // The block n + 1
if (blockN && blockNp1 && (blockN.number + 1 !== blockNp1.number || blockN.hash != blockNp1.previousHash)) {
logger.error('Chunk is not referenced by the upper one');
return false;
}
}
return true;
});
/**
* Download worker
* @type {*|Promise} When finished.
*/
co(function*() {
try {
yield downloadStarter;
let doneCount = 0, resolvedCount = 0;
while (resolvedCount < chunks.length) {
doneCount = 0;
resolvedCount = 0;
// Add as much possible downloads as possible, and count the already done ones
for (let i = chunks.length - 1; i >= 0; i--) {
if (chunks[i] === null && !processing[i] && slots.indexOf(i) === -1 && slots.length < downloadSlots) {
slots.push(i);
processing[i] = true;
downloads[i] = makeQuerablePromise(downloadChunk(i)); // Starts a new download
} else if (downloads[i] && downloads[i].isFulfilled() && processing[i]) {
doneCount++;
}
// We count the number of perfectly downloaded & validated chunks
if (chunks[i]) {
resolvedCount++;
}
}
watcher.downloadPercent(Math.round(doneCount / numberOfChunksToDownload * 100));
let races = slots.map((i) => downloads[i]);
if (races.length) {
try {
yield raceOrCancelIfTimeout(MAX_DELAY_PER_DOWNLOAD, races);
} catch (e) {
logger.warn(e);
}
for (let i = 0; i < slots.length; i++) {
// We must know the index of what resolved/rejected to free the slot
const doneIndex = slots.reduce((found, realIndex, index) => {
if (found !== null) return found;
if (downloads[realIndex].isFulfilled()) return index;
return null;
}, null);
if (doneIndex !== null) {
const realIndex = slots[doneIndex];
if (downloads[realIndex].isResolved()) {
// IIFE to be safe about `realIndex`
(function() {
co(function*() {
const blocks = yield downloads[realIndex];
if (realIndex < chunks.length - 1) {
// We must wait for NEXT blocks to be STRONGLY validated before going any further, otherwise we
// could be on the wrong chain
yield that.getChunk(realIndex + 1);
}
const chainsWell = yield chainsCorrectly(blocks, realIndex);
if (chainsWell) {
// Chunk is COMPLETE
logger.warn("Chunk #%s is COMPLETE from %s", realIndex, [handler[realIndex].host, handler[realIndex].port].join(':'));
chunks[realIndex] = blocks;
resultsDeferers[realIndex].resolve(chunks[realIndex]);
} else {
logger.warn("Chunk #%s DOES NOT CHAIN CORRECTLY from %s", realIndex, [handler[realIndex].host, handler[realIndex].port].join(':'));
// Penality on this node to avoid its usage
if (handler[realIndex].resetFunction) {
yield handler[realIndex].resetFunction();
}
if (handler[realIndex].tta !== undefined) {
handler[realIndex].tta += MAX_DELAY_PER_DOWNLOAD;
}
// Need a retry
processing[realIndex] = false;
}
});
})(realIndex);
} else {
processing[realIndex] = false; // Need a retry
}
slots.splice(doneIndex, 1);
}
}
}
// Wait a bit
yield new Promise((resolve, reject) => setTimeout(resolve, 10));
}
} catch (e) {
logger.error('Fatal error in the downloader:');
logger.error(e);
}
});
/**
* PUBLIC API
*/
/***
* Triggers the downloading
*/
this.start = () => startResolver();
/***
* Promises a chunk to be downloaded and returned
* @param index The number of the chunk to download & return
*/
this.getChunk = (index) => resultsData[index] || Promise.resolve([]);
}
util.inherits(Synchroniser, stream.Duplex);