ndn-js
Version:
A JavaScript client library for Named Data Networking
646 lines (555 loc) • 23.1 kB
JavaScript
/**
* Copyright (C) 2018-2019 Regents of the University of California.
* @author: Chavoosh Ghasemi <chghasemi@cs.arizona.edu>
* @author: From https://github.com/named-data/ndn-tools/tree/master/tools/chunks
*
* 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 Name = require('../name.js').Name; /** @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 Pipeline = require('./pipeline.js').Pipeline; /** @ignore */
var LOG = require('../log.js').Log.LOG;
/**
* Implementation of Cubic pipeline according to:
* [RFC8312](https://tools.ietf.org/html/rfc8312)
* [Linux kernel implementation](https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_cubic.c)
* [ndnchunks tool bundle](https://github.com/named-data/ndn-tools/tree/master/tools/chunks)
*
* This is a public constructor to create a new PipelineCubic.
* @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 PipelineCubic = function PipelineCubic
(baseInterest, face, opts, validatorKeyChain, onComplete, onError, stats)
{
this.pipeline = new Pipeline(baseInterest);
this.face = face;
this.validatorKeyChain = validatorKeyChain;
this.onComplete = onComplete;
this.onError = onError;
// Adaptive options
this.initCwnd = Pipeline.op("initCwnd", 1.0, opts);
this.cwnd = Pipeline.op("cwnd", this.initCwnd, opts);
this.ssthresh = Pipeline.op("ssthresh", Number.MAX_VALUE, opts);
this.rtoCheckInterval = Pipeline.op("rtoCheckInterval", 10, opts);
this.disableCwa = Pipeline.op("disableCwa", false, opts);
this.maxRetriesOnTimeoutOrNack = Pipeline.op("maxRetriesOnTimeoutOrNack", 3, opts);
// Cubic options
this.enableFastConv = Pipeline.op("enableFastConv", false, opts);
this.cubicBeta = Pipeline.op("cubicBeta", 0.7, opts);
this.wmax = Pipeline.op("wmax", 0, opts); // window size before last window decrease
this.lastWmax = Pipeline.op("lastWmax", 0, opts); // last wmax
this.lastDecrease = Date.now(); // time of last window decrease
this.cubic_c = 0.4;
// Run options
this.highData = 0; // the highest segment number of the Data packet the consumer has received so far
this.highInterest = 0; // the highest segment number of the Interests the consumer has sent so far
this.recPoint = 0; // the value of highInterest when a packet loss event occurred,
// it remains fixed until the next packet loss event happens
this.nInFlight = 0; // # of segments in flight
this.nLossDecr = 0; // # of window decreases caused by packet loss
this.nTimeouts = 0; // # of timed out segments
this.nNacks = 0; // # of nack segments
this.nSkippedRetx = 0; // # of segments queued for retransmission but received before
// retransmission occurred
this.nRetransmitted = 0; // # of retransmitted segments
this.nSent = 0; // # of interest packets sent out (including retransmissions)
this.segmentInfo = []; // track information that is necessary for segment transmission
this.retxQueue = []; // a queue to store segments that need to retransmitted
this.retxCount = []; // track number of retx of each segment
this.rttEstimator = new RttEstimator(opts);
// Stats collector
this.stats = stats != null ? stats : {};
}
exports.PipelineCubic = PipelineCubic;
PipelineCubic.SegmentState = {
FirstTimeSent: 1, // segment has been sent for the first time
InRetxQueue: 2, // segment is in retransmission queue
Retransmitted: 3 // segment has been retransmitted
};
PipelineCubic.prototype.increaseWindow = function()
{
// Slow start phase
if (this.cwnd < this.ssthresh) {
this.cwnd += 1;
}
// Congestion avoidance phase
else {
// If wmax is still 0, set it to the current cwnd. Usually unnecessary,
// if m_ssthresh is large enough.
if (this.wmax < this.initCwnd) {
this.wmax = this.cwnd;
}
// 1. Time since last congestion event in seconds
var t = (Date.now() - this.lastDecrease) / 1000;
// 2. Time it takes to increase the window to wmax
var k = Math.cbrt(this.wmax * (1 - this.cubicBeta) / this.cubic_c);
// 3. Target: W_cubic(t) = C*(t-K)^3 + wmax (Eq. 1)
var wCubic = this.cubic_c * Math.pow(t - k, 3) + this.wmax;
// 4. Estimate of Reno Increase (Currently Disabled)
var wEst = 0.0;
// Actual adaptation
var cubicIncrement = Math.max(wCubic, wEst) - this.cwnd;
// Cubic increment must be positive
// Note: This change is not part of the RFC, but it is added performance improvement
cubicIncrement = Math.max(0, cubicIncrement);
this.cwnd += cubicIncrement / this.cwnd;
}
};
PipelineCubic.prototype.decreaseWindow = function()
{
// A flow remembers the last value of wmax,
// before it updates wmax for the current congestion event.
// Current wmax < last_wmax
if (this.enableFastConv && this.cwnd < this.lastWmax) {
this.lastWmax = this.cwnd;
this.wmax = this.cwnd * (1.0 + this.cubicBeta) / 2.0;
}
else {
// Save old cwnd as wmax
this.lastWmax = this.cwnd;
this.wmax = this.cwnd;
}
this.ssthresh = Math.max(this.initCwnd, this.cwnd * this.cubicBeta);
this.cwnd = this.ssthresh;
this.lastDecrease = Date.now();
};
PipelineCubic.prototype.run = function()
{
this.stats.pipelineStartTime = Date.now();
// Schedule the next check after the predefined interval
setTimeout(this.checkRto.bind(this), this.rtoCheckInterval);
this.sendInterest(this.pipeline.getNextSegmentNo(), false);
};
PipelineCubic.prototype.cancel = function()
{
this.pipeline.cancel();
this.segmentInfo.length = 0;
};
/**
* @param segNo to-be-sent segment number
* @param isRetransmission true if this is a retransmission
*/
PipelineCubic.prototype.sendInterest = function(segNo, isRetransmission)
{
if (this.pipeline.isStopped)
return;
if (this.pipeline.hasFinalBlockId && segNo > this.pipeline.finalBlockId)
return;
if (!isRetransmission && this.pipeline.hasFailure)
return;
if (isRetransmission) {
// keep track of retx count for this segment
if (this.retxCount[segNo] === undefined) {
this.retxCount[segNo] = 1;
}
else { // not the first retransmission
this.retxCount[segNo]++;
if (this.retxCount[segNo] > this.maxRetriesOnTimeoutOrNack) {
return this.handleFailure(segNo, Pipeline.ErrorCode.MAX_NACK_TIMEOUT_RETRIES,
"Reached the maximum number of retries (" +
this.maxRetriesOnTimeoutOrNack + ") while retrieving segment #" + segNo);
}
}
if (LOG > 1)
console.log("Retransmitting segment #" + segNo + " (" + this.retxCount[segNo] + ")");
}
if (LOG > 1 && !isRetransmission)
console.log("Requesting segment #" + segNo);
var interest = this.pipeline.makeInterest(segNo);
if (Number.isNaN(this.pipeline.versionNo) ) {
interest.setMustBeFresh(true);
}
else {
interest.setMustBeFresh(false);
}
var segInfo = {};
segInfo.pendingInterestId = this.face.expressInterest
(interest,
this.handleData.bind(this),
this.handleLifetimeExpiration.bind(this),
this.handleNack.bind(this));
// initTimeSent allows calculating full delay
if (isRetransmission && segInfo.initTimeSent === undefined)
segInfo.initTimeSent = segInfo.timeSent;
segInfo.timeSent = Date.now();
segInfo.rto = this.rttEstimator.getEstimatedRto();
this.nInFlight++;
this.nSent++;
if (isRetransmission) {
segInfo.state = PipelineCubic.SegmentState.Retransmitted;
this.nRetransmitted++;
}
else {
this.highInterest = segNo;
segInfo.state = PipelineCubic.SegmentState.FirstTimeSent;
}
this.segmentInfo[segNo] = segInfo;
};
PipelineCubic.prototype.schedulePackets = function()
{
if (this.nInFlight < 0) {
this.handleFailure(-1, Pipeline.ErrorCode.MISC, "Number of in flight Interests is negative.");
return;
}
var availableWindowSize = this.cwnd - this.nInFlight;
while (availableWindowSize > 0) {
if (this.retxQueue.length != 0) { // do retransmission first
var retxSegNo = this.retxQueue.shift();
if (this.segmentInfo[retxSegNo] === undefined) {
this.nSkippedRetx++;
continue;
}
// the segment is still in the map, that means it needs to be retransmitted
this.sendInterest(retxSegNo, true);
}
else { // send next segment
this.sendInterest(this.pipeline.getNextSegmentNo(), false);
}
availableWindowSize--;
}
};
PipelineCubic.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);
}
};
PipelineCubic.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 (!this.pipeline.hasFinalBlockId && data.getMetaInfo().getFinalBlockId().getValue().size() > 0) {
try {
this.pipeline.finalBlockId = data.getMetaInfo().getFinalBlockId().toSegment();
}
catch (ex) {
this.handleFailure(recSegmentNo, Pipeline.ErrorCode.DATA_HAS_NO_SEGMENT,
"Error while decoding FinalBlockId field " +
data.getName().get(-1).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,
this.cancel.bind(this));
}
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 = 0;
if (recSeg.initTimeSent !== undefined)
fullDelay = Date.now() - recSeg.initTimeSent;
if (LOG > 1) {
console.log ("Received segment #" + recSegmentNo
+ ", rtt=" + rtt + "ms"
+ ", rto=" + recSeg.rto + "ms");
}
if (this.highData < recSegmentNo) {
this.highData = recSegmentNo;
}
// For segments in retx queue, we must not decrement nInFlight
// because it was already decremented when the segment timed out
if (recSeg.state !== PipelineCubic.SegmentState.InRetxQueue) {
this.nInFlight--;
}
// Do not sample RTT for retransmitted segments
if ((recSeg.state === PipelineCubic.SegmentState.FirstTimeSent ||
recSeg.state === PipelineCubic.SegmentState.InRetxQueue) &&
this.retxCount[recSegmentNo] === undefined) {
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));
}
// Clear the entry associated with the received segment
this.segmentInfo[recSegmentNo] = undefined; // do not splice
this.pipeline.numberOfSatisfiedSegments++;
// Check whether we are finished
if (this.pipeline.hasFinalBlockId &&
this.pipeline.numberOfSatisfiedSegments > this.pipeline.finalBlockId) {
// Concatenate to get the content
var content = Buffer.concat(this.pipeline.contentParts);
this.cancelInFlightSegmentsGreaterThan(this.pipeline.finalBlockId);
// fill out the stats
this.stats.nTimeouts = this.nTimeouts;
this.stats.nNacks = this.nNacks;
this.stats.nRetransmitted = this.nRetransmitted;
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.cancel();
this.printSummary();
this.onComplete(new Blob(content, false));
}
catch (ex) {
this.handleFailure(-1, Pipeline.ErrorCode.MISC,
"Error in onComplete: " + NdnCommon.getErrorWithStackTrace(ex));
return;
}
return;
}
this.increaseWindow();
// Schedule the next segments to be fetched
this.schedulePackets();
};
PipelineCubic.prototype.checkRto = function()
{
if (this.pipeline.isStopped)
return;
var hasTimeout = false;
for (var i=0; i < this.segmentInfo.length; ++i) {
if (this.segmentInfo[i] === undefined)
continue;
var segInfo = this.segmentInfo[i];
if (segInfo.state !== PipelineCubic.SegmentState.InRetxQueue) { // skip segments in the retx queue
var timeElapsed = Date.now() - segInfo.timeSent;
if (timeElapsed > segInfo.rto) { // timer expired?
this.nTimeouts++;
hasTimeout = true;
this.onWarning(Pipeline.ErrorCode.INTEREST_TIMEOUT, "handle timeout for segment " + i);
this.enqueueForRetransmission(i);
}
}
}
if (hasTimeout) {
this.recordTimeout();
this.schedulePackets();
}
// schedule the next check after the predefined interval
setTimeout(this.checkRto.bind(this), this.rtoCheckInterval);
};
PipelineCubic.prototype.enqueueForRetransmission = function(segNo)
{
if (this.nInFlight <= 0) {
this.handleFailure(-1, Pipeline.ErrorCode.MISC, "Number of in flight Interests <= 0.");
return;
}
this.nInFlight--;
this.retxQueue.push(segNo);
this.segmentInfo[segNo].state = PipelineCubic.SegmentState.InRetxQueue;
};
PipelineCubic.prototype.recordTimeout = function()
{
if (this.disableCwa || this.highData > this.recPoint) {
// react to only one timeout per RTT (conservative window adaptation)
this.recPoint = this.highInterest;
this.decreaseWindow();
this.rttEstimator.backoffRto();
this.nLossDecr++;
if (LOG > 1) {
console.log("Packet loss event, new cwnd = " + this.cwnd
+ ", ssthresh = " + this.ssthresh);
}
}
};
PipelineCubic.prototype.cancelInFlightSegmentsGreaterThan = function(segNo)
{
for (var i = segNo + 1; i < this.segmentInfo.length; ++i) {
// cancel fetching all segments that follow
if (this.segmentInfo[i] !== undefined)
this.face.removePendingInterest(this.segmentInfo[i].pendingInterestId);
this.segmentInfo[i] = undefined; // do no splice
this.nInFlight--;
}
};
/**
* @param {int} 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 {Pipeline.ErrorCode} errCode One of the predefined error codes.
* @param {string} reason A short description about the error.
*/
PipelineCubic.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, this.cancel.bind(this));
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, this.cancel.bind(this));
if (!this.pipeline.hasFinalBlockId) {
this.segmentInfo[segNo] = undefined; // do not splice
this.nInFlight--;
var queueIsEmpty = true;
for (var i = 0; i < this.segmentInfo.length; ++i) {
if (this.segmentInfo[i] !== undefined) {
queueIsEmpty = false;
break;
}
}
if (queueIsEmpty) {
this.pipeline.onFailure(Pipeline.ErrorCode.NO_FINALBLOCK,
"Fetching terminated at segment " + segNo +
" but no finalBlockId has been found",
this.onError,
this.cancel.bind(this));
}
else {
this.cancelInFlightSegmentsGreaterThan(segNo);
this.pipeline.hasFailure = true;
this.pipeline.failedSegNo = segNo;
this.pipeline.failureErrorCode = errCode;
this.pipeline.failureReason = reason;
}
}
};
PipelineCubic.prototype.handleLifetimeExpiration = function(interest)
{
if (this.pipeline.isStopped)
return;
this.nTimeouts++;
var recSegmentNo = 0; // the very first Interest does not have segment number
// Treated the same as timeout for now
if (interest.getName().components.length > 0 && interest.getName().get(-1).isSegment())
recSegmentNo = interest.getName().get(-1).toSegment();
this.enqueueForRetransmission(recSegmentNo);
this.onWarning(Pipeline.ErrorCode.INTEREST_LIFETIME_EXPIRATION,
"handle interest lifetime expiration for segment " + recSegmentNo);
this.recordTimeout();
this.schedulePackets();
};
PipelineCubic.prototype.handleNack = function(interest)
{
if (this.pipeline.isStopped)
return;
this.nNacks++;
var recSegmentNo = 0; // the very first Interest does not have segment number
// Treated the same as timeout for now
if (interest.getName().components.length > 0 && interest.getName().get(-1).isSegment())
recSegmentNo = interest.getName().get(-1).toSegment();
this.enqueueForRetransmission(recSegmentNo);
this.onWarning(Pipeline.ErrorCode.NACK_RECEIVED, "handle nack for segment " + recSegmentNo);
this.recordTimeout();
this.schedulePackets();
};
PipelineCubic.prototype.onValidationFailed = function(data, reason)
{
Pipeline.reportError(this.onError, Pipeline.ErrorCode.SEGMENT_VERIFICATION_FAILED,
"Segment verification failed for " + data.getName().toUri() +
" . Reason: " + reason);
};
PipelineCubic.prototype.onWarning = function(errCode, reason)
{
if (LOG > 2) {
Pipeline.reportWarning(errCode, reason);
}
};
PipelineCubic.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.nTimeouts + " (caused " + this.nLossDecr + " window decreases)\n" +
"Nacks: " + this.nNacks + "\n" +
"Retransmitted segments: " + this.nRetransmitted +
" (" + (this.nSent == 0 ? 0 : (this.nRetransmitted / this.nSent * 100)) + "%)" +
", skipped: " + this.nSkippedRetx + "\n" +
"RTT " + rttMsg + "\n" +
"Average jitter: " + this.rttEstimator.getAvgJitter().toPrecision(3) + " ms\n" +
"Completion time: " + this.stats.completionTime + "ms");
};