Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compute: VM.waitFor() #2047

Merged
merged 11 commits into from
Apr 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 176 additions & 3 deletions packages/compute/src/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,40 @@ var Disk = require('./disk.js');
* @param {string} message - Custom error message.
* @return {Error}
*/
var DetachDiskError = createErrorClass('DetachDiskError', function(message) {
this.message = message;
});
var DetachDiskError = createErrorClass('DetachDiskError');

/**
* Custom error type for when `waitFor()` does not return a status in a timely
* fashion.
*
* @private
*
* @param {string} message - Custom error message.
* @return {Error}
*/
var WaitForTimeoutError = createErrorClass('WaitForTimeoutError');

/**
* The statuses that a VM can be in.
*
* @private
*/
var VALID_STATUSES = [
'PROVISIONING',
'STAGING',
'RUNNING',
'STOPPING',
'SUSPENDING',
'SUSPENDED',
'TERMINATED'
];

/**
* Interval for polling during waitFor.
*
* @private
*/
var WAIT_FOR_POLLING_INTERVAL_MS = 2000;

/*! Developer Documentation
*
Expand All @@ -69,6 +100,9 @@ function VM(zone, name) {
this.name = name.replace(/.*\/([^/]+)$/, '$1'); // Just the instance name.
this.zone = zone;

this.hasActiveWaiters = false;
this.waiters = [];

this.url = format('{base}/{project}/zones/{zone}/instances/{name}', {
base: 'https://www.googleapis.com/compute/v1/projects',
project: zone.compute.projectId,
Expand Down Expand Up @@ -779,6 +813,145 @@ VM.prototype.stop = function(callback) {
}, callback || common.util.noop);
};

/**
* This function will callback when the VM is in the specified state.
*
* Will time out after the specified time (default: 300 seconds).
*
* @param {string} status - The status to wait for. This can be:
* - "PROVISIONING"
* - "STAGING"
* - "RUNNING"
* - "STOPPING"
* - "SUSPENDING"
* - "SUSPENDED"
* - "TERMINATED"
* @param {object=} options - Configuration object.
* @param {number} options.timeout - The number of seconds to wait until timing
* out, between `0` and `600`. Default: `300`
* @param {function} callback - The callback function.
* @param {?error} callback.err - An error returned while waiting for the
* status.
* @param {object} callback.metadata - The instance's metadata.
*
* @example
* vm.waitFor('RUNNING', function(err, metadata) {
* if (!err) {
* // The VM is running.
* }
* });
*
* //-
* // By default, `waitFor` will timeout after 300 seconds while waiting for the
* // desired state to occur. This can be changed to any number between 0 and
* // 600. If the timeout is set to 0, it will poll once for status and then
* // timeout if the desired state is not reached.
* //-
* var options = {
* timeout: 600
* };
*
* vm.waitFor('TERMINATED', options, function(err, metadata) {
* if (!err) {
* // The VM is terminated.
* }
* });
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* vm.waitFor('RUNNING', options).then(function(data) {
* var metadata = data[0];
* });
*/
VM.prototype.waitFor = function(status, options, callback) {

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

if (is.fn(options)) {
callback = options;
options = {};
}

options = options || {};

status = status.toUpperCase();

// The timeout should default to five minutes, be less than or equal to 10
// minutes, and be greater than or equal to 0 seconds.
var timeout = 300;

if (is.number(options.timeout)) {
timeout = Math.min(Math.max(options.timeout, 0), 600);
}

if (VALID_STATUSES.indexOf(status) === -1) {
throw new Error('Status passed to waitFor is invalid.');
}

this.waiters.push({
status: status,
timeout: timeout,
startTime: new Date() / 1000,
callback: callback
});

if (!this.hasActiveWaiters) {
this.hasActiveWaiters = true;
this.startPolling_();
}
};

/**
* Poll `getMetadata` to check the VM's status. This runs a loop to ping
* the API on an interval.
*
* Note: This method is automatically called when a `waitFor()` call
* is made.
*
* @private
*/
VM.prototype.startPolling_ = function() {
var self = this;

if (!this.hasActiveWaiters) {
return;
}

this.getMetadata(function(err, metadata) {
var now = new Date() / 1000;

var waitersToRemove = self.waiters.filter(function(waiter) {
if (err) {
waiter.callback(err);
return true;
}

if (metadata.status === waiter.status) {
waiter.callback(null, metadata);
return true;
}

if (now - waiter.startTime >= waiter.timeout) {
var waitForTimeoutError = new WaitForTimeoutError([
'waitFor timed out waiting for VM ' + self.name,
'to be in status: ' + waiter.status
].join(' '));
waiter.callback(waitForTimeoutError);
return true;
}
});

waitersToRemove.forEach(function(waiter) {
self.waiters.splice(self.waiters.indexOf(waiter), 1);
});

self.hasActiveWaiters = self.waiters.length > 0;

if (self.hasActiveWaiters) {
setTimeout(self.startPolling_.bind(self), WAIT_FOR_POLLING_INTERVAL_MS);
}
});
};


/**
* Make a new request object from the provided arguments and wrap the callback
* to intercept non-successful responses.
Expand Down
12 changes: 10 additions & 2 deletions packages/compute/system-test/compute.js
Original file line number Diff line number Diff line change
Expand Up @@ -1290,8 +1290,16 @@ describe('Compute', function() {
vm.start(compute.execAfterOperation_(done));
});

it('should stop', function(done) {
vm.stop(compute.execAfterOperation_(done));
it('should stop and trigger STOPPING `waitFor` event', function(done) {
async.parallel([
function(callback) {
vm.waitFor('STOPPING', { timeout: 600 }, callback);
},

function(callback) {
vm.stop(compute.execAfterOperation_(callback));
}
], done);
});
});

Expand Down
Loading