Add support for efficient range checking to RangeSet

This commit is contained in:
Chris Clark
2015-07-09 13:08:58 -07:00
parent 1068b68568
commit f2f4173d7b
6 changed files with 95 additions and 125 deletions

View File

@@ -11,12 +11,7 @@ function hasCompleteLedgerRange(remote, options) {
const minLedgerVersion = options.minLedgerVersion || MIN_LEDGER_VERSION; const minLedgerVersion = options.minLedgerVersion || MIN_LEDGER_VERSION;
const maxLedgerVersion = options.maxLedgerVersion const maxLedgerVersion = options.maxLedgerVersion
|| remote.getLedgerSequence(); || remote.getLedgerSequence();
for (let i = minLedgerVersion; i <= maxLedgerVersion; i++) { return remote.getServer().hasLedgerRange(minLedgerVersion, maxLedgerVersion);
if (!remote.getServer().hasLedger(i)) { // TODO: optimize this
return false;
}
}
return true;
} }
function attachTransactionDate(remote, tx, callback) { function attachTransactionDate(remote, tx, callback) {

View File

@@ -18,13 +18,13 @@ exports.utils = require('./utils');
exports.Server = require('./server').Server; exports.Server = require('./server').Server;
exports.Ledger = require('./ledger').Ledger; exports.Ledger = require('./ledger').Ledger;
exports.TransactionQueue = require('./transactionqueue').TransactionQueue; exports.TransactionQueue = require('./transactionqueue').TransactionQueue;
exports.RangeSet = require('./rangeset').RangeSet;
exports.convertBase = require('./baseconverter'); exports.convertBase = require('./baseconverter');
exports._test = { exports._test = {
Log: require('./log'), Log: require('./log'),
PathFind: require('./pathfind').PathFind, PathFind: require('./pathfind').PathFind,
TransactionManager: require('./transactionmanager').TransactionManager TransactionManager: require('./transactionmanager').TransactionManager,
RangeSet: require('./rangeset').RangeSet
}; };
// Important: We do not guarantee any specific version of SJCL or for any // Important: We do not guarantee any specific version of SJCL or for any

View File

@@ -1,67 +1,61 @@
var assert = require('assert'); /* @flow */
var lodash = require('lodash'); 'use strict';
const _ = require('lodash');
const assert = require('assert');
const ranges = Symbol();
function RangeSet() { function mergeIntervals(intervals: Array<[number, number]>) {
this._ranges = [ ]; const stack = [[-Infinity, -Infinity]];
}; _.forEach(_.sortBy(intervals, x => x[0]), interval => {
const lastInterval = stack.pop();
if (interval[0] <= lastInterval[1] + 1) {
stack.push([lastInterval[0], Math.max(interval[1], lastInterval[1])]);
} else {
stack.push(lastInterval);
stack.push(interval);
}
});
return stack.slice(1);
}
/** class RangeSet {
* Add a ledger range constructor() {
* this.reset();
* @param {Number|String} range string (n-n2,n3-n4)
*/
RangeSet.prototype.add = function(range) {
assert(typeof range !== 'number' || !isNaN(range), 'Ledger range malformed');
range = String(range).split(',');
if (range.length > 1) {
return range.forEach(this.add, this);
} }
range = range[0].split('-').map(Number); reset() {
this[ranges] = [];
}
var lRange = { serialize() {
start: range[0], return this[ranges].map(range =>
end: range[range.length === 1 ? 0 : 1] range[0].toString() + '-' + range[1].toString()).join(',');
}; }
// Comparisons on NaN should be falsy addRange(start: number, end: number) {
assert(lRange.start <= lRange.end, 'Ledger range malformed'); assert(start <= end, 'invalid range');
this[ranges] = mergeIntervals(this[ranges].concat([[start, end]]));
}
var insertionPoint = lodash.sortedIndex(this._ranges, lRange, function(r) { addValue(value: number) {
return r.start; this.addRange(value, value);
}); }
this._ranges.splice(insertionPoint, 0, lRange); parseAndAddRanges(rangesString: string) {
}; const rangeStrings = rangesString.split(',');
_.forEach(rangeStrings, rangeString => {
const range = rangeString.split('-').map(Number);
this.addRange(range[0], range[1]);
});
}
containsRange(start: number, end: number) {
return _.some(this[ranges], range => range[0] <= start && range[1] >= end);
}
/* containsValue(value: number) {
* Check presence of ledger in range return this.containsRange(value, value);
* }
* @param {Number|String} ledger }
* @return Boolean
*/
RangeSet.prototype.has =
RangeSet.prototype.contains = function(ledger) {
assert(ledger != null && !isNaN(ledger), 'Ledger must be a number');
ledger = Number(ledger);
return this._ranges.some(function(r) {
return ledger >= r.start && ledger <= r.end;
});
};
/**
* Reset ledger ranges
*/
RangeSet.prototype.reset = function() {
this._ranges = [ ];
};
exports.RangeSet = RangeSet; exports.RangeSet = RangeSet;

View File

