ndn-js
Version:
A JavaScript client library for Named Data Networking
400 lines (356 loc) • 14.9 kB
JavaScript
/**
* Copyright (C) 2018-2019 Regents of the University of California.
* @author: Chavoosh Ghasemi <chghasemi@cs.arizona.edu>
* @author: Jeff Thompson <jefft0@remap.ucla.edu>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* A copy of the GNU Lesser General Public License is in the file COPYING.
*/
/** @ignore */
var Interest = require('../interest.js').Interest; /** @ignore */
var Blob = require('./blob.js').Blob; /** @ignore */
var KeyChain = require('../security/key-chain.js').KeyChain; /** @ignore */
var NdnCommon = require('./ndn-common.js').NdnCommon; /** @ignore */
var RttEstimator = require('./rtt-estimator.js').RttEstimator; /** @ignore */
var DataFetcher = require('./data-fetcher.js').DataFetcher; /** @ignore */
var Pipeline = require('./pipeline.js').Pipeline;
var LOG = require('../log.js').Log.LOG;
/**
* Retrieve the segments of solicited data by keeping a fixed-size window of N
* of in fly Interests at any given time.
*
* To handle timeout and nack we use DataFetcher class which upon facing
* timeout or nack will try to resolve the corresponded segment by retransmitting
* the Interest a few times.
*
* After discovering the version number from the very first Data packet
* (see Pipeline documentation), then:
*
* 1. Pipeline the Interests starting from segment 0:
*
* >> Interest: /<prefix>/<version>/<segment=0>
* >> Interest: /<prefix>/<version>/<segment=1>
* ...
* >> Interest: /<prefix>/<version>/<segment=this.windowSize-1>
*
* We do not issue interest for segments that are already received.
* At any given time the number of on the fly Interests should be equal
* to this.windowSize. The next expected segment to fetch will be this.windowSize
*
* 2. Upon receiving a valid Data back we pipeline an Interest for the
* next expected segment.
*
* We repeat step 2 until the FinalBlockId === Data.getName().get(-1).
*
* 3. Call the onComplete callback with a Blob that concatenates the content
* from all segments.
*
* If an error occurs during the fetching process, the onError callback is called
* with a proper error code (see Pipeline documentation).
*
* This is a public constructor to create a new PipelineFixed.
* @param {Interest} baseInterest This interest should be well-formed to represent all necessary fields
* that the application wants to use for all Interests (e.g., interest
* lifetime).
* @param {Face} face The segments will be fetched through this face.
* @param {Object} opts An object that can contain pipeline options to overwrite their default values.
* If null is passed then all pipeline options will be set to their default values.
* @param {KeyChain} validatorKeyChain If this is not null, use its verifyData otherwise skip
* Data validation.
* @param {function} onComplete When all segments are received, call onComplete(content) where content
* is a Blob which has the concatenation of the content of all the segments.
* NOTE: The library will log any exceptions thrown by this callback, but for better error handling the
* callback should catch and properly handle any exceptions.
* @param {function} onError Call onError(errorCode, message) for any error during content retrieval
* (see Pipeline documentation for ful list of errors).
* NOTE: The library will log any exceptions thrown by this callback, but for better error handling the
* callback should catch and properly handle any exceptions.
* @param {Object} stats An object that exposes statistics of content retrieval performance to caller.
* @constructor
*/
var PipelineFixed = function PipelineFixed
(baseInterest, face, opts, validatorKeyChain, onComplete, onError, stats)
{
this.face = face;
this.validatorKeyChain = validatorKeyChain;
this.onComplete = onComplete;
this.onError = onError;
this.pipeline = new Pipeline(baseInterest);
this.nInFlight = 0;
// Options
this.windowSize = Pipeline.op("windowSize", 10, opts);
this.maxRetriesOnTimeoutOrNack = Pipeline.op("maxRetriesOnTimeoutOrNack", 3, opts);
this.segmentInfo = []; // track information that is necessary for segment transmission.
// If a segment experienced retransmission its status will
// be `retx`, otherwise `normal`
this.dataFetchersContainer = []; // if we need to cancel pending interests
this.rttEstimator = new RttEstimator(opts);
// Stats collector
this.stats = stats != null ? stats : {};
this.stats.nTimeouts = 0;
this.stats.nNacks = 0;
this.stats.nRetransmitted = 0;
};
exports.PipelineFixed = PipelineFixed;
PipelineFixed.prototype.run = function()
{
this.stats.pipelineStartTime = Date.now();
var interest = this.pipeline.makeInterest(0);
if (Number.isNaN(this.pipeline.versionNo) ) {
interest.setMustBeFresh(true);
}
else {
interest.setMustBeFresh(false);
}
this.sendInterest(interest);
};
/**
* Use DataFetcher to fetch the solicited segment. Timeouts and Nacks will be
* handled by DataFetcher class.
*/
PipelineFixed.prototype.sendInterest = function(interest)
{
if (this.pipeline.isStopped)
return;
if (this.pipeline.hasFailure)
return;
var segmentNo = 0;
if (interest.getName().components.length > 0 && interest.getName().get(-1).isSegment()) {
segmentNo = interest.getName().get(-1).toSegment();
this.dataFetchersContainer[segmentNo] = new DataFetcher
(this.face, interest, this.maxRetriesOnTimeoutOrNack,
this.handleData.bind(this), this.handleFailure.bind(this),
this.segmentInfo, this.stats);
this.dataFetchersContainer[segmentNo].fetch();
}
else { // this is the very first interest
this.dataFetchersContainer[segmentNo] = new DataFetcher
(this.face, interest, this.maxRetriesOnTimeoutOrNack,
this.handleData.bind(this), this.handleFailure.bind(this),
this.segmentInfo, this.stats);
this.dataFetchersContainer[segmentNo].fetch();
}
};
PipelineFixed.prototype.sendNextInterests = function()
{
if (this.pipeline.isStopped)
return;
while (this.pipeline.nextSegmentNo <= this.pipeline.finalBlockId && this.nInFlight <= this.windowSize) {
// do not re-send an interest for existing segments
if (this.pipeline.contentParts[this.pipeline.nextSegmentNo] !== undefined) {
this.pipeline.getNextSegmentNo();
continue;
}
var interest = this.pipeline.makeInterest(this.pipeline.getNextSegmentNo());
// Changing a field clears the nonce so that a new nonce will be generated
interest.setMustBeFresh(false);
interest.refreshNonce();
this.sendInterest(interest);
this.nInFlight++;
}
};
PipelineFixed.prototype.cancelInFlightSegmentsGreaterThan = function(segNo)
{
var len = this.dataFetchersContainer.length;
for (var i = segNo + 1; i < len; ++i) {
if (this.dataFetchersContainer[i] !== null) {
this.face.removePendingInterest(this.dataFetchersContainer[i].getPendingInterestId());
}
}
};
PipelineFixed.prototype.handleData = function(interest, data)
{
if (this.validatorKeyChain !== null) {
try {
var thisPipeline = this;
this.validatorKeyChain.verifyData
(data,
function(localData) {
thisPipeline.onData(localData);
},
this.onValidationFailed.bind(this));
}
catch (ex) {
Pipeline.reportError(this.onError, Pipeline.ErrorCode.SEGMENT_VERIFICATION_FAILED,
"Error in KeyChain.verifyData: " + ex);
return;
}
}
else {
this.onData(data);
}
};
PipelineFixed.prototype.onData = function(data)
{
if (this.pipeline.isStopped)
return;
var recSegmentNo = 0;
try {
recSegmentNo = data.getName().get(-1).toSegment();
}
catch (ex) {
this.handleFailure(recSegmentNo, Pipeline.ErrorCode.DATA_HAS_NO_SEGMENT,
"Error while decoding the segment number " +
data.getName().get(-1).toEscapedString() + ": " + ex);
return;
}
if (Number.isNaN(this.pipeline.versionNo)) {
try {
this.pipeline.versionNo = data.getName().get(-2).toVersion();
}
catch (ex) {
this.handleFailure(recSegmentNo, Pipeline.ErrorCode.DATA_HAS_NO_VERSION,
"Error while decoding the version number " +
data.getName().get(-2).toEscapedString() + ": " + ex);
return;
}
}
// set finalBlockId
if (data.getMetaInfo().getFinalBlockId().getValue().size() > 0) {
try {
this.pipeline.finalBlockId = data.getMetaInfo().getFinalBlockId().toSegment();
this.cancelInFlightSegmentsGreaterThan(this.pipeline.finalBlockId);
}
catch (ex) {
Pipeline.reportError(this.onError, Pipeline.ErrorCode.DATA_HAS_NO_SEGMENT,
"Error while decoding the FinalBlockId field " +
data.getMetaInfo().getFinalBlockId().toEscapedString() +
": " + ex);
return;
}
this.pipeline.hasFinalBlockId = true;
}
// Save the content
this.pipeline.contentParts[recSegmentNo] = data.getContent().buf();
if (this.pipeline.hasFailure && this.pipeline.hasFinalBlockId) {
if(this.pipeline.finalBlockId >= this.pipeline.failedSegNo) {
// Previously failed segment is part of the content
return this.pipeline.onFailure(this.pipeline.failureErrorCode,
this.pipeline.failureReason,
this.onError);
}
else {
this.pipeline.hasFailure = false;
}
}
var recSeg = this.segmentInfo[recSegmentNo];
if (recSeg === undefined) {
return; // ignore an already-received segment
}
var rtt = Date.now() - recSeg.timeSent;
var fullDelay = Date.now() - recSeg.initTimeSent;
if (LOG > 1) {
console.log ("Received segment #" + recSegmentNo
+ ", rtt=" + rtt + "ms");
}
// Do not sample RTT for retransmitted segments
if (this.segmentInfo[recSegmentNo].stat === "normal") {
var nExpectedSamples = Math.max((this.nInFlight + 1) >> 1, 1);
if (nExpectedSamples <= 0) {
this.handleFailure(-1, Pipeline.ErrorCode.MISC, "nExpectedSamples is less than or equal to ZERO.");
}
this.rttEstimator.addMeasurement(recSegmentNo, rtt, nExpectedSamples);
this.rttEstimator.addDelayMeasurement(recSegmentNo, Math.max(rtt, fullDelay));
}
else { // Sample the retrieval delay to calculate jitter
this.rttEstimator.addDelayMeasurement(recSegmentNo, Math.max(rtt, fullDelay));
}
this.pipeline.numberOfSatisfiedSegments++;
// Check whether we are finished
if (this.pipeline.hasFinalBlockId &&
this.pipeline.numberOfSatisfiedSegments > this.pipeline.finalBlockId) {
// Concatenate to get content.
var content = Buffer.concat(this.pipeline.contentParts);
this.cancelInFlightSegmentsGreaterThan(this.pipeline.finalBlockId);
this.stats.avgRtt = this.rttEstimator.getAvgRtt().toPrecision(3),
this.stats.avgJitter = this.rttEstimator.getAvgJitter().toPrecision(3),
this.stats.nSegments = this.pipeline.numberOfSatisfiedSegments;
this.stats.completionTime = Date.now() - this.stats.pipelineStartTime;
try {
this.pipeline.cancel();
this.printSummary();
this.onComplete(new Blob(content, false));
}
catch (ex) {
console.log("Error in onComplete: " + NdnCommon.getErrorWithStackTrace(ex));
return;
}
return;
}
this.nInFlight = Math.max(this.nInFlight - 1, 0);
// Send next Interests
this.sendNextInterests();
};
/**
* @param segNo the segment for which a failure happened
* @note if segNo is `-1` it means a general failure happened
* (e.g., negative number of in flight segments)
* @param errCode comes from Pipeline.ErrorCode
* @param reason a description about the failure
*/
PipelineFixed.prototype.handleFailure = function(segNo, errCode, reason)
{
if (this.pipeline.isStopped)
return;
// this is a general failure; not specific to one segment
if (segNo === -1) {
this.pipeline.onFailure(errCode, reason, this.onError);
return;
}
// if the failed segment is definitely part of the content, raise a fatal error
if (segNo === 0 || (this.pipeline.hasFinalBlockId && segNo <= this.pipeline.finalBlockId))
return this.pipeline.onFailure(errCode, reason, this.onError);
if (!this.pipeline.hasFinalBlockId) {
this.nInFlight--;
if (this.nInFlight <= 0) {
this.pipeline.onFailure(Pipeline.ErrorCode.NO_FINALBLOCK,
"Fetching terminated at segment " + segNo +
" but no finalBlockId has been found",
this.onError);
}
else {
this.cancelInFlightSegmentsGreaterThan(segNo);
this.pipeline.hasFailure = true;
this.pipeline.failedSegNo = segNo;
this.pipeline.failureErrorCode = errCode;
this.pipeline.failureReason = reason;
}
}
};
PipelineFixed.prototype.onValidationFailed = function(data, reason)
{
Pipeline.reportError(this.onError, Pipeline.ErrorCode.SEGMENT_VERIFICATION_FAILED,
"Segment verification failed for " + data.getName().toUri() +
" . Reason: " + reason);
};
PipelineFixed.prototype.printSummary = function()
{
if (LOG < 2)
return;
var rttMsg = "";
if (this.rttEstimator.getMinRtt() === Number.MAX_VALUE ||
this.rttEstimator.getMaxRtt() === Number.NEGATIVE_INFINITY) {
rttMsg = "stats unavailable";
}
else {
rttMsg = "min/avg/max = " + this.rttEstimator.getMinRtt().toPrecision(3) + "/"
+ this.rttEstimator.getAvgRtt().toPrecision(3) + "/"
+ this.rttEstimator.getMaxRtt().toPrecision(3) + " ms";
}
console.log("Timeouts: " + this.stats.nTimeouts + " Nacks: " + this.stats.nNacks + "\n" +
"Retransmitted segments: " + this.stats.nRetransmitted + "\n" +
"RTT " + rttMsg + "\n" +
"Average jitter: " + this.rttEstimator.getAvgJitter().toPrecision(3) + " ms\n" +
"Completion time: " + this.stats.completionTime + "ms");
};