From f2f4173d7b63be2035470719a589c903980d2095 Mon Sep 17 00:00:00 2001 From: Chris Clark Date: Thu, 9 Jul 2015 13:08:58 -0700 Subject: [PATCH] Add support for efficient range checking to RangeSet --- src/api/ledger/transaction.js | 7 +-- src/core/index.js | 4 +- src/core/rangeset.js | 104 ++++++++++++++++------------------ src/core/server.js | 10 +++- test/rangeset-test.js | 91 +++++++++++------------------ test/request-test.js | 4 +- 6 files changed, 95 insertions(+), 125 deletions(-) diff --git a/src/api/ledger/transaction.js b/src/api/ledger/transaction.js index 80818e25..9088cc8d 100644 --- a/src/api/ledger/transaction.js +++ b/src/api/ledger/transaction.js @@ -11,12 +11,7 @@ function hasCompleteLedgerRange(remote, options) { const minLedgerVersion = options.minLedgerVersion || MIN_LEDGER_VERSION; const maxLedgerVersion = options.maxLedgerVersion || remote.getLedgerSequence(); - for (let i = minLedgerVersion; i <= maxLedgerVersion; i++) { - if (!remote.getServer().hasLedger(i)) { // TODO: optimize this - return false; - } - } - return true; + return remote.getServer().hasLedgerRange(minLedgerVersion, maxLedgerVersion); } function attachTransactionDate(remote, tx, callback) { diff --git a/src/core/index.js b/src/core/index.js index 19e27bca..3c31033f 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -18,13 +18,13 @@ exports.utils = require('./utils'); exports.Server = require('./server').Server; exports.Ledger = require('./ledger').Ledger; exports.TransactionQueue = require('./transactionqueue').TransactionQueue; -exports.RangeSet = require('./rangeset').RangeSet; exports.convertBase = require('./baseconverter'); exports._test = { Log: require('./log'), 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 diff --git a/src/core/rangeset.js b/src/core/rangeset.js index 9ba5b505..02aa39dd 100644 --- a/src/core/rangeset.js +++ b/src/core/rangeset.js @@ -1,67 +1,61 @@ -var assert = require('assert'); -var lodash = require('lodash'); +/* @flow */ +'use strict'; +const _ = require('lodash'); +const assert = require('assert'); +const ranges = Symbol(); -function RangeSet() { - this._ranges = [ ]; -}; +function mergeIntervals(intervals: Array<[number, number]>) { + 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); +} -/** - * Add a ledger range - * - * @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); +class RangeSet { + constructor() { + this.reset(); } - range = range[0].split('-').map(Number); + reset() { + this[ranges] = []; + } - var lRange = { - start: range[0], - end: range[range.length === 1 ? 0 : 1] - }; + serialize() { + return this[ranges].map(range => + range[0].toString() + '-' + range[1].toString()).join(','); + } - // Comparisons on NaN should be falsy - assert(lRange.start <= lRange.end, 'Ledger range malformed'); + addRange(start: number, end: number) { + assert(start <= end, 'invalid range'); + this[ranges] = mergeIntervals(this[ranges].concat([[start, end]])); + } - var insertionPoint = lodash.sortedIndex(this._ranges, lRange, function(r) { - return r.start; - }); + addValue(value: number) { + 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); + } -/* - * Check presence of ledger in range - * - * @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 = [ ]; -}; + containsValue(value: number) { + return this.containsRange(value, value); + } +} exports.RangeSet = RangeSet; diff --git a/src/core/server.js b/src/core/server.js index ca678280..fff5842e 100644 --- a/src/core/server.js +++ b/src/core/server.js @@ -597,7 +597,7 @@ Server.prototype._handleMessage = function(message) { Server.prototype._handleLedgerClosed = function(message) { this._lastLedgerIndex = message.ledger_index; 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.emit('ledger_closed', message); }; @@ -713,7 +713,7 @@ Server.prototype._handleResponseSubscribe = function(message) { if (message.validated_ledgers) { // 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)) { @@ -913,12 +913,16 @@ Server.prototype.hasLedger = function(ledger) { if (typeof ledger === 'string' && /^[A-F0-9]{64}$/.test(ledger)) { result = this._ledgerMap.has(ledger); } else if (ledger !== null && !isNaN(ledger)) { - result = this._ledgerRanges.has(ledger); + result = this._ledgerRanges.containsValue(ledger); } return result; }; +Server.prototype.hasLedgerRange = function(startLedger, endLedger) { + return this._ledgerRanges.containsRange(startLedger, endLedger); +}; + /** * Get ledger index of last seen validated ledger * diff --git a/test/rangeset-test.js b/test/rangeset-test.js index 61d48583..98cbb47a 100644 --- a/test/rangeset-test.js +++ b/test/rangeset-test.js @@ -1,78 +1,55 @@ -var assert = require('assert'); -var RangeSet = require('ripple-lib').RangeSet; +'use strict'; +const assert = require('assert'); +const RangeSet = require('ripple-lib')._test.RangeSet; describe('RangeSet', function() { - it('add()', function() { - var r = new RangeSet(); + it('addRange()/addValue()', function() { + const r = new RangeSet(); - r.add('4-5'); - r.add('7-10'); - r.add('1-2'); - r.add('3'); + r.addRange(4, 5); + r.addRange(7, 10); + r.addRange(1, 2); + r.addValue(3); - assert.deepEqual(r._ranges, [ - { start: 1, end: 2 }, - { start: 3, end: 3 }, - { start: 4, end: 5 }, - { start: 7, end: 10 } - ]); + assert.deepEqual(r.serialize(), '1-5,7-10'); }); - it('add() -- malformed range', function() { - var r = new RangeSet(); - + it('addValue()/addRange() -- malformed', function() { + const r = new RangeSet(); assert.throws(function() { - r.add(null); - }); - assert.throws(function() { - r.add(void(0)); - }); - assert.throws(function() { - r.add('a'); - }); - assert.throws(function() { - r.add('2-1'); + r.addRange(2, 1); }); }); - it('contains()', function() { - var r = new RangeSet(); - - r.add('32570-11005146'); - 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('parseAndAddRanges()', function() { + const r = new RangeSet(); + r.parseAndAddRanges('4-5,7-10,1-2,3-3'); + assert.deepEqual(r.serialize(), '1-5,7-10'); }); - it('contains() -- invalid ledger', function() { - var r = new RangeSet(); + it('containsValue()', function() { + const r = new RangeSet(); - assert.throws(function() { - r.contains(null); - }); - assert.throws(function() { - r.contains(void(0)); - }); - assert.throws(function() { - r.contains('a'); - }); + r.addRange(32570, 11005146); + r.addValue(11005147); + + assert.strictEqual(r.containsValue(1), false); + assert.strictEqual(r.containsValue(32569), false); + assert.strictEqual(r.containsValue(32570), true); + assert.strictEqual(r.containsValue(50000), true); + 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() { - var r = new RangeSet(); + const r = new RangeSet(); - r.add('4-5'); - r.add('7-10'); + r.addRange(4, 5); + r.addRange(7, 10); r.reset(); - assert.deepEqual(r._ranges, [ ]); + assert.deepEqual(r.serialize(), ''); }); }); diff --git a/test/request-test.js b/test/request-test.js index 8c3b6927..17122029 100644 --- a/test/request-test.js +++ b/test/request-test.js @@ -257,8 +257,8 @@ describe('Request', function() { }); }; - servers[0]._ledgerRanges.add('5-6'); - servers[1]._ledgerRanges.add('1-4'); + servers[0]._ledgerRanges.addRange(5, 6); + servers[1]._ledgerRanges.addRange(1, 4); const remote = new Remote(); remote._connected = true;