@@ -597,7 +597,7 @@ Server.prototype._handleMessage = function(message) {
Server.prototype._handleLedgerClosed = function(message) { Server.prototype._handleLedgerClosed = function(message) {
this._lastLedgerIndex = message.ledger_index; this._lastLedgerIndex = message.ledger_index;
this._lastLedgerClose = Date.now(); this._lastLedgerClose = Date.now();
this._ledgerRanges.add(message.ledger_index); this._ledgerRanges.addValue(message.ledger_index);
this._ledgerMap.set(message.ledger_hash, message.ledger_index); this._ledgerMap.set(message.ledger_hash, message.ledger_index);
this.emit('ledger_closed', message); this.emit('ledger_closed', message);
}; };
@@ -713,7 +713,7 @@ Server.prototype._handleResponseSubscribe = function(message) {
if (message.validated_ledgers) { if (message.validated_ledgers) {
// Add validated ledgers to ledger range set // Add validated ledgers to ledger range set
this._ledgerRanges.add(message.validated_ledgers); this._ledgerRanges.parseAndAddRanges(message.validated_ledgers);
} }
if (~Server.onlineStates.indexOf(message.server_status)) { if (~Server.onlineStates.indexOf(message.server_status)) {
@@ -913,12 +913,16 @@ Server.prototype.hasLedger = function(ledger) {
if (typeof ledger === 'string' && /^[A-F0-9]{64}$/.test(ledger)) { if (typeof ledger === 'string' && /^[A-F0-9]{64}$/.test(ledger)) {
result = this._ledgerMap.has(ledger); result = this._ledgerMap.has(ledger);
} else if (ledger !== null && !isNaN(ledger)) { } else if (ledger !== null && !isNaN(ledger)) {
result = this._ledgerRanges.has(ledger); result = this._ledgerRanges.containsValue(ledger);
} }
return result; return result;
}; };
Server.prototype.hasLedgerRange = function(startLedger, endLedger) {
return this._ledgerRanges.containsRange(startLedger, endLedger);
};
/** /**
* Get ledger index of last seen validated ledger * Get ledger index of last seen validated ledger
* *

View File

@@ -1,78 +1,55 @@
var assert = require('assert'); 'use strict';
var RangeSet = require('ripple-lib').RangeSet; const assert = require('assert');
const RangeSet = require('ripple-lib')._test.RangeSet;
describe('RangeSet', function() { describe('RangeSet', function() {
it('add()', function() { it('addRange()/addValue()', function() {
var r = new RangeSet(); const r = new RangeSet();
r.add('4-5'); r.addRange(4, 5);
r.add('7-10'); r.addRange(7, 10);
r.add('1-2'); r.addRange(1, 2);
r.add('3'); r.addValue(3);
assert.deepEqual(r._ranges, [ assert.deepEqual(r.serialize(), '1-5,7-10');
{ start: 1, end: 2 },
{ start: 3, end: 3 },
{ start: 4, end: 5 },
{ start: 7, end: 10 }
]);
}); });
it('add() -- malformed range', function() { it('addValue()/addRange() -- malformed', function() {
var r = new RangeSet(); const r = new RangeSet();
assert.throws(function() { assert.throws(function() {
r.add(null); r.addRange(2, 1);
});
assert.throws(function() {
r.add(void(0));
});
assert.throws(function() {
r.add('a');
});
assert.throws(function() {
r.add('2-1');
}); });
}); });
it('contains()', function() { it('parseAndAddRanges()', function() {
var r = new RangeSet(); const r = new RangeSet();
r.parseAndAddRanges('4-5,7-10,1-2,3-3');
r.add('32570-11005146'); assert.deepEqual(r.serialize(), '1-5,7-10');
r.add('11005147');
assert.strictEqual(r.contains(1), false);
assert.strictEqual(r.contains(32569), false);
assert.strictEqual(r.contains(32570), true);
assert.strictEqual(r.contains('32570'), true);
assert.strictEqual(r.contains(50000), true);
assert.strictEqual(r.contains(11005146), true);
assert.strictEqual(r.contains(11005147), true);
assert.strictEqual(r.contains(11005148), false);
assert.strictEqual(r.contains(12000000), false);
}); });
it('contains() -- invalid ledger', function() { it('containsValue()', function() {
var r = new RangeSet(); const r = new RangeSet();
assert.throws(function() { r.addRange(32570, 11005146);
r.contains(null); r.addValue(11005147);
});
assert.throws(function() { assert.strictEqual(r.containsValue(1), false);
r.contains(void(0)); assert.strictEqual(r.containsValue(32569), false);
}); assert.strictEqual(r.containsValue(32570), true);
assert.throws(function() { assert.strictEqual(r.containsValue(50000), true);
r.contains('a'); assert.strictEqual(r.containsValue(11005146), true);
}); assert.strictEqual(r.containsValue(11005147), true);
assert.strictEqual(r.containsValue(11005148), false);
assert.strictEqual(r.containsValue(12000000), false);
}); });
it('reset()', function() { it('reset()', function() {
var r = new RangeSet(); const r = new RangeSet();
r.add('4-5'); r.addRange(4, 5);
r.add('7-10'); r.addRange(7, 10);
r.reset(); r.reset();
assert.deepEqual(r._ranges, [ ]); assert.deepEqual(r.serialize(), '');
}); });
}); });

View File

@@ -257,8 +257,8 @@ describe('Request', function() {
}); });
}; };
servers[0]._ledgerRanges.add('5-6'); servers[0]._ledgerRanges.addRange(5, 6);
servers[1]._ledgerRanges.add('1-4'); servers[1]._ledgerRanges.addRange(1, 4);
const remote = new Remote(); const remote = new Remote();
remote._connected = true; remote._connected = true;