Skip to content

Commit 20cc892

Browse files
NickZstephenplusplus
authored andcommitted
compute: VM.waitFor() (#2047)
1 parent 7166f1a commit 20cc892

3 files changed

Lines changed: 456 additions & 5 deletions

File tree

packages/compute/src/vm.js

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,40 @@ var Disk = require('./disk.js');
4141
* @param {string} message - Custom error message.
4242
* @return {Error}
4343
*/
44-
var DetachDiskError = createErrorClass('DetachDiskError', function(message) {
45-
this.message = message;
46-
});
44+
var DetachDiskError = createErrorClass('DetachDiskError');
45+
46+
/**
47+
* Custom error type for when `waitFor()` does not return a status in a timely
48+
* fashion.
49+
*
50+
* @private
51+
*
52+
* @param {string} message - Custom error message.
53+
* @return {Error}
54+
*/
55+
var WaitForTimeoutError = createErrorClass('WaitForTimeoutError');
56+
57+
/**
58+
* The statuses that a VM can be in.
59+
*
60+
* @private
61+
*/
62+
var VALID_STATUSES = [
63+
'PROVISIONING',
64+
'STAGING',
65+
'RUNNING',
66+
'STOPPING',
67+
'SUSPENDING',
68+
'SUSPENDED',
69+
'TERMINATED'
70+
];
71+
72+
/**
73+
* Interval for polling during waitFor.
74+
*
75+
* @private
76+
*/
77+
var WAIT_FOR_POLLING_INTERVAL_MS = 2000;
4778

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

103+
this.hasActiveWaiters = false;
104+
this.waiters = [];
105+
72106
this.url = format('{base}/{project}/zones/{zone}/instances/{name}', {
73107
base: 'https://www.googleapis.com/compute/v1/projects',
74108
project: zone.compute.projectId,
@@ -779,6 +813,145 @@ VM.prototype.stop = function(callback) {
779813
}, callback || common.util.noop);
780814
};
781815

816+
/**
817+
* This function will callback when the VM is in the specified state.
818+
*
819+
* Will time out after the specified time (default: 300 seconds).
820+
*
821+
* @param {string} status - The status to wait for. This can be:
822+
* - "PROVISIONING"
823+
* - "STAGING"
824+
* - "RUNNING"
825+
* - "STOPPING"
826+
* - "SUSPENDING"
827+
* - "SUSPENDED"
828+
* - "TERMINATED"
829+
* @param {object=} options - Configuration object.
830+
* @param {number} options.timeout - The number of seconds to wait until timing
831+
* out, between `0` and `600`. Default: `300`
832+
* @param {function} callback - The callback function.
833+
* @param {?error} callback.err - An error returned while waiting for the
834+
* status.
835+
* @param {object} callback.metadata - The instance's metadata.
836+
*
837+
* @example
838+
* vm.waitFor('RUNNING', function(err, metadata) {
839+
* if (!err) {
840+
* // The VM is running.
841+
* }
842+
* });
843+
*
844+
* //-
845+
* // By default, `waitFor` will timeout after 300 seconds while waiting for the
846+
* // desired state to occur. This can be changed to any number between 0 and
847+
* // 600. If the timeout is set to 0, it will poll once for status and then
848+
* // timeout if the desired state is not reached.
849+
* //-
850+
* var options = {
851+
* timeout: 600
852+
* };
853+
*
854+
* vm.waitFor('TERMINATED', options, function(err, metadata) {
855+
* if (!err) {
856+
* // The VM is terminated.
857+
* }
858+
* });
859+
*
860+
* //-
861+
* // If the callback is omitted, we'll return a Promise.
862+
* //-
863+
* vm.waitFor('RUNNING', options).then(function(data) {
864+
* var metadata = data[0];
865+
* });
866+
*/
867+
VM.prototype.waitFor = function(status, options, callback) {
868+
if (is.fn(options)) {
869+
callback = options;
870+
options = {};
871+
}
872+
873+
options = options || {};
874+
875+
status = status.toUpperCase();
876+
877+
// The timeout should default to five minutes, be less than or equal to 10
878+
// minutes, and be greater than or equal to 0 seconds.
879+
var timeout = 300;
880+
881+
if (is.number(options.timeout)) {
882+
timeout = Math.min(Math.max(options.timeout, 0), 600);
883+
}
884+
885+
if (VALID_STATUSES.indexOf(status) === -1) {
886+
throw new Error('Status passed to waitFor is invalid.');
887+
}
888+
889+
this.waiters.push({
890+
status: status,
891+
timeout: timeout,
892+
startTime: new Date() / 1000,
893+
callback: callback
894+
});
895+
896+
if (!this.hasActiveWaiters) {
897+
this.hasActiveWaiters = true;
898+
this.startPolling_();
899+
}
900+
};
901+
902+
/**
903+
* Poll `getMetadata` to check the VM's status. This runs a loop to ping
904+
* the API on an interval.
905+
*
906+
* Note: This method is automatically called when a `waitFor()` call
907+
* is made.
908+
*
909+
* @private
910+
*/
911+
VM.prototype.startPolling_ = function() {
912+
var self = this;
913+
914+
if (!this.hasActiveWaiters) {
915+
return;
916+
}
917+
918+
this.getMetadata(function(err, metadata) {
919+
var now = new Date() / 1000;
920+
921+
var waitersToRemove = self.waiters.filter(function(waiter) {
922+
if (err) {
923+
waiter.callback(err);
924+
return true;
925+
}
926+
927+
if (metadata.status === waiter.status) {
928+
waiter.callback(null, metadata);
929+
return true;
930+
}
931+
932+
if (now - waiter.startTime >= waiter.timeout) {
933+
var waitForTimeoutError = new WaitForTimeoutError([
934+
'waitFor timed out waiting for VM ' + self.name,
935+
'to be in status: ' + waiter.status
936+
].join(' '));
937+
waiter.callback(waitForTimeoutError);
938+
return true;
939+
}
940+
});
941+
942+
waitersToRemove.forEach(function(waiter) {
943+
self.waiters.splice(self.waiters.indexOf(waiter), 1);
944+
});
945+
946+
self.hasActiveWaiters = self.waiters.length > 0;
947+
948+
if (self.hasActiveWaiters) {
949+
setTimeout(self.startPolling_.bind(self), WAIT_FOR_POLLING_INTERVAL_MS);
950+
}
951+
});
952+
};
953+
954+
782955
/**
783956
* Make a new request object from the provided arguments and wrap the callback
784957
* to intercept non-successful responses.

packages/compute/system-test/compute.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,8 +1290,16 @@ describe('Compute', function() {
12901290
vm.start(compute.execAfterOperation_(done));
12911291
});
12921292

1293-
it('should stop', function(done) {
1294-
vm.stop(compute.execAfterOperation_(done));
1293+
it('should stop and trigger STOPPING `waitFor` event', function(done) {
1294+
async.parallel([
1295+
function(callback) {
1296+
vm.waitFor('STOPPING', { timeout: 600 }, callback);
1297+
},
1298+
1299+
function(callback) {
1300+
vm.stop(compute.execAfterOperation_(callback));
1301+
}
1302+
], done);
12951303
});
12961304
});
12971305

0 commit comments

Comments
 (0)