UNPKG

azure-storage

Version:

Microsoft Azure Storage Client Library for Node.js

426 lines (378 loc) 14.5 kB
// // Copyright (c) Microsoft and contributors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // // See the License for the specific language governing permissions and // limitations under the License. // var util = require('util'); var http = require('http'); var https = require('https'); var EventEmitter = require('events').EventEmitter; var os = require('os'); var azureutil = require('../util/util'); var Logger = require('../diagnostics/logger'); var Constants = require('../util/constants'); var errors = require('../errors/errors'); var ArgumentError = errors.ArgumentError; var DEFAULT_OPERATION_MEMORY_USAGE = Constants.BlobConstants.DEFAULT_WRITE_BLOCK_SIZE_IN_BYTES; var DEFAULT_CRITICAL_MEMORY_LIMITATION_32_IN_BYTES = Constants.BlobConstants.DEFAULT_CRITICAL_MEMORY_LIMITATION_32_IN_BYTES; var DEFAULT_CRITICAL_MEMORY_LIMITATION_BROWSER_IN_BYTES = Constants.BlobConstants.DEFAULT_CRITICAL_MEMORY_LIMITATION_BROWSER_IN_BYTES; var DEFAULT_MINIMUM_MEMORY_USAGE_BROWSER_IN_BYTES = Constants.BlobConstants.DEFAULT_MINIMUM_MEMORY_USAGE_BROWSER_IN_BYTES; var DEFAULT_GLOBAL_CONCURRENCY = 5; //Default http connection limitation for nodejs var SystemTotalMemory = os.totalmem(); var CriticalFreeMemory = 0.1 * SystemTotalMemory; var nodeVersion = azureutil.getNodeVersion(); /** * Concurrently execute batch operations and call operation callback randomly or in sequence. * Random mode is for uploading. * 1. Fire user callback when the operation is done. * Sequence mode is for downloading. * 1. Fire user callback when the operation is done and all previous operations and callback has finished. * 2. BatchOperation guarantees the user callback is fired one by one. * 3. The next user callback can't be fired until the current one is completed. */ function BatchOperation(name, options) { if (!options) { options = {}; } this.name = name; this.logger = options.logger || new Logger(Logger.LogLevels.INFO); this.operationMemoryUsage = options.operationMemoryUsage || DEFAULT_OPERATION_MEMORY_USAGE; this.callbackInOrder = options.callbackInOrder === true; this.callInOrder = options.callInOrder === true; this._currentOperationId = this.callbackInOrder ? 1 : -1; this.concurrency = DEFAULT_GLOBAL_CONCURRENCY; this.enableReuseSocket = (nodeVersion.major > 0 || nodeVersion.minor >= 10) && options.enableReuseSocket; this._emitter = new EventEmitter(); this._enableComplete = false; this._ended = false; this._error = null; this._paused = false; //Total operations count(queued and active and connected) this._totalOperation = 0; //Action operations count(The operations which are connecting to remote or executing callback or queued for executing) this._activeOperation = 0; //Queued operations count(The operations which are connecting to remote or queued for executing) this._queuedOperation = 0; //finished operation should be removed from this array this._operations = []; } /** * Operation state */ var OperationState = { INITED : 'inited', QUEUED : 'queued', RUNNING : 'running', COMPLETE : 'complete', CALLBACK : 'callback', ERROR : 'error' }; BatchOperation.OperationState = OperationState; /** * Set batch operation concurrency */ BatchOperation.prototype.setConcurrency = function(concurrency) { if (concurrency) { this.concurrency = concurrency; http.Agent.maxSockets = this.concurrency; https.Agent.maxSockets = this.concurrency; } }; /** * Is the workload heavy and It can be used to determine whether we could queue operations */ BatchOperation.prototype.IsWorkloadHeavy = function() { //Only support one batch operation for now. //In order to work with the multiple batch operation, we can use global operation track objects //BatchOperation acquire a bunch of operation ids from global and allocated ids to RestOperation //RestOperation start to run in order of id var sharedRequest = 1; if(this.enableReuseSocket && !this.callInOrder) { sharedRequest = 2; } return this._activeOperation >= sharedRequest * this.concurrency || this._isLowMemory(); }; /** * Get the approximate memory usage for batch operation. */ BatchOperation.prototype._getApproximateMemoryUsage = function() { var currentUsage = azureutil.isBrowser() ? DEFAULT_MINIMUM_MEMORY_USAGE_BROWSER_IN_BYTES : process.memoryUsage().rss; // Currently, we cannot get memory usage in browsers var futureUsage = this._queuedOperation * this.operationMemoryUsage; return currentUsage + futureUsage; }; /** * Return whether in a low memory situation. */ BatchOperation.prototype._isLowMemory = function() { var approximateMemoryUsage = this._getApproximateMemoryUsage(); return os.freemem() < CriticalFreeMemory || (this._activeOperation >= this.concurrency && approximateMemoryUsage > 0.5 * SystemTotalMemory) || (azureutil.is32() && approximateMemoryUsage > DEFAULT_CRITICAL_MEMORY_LIMITATION_32_IN_BYTES) || (azureutil.isBrowser() && approximateMemoryUsage > DEFAULT_CRITICAL_MEMORY_LIMITATION_BROWSER_IN_BYTES); }; /** * Add a operation into batch operation */ BatchOperation.prototype.addOperation = function(operation) { this._operations.push(operation); operation.status = OperationState.QUEUED; operation.operationId = ++this._totalOperation; this._queuedOperation++; this.logger.debug(util.format('Add operation %d into batch operation %s. Active: %s; Queued: %s', operation.operationId, this.name, this._activeOperation, this._queuedOperation)); //Immediately start the idle operation if workload isn't heavy this._runOperation(operation); return this.IsWorkloadHeavy(); }; /** * Enable batch operation complete when there is no operation to run. */ BatchOperation.prototype.enableComplete = function() { this._enableComplete = true; this.logger.debug(util.format('Enable batch operation %s complete', this.name)); this._tryEmitEndEvent(); }; /** * Stop firing user call back */ BatchOperation.prototype.pause = function () { this._paused = true; }; /** * Start firing user call back */ BatchOperation.prototype.resume = function () { if (this._paused) { this._paused = false; this._fireOperationUserCallback(); } }; /** * Add event listener */ BatchOperation.prototype.on = function(event, listener) { // only emit end if the batch has completed all operations if(this._ended && event === 'end') { listener(); } else { this._emitter.on(event, listener); } }; /** * Run operation */ BatchOperation.prototype._runOperation = function (operation) { this.logger.debug(util.format('Operation %d start to run', operation.operationId)); var cb = this.getBatchOperationCallback(operation); if(this._error) { cb(this._error);//Directly call the callback with previous error. } else { operation.run(cb); } this._activeOperation++; }; /** * Return an general operation call back. * This callback is used to update the internal status and fire user's callback when operation is complete. */ BatchOperation.prototype.getBatchOperationCallback = function (operation) { var self = this; return function (error) { self._queuedOperation--; if (error) { operation.status = OperationState.ERROR; self.logger.debug(util.format('Operation %d failed. Error %s', operation.operationId, error)); self._error = error; } else { operation.status = OperationState.CALLBACK; self.logger.debug(util.format('Operation %d succeed', operation.operationId)); } operation._callbackArguments = arguments; if (self._paused) { operation.status = OperationState.CALLBACK; self.logger.debug(util.format('Batch operation paused and Operation %d wait for firing callback', operation.operationId)); } else if (self.callbackInOrder) { operation.status = OperationState.CALLBACK; if (self._currentOperationId === operation.operationId) { self._fireOperationUserCallback(operation); } else if (self._currentOperationId > operation.operationId) { throw new Error('Debug error: current callback operation id cannot be larger than operation id'); } else { self.logger.debug(util.format('Operation %d is waiting for firing callback %s', operation.operationId, self._currentOperationId)); } } else { self._fireOperationUserCallback(operation); } self._tryEmitDrainEvent(); operation = null; self = null; }; }; /** * Fire user's call back */ BatchOperation.prototype._fireOperationUserCallback = function (operation) { var index = this._getCallbackOperationIndex(); if (!operation && index != -1) { operation = this._operations[index]; } if (operation && !this._paused) { // fire the callback, if exists if (operation._userCallback) { this.logger.debug(util.format('Fire user call back for operation %d', operation.operationId)); // make sure UserCallback is a sync operation in sequence mode. // both async and sync operations are available for random mode. operation._fireUserCallback(); } // remove the operation from the array and decrement the counter this._operations.splice(index, 1); this._activeOperation--; operation.status = OperationState.COMPLETE; index = operation = null; if (this.callbackInOrder) { this._currentOperationId++; } this._fireOperationUserCallback(); } else if (this._paused) { this._tryEmitDrainEvent(); } else { // check if batch has ended and if so emit end event this._tryEmitEndEvent(); } }; /** * Try to emit the BatchOperation end event * End event means all the operation and callback already finished. */ BatchOperation.prototype._tryEmitEndEvent = function () { if(this._enableComplete && this._activeOperation === 0 && this._operations.length === 0) { this._ended = true; this.logger.debug(util.format('Batch operation %s emits the end event', this.name)); this._emitter.emit('end', this._error, null); return true; } // Workaround to recover from the 'hang' edge case. _tryEmitEndEvent function is not supposed to be called if the bacth is not really completed. this._tryEmitDrainEvent(); return false; }; /** * Try to emit the drain event */ BatchOperation.prototype._tryEmitDrainEvent = function () { if (!this._emitter) return false; if(!this.IsWorkloadHeavy() || this._activeOperation < this.concurrency) { this._emitter.emit('drain'); return true; } return false; }; /** * Get the current active operation index. * Only the active operation could call user's callback in sequence model. * The other finished but not active operations should wait for wake up. */ BatchOperation.prototype._getCallbackOperationIndex = function () { var operation = null; for (var i = 0; i < this._operations.length; i++) { operation = this._operations[i]; if (this.callbackInOrder) { //Sequence mode if (operation.operationId == this._currentOperationId) { if (operation.status === OperationState.CALLBACK) { return i; } else { return -1; } } } else { //Random mode if (operation.status === OperationState.CALLBACK) { return i; } } } return -1; }; /** * Do nothing and directly call the callback. * In random mode, the user callback will be called immediately * In sequence mode, the user callback will be called after the previous callback has been called */ BatchOperation.noOperation = function (cb) { cb(); }; /** * Rest operation in sdk */ function RestOperation(serviceClient, operation) { this.status = OperationState.Inited; this.operationId = -1; this._callbackArguments = null; // setup callback and args this._userCallback = arguments[arguments.length - 1]; var sliceEnd = arguments.length; if(azureutil.objectIsFunction(this._userCallback)) { sliceEnd--; } else { this._userCallback = null; } var operationArguments = Array.prototype.slice.call(arguments).slice(2, sliceEnd); this.run = function(cb) { var func = serviceClient[operation]; if(!func) { throw new ArgumentError('operation', util.format('Unknown operation %s in serviceclient', operation)); } else { if(!cb) cb = this._userCallback; operationArguments.push(cb); this.status = OperationState.RUNNING; func.apply(serviceClient, operationArguments); operationArguments = operation = null; } }; this._fireUserCallback = function () { if(this._userCallback) { this._userCallback.apply(null, this._callbackArguments); } }; } BatchOperation.RestOperation = RestOperation; /** * Common operation wrapper */ function CommonOperation(operationFunc, callback) { this.status = OperationState.Inited; this.operationId = -1; this._callbackArguments = null; var sliceStart = 2; if (azureutil.objectIsFunction(callback)) { this._userCallback = callback; } else { this._userCallback = null; sliceStart = 1; } var operationArguments = Array.prototype.slice.call(arguments).slice(sliceStart); this.run = function (cb) { if (!cb) cb = this._userCallback; operationArguments.push(cb); this.status = OperationState.RUNNING; operationFunc.apply(null, operationArguments); operationArguments = operationFunc = null; }; this._fireUserCallback = function () { if (this._userCallback) { this._userCallback.apply(null, this._callbackArguments); } this._userCallback = this._callbackArguments = null; }; } BatchOperation.CommonOperation = CommonOperation; module.exports = BatchOperation;