Skip to content

Commit 6f2d145

Browse files
compute: turn operations into event emitters
1 parent 7d0fe36 commit 6f2d145

File tree

4 files changed

+319
-176
lines changed

4 files changed

+319
-176
lines changed

lib/compute/operation.js

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@
2020

2121
'use strict';
2222

23-
var extend = require('extend');
24-
var is = require('is');
25-
var nodeutil = require('util');
23+
var events = require('events');
24+
var modelo = require('modelo');
2625

2726
/**
2827
* @type {module:common/serviceObject}
@@ -79,6 +78,30 @@ var util = require('../common/util.js');
7978
* //-
8079
* var zone = gce.zone('us-central1-a');
8180
* var operation = zone.operation('operation-id');
81+
*
82+
* //-
83+
* // All operations are event emitters. The status of each operation is polled
84+
* // continuously, starting only after you register a "complete" listener.
85+
* //-
86+
* operation.on('complete', function(metadata) {
87+
* // The operation is complete.
88+
* });
89+
*
90+
* //-
91+
* // Be sure to register an error handler as well to catch any issues which
92+
* // impeded the operation.
93+
* //-
94+
* operation.on('error', function(err) {
95+
* // An error occurred during the operation.
96+
* });
97+
*
98+
* //-
99+
* // To force the Operation object to stop polling for updates, simply remove
100+
* // any "complete" listeners you've registered.
101+
* //
102+
* // The easiest way to do this is with `removeAllListeners()`.
103+
* //-
104+
* operation.removeAllListeners();
82105
*/
83106
function Operation(scope, name) {
84107
var isCompute = scope.constructor.name === 'Compute';
@@ -132,10 +155,16 @@ function Operation(scope, name) {
132155
methods: methods
133156
});
134157

158+
events.EventEmitter.call(this);
159+
160+
this.completeListeners = 0;
161+
this.hasActiveListeners = false;
135162
this.name = name;
163+
164+
this.listenForEvents_();
136165
}
137166

138-
nodeutil.inherits(Operation, ServiceObject);
167+
modelo.inherits(Operation, ServiceObject, events.EventEmitter);
139168

140169
/**
141170
* Get the operation's metadata. For a detailed description of metadata see
@@ -167,9 +196,9 @@ Operation.prototype.getMetadata = function(callback) {
167196
// this callback. We have to make sure this isn't a false error by seeing if
168197
// the response body contains a property that wouldn't exist on a failed API
169198
// request (`name`).
170-
var isActualError = err && (!apiResponse || apiResponse.name !== self.name);
199+
var requestFailed = err && (!apiResponse || apiResponse.name !== self.name);
171200

172-
if (isActualError) {
201+
if (requestFailed) {
173202
callback(err, null, apiResponse);
174203
return;
175204
}
@@ -181,80 +210,70 @@ Operation.prototype.getMetadata = function(callback) {
181210
};
182211

183212
/**
184-
* Register a callback for when the operation is complete.
213+
* Begin listening for events on the operation. This method keeps track of how
214+
* many "complete" listeners are registered and removed, making sure polling is
215+
* handled automatically.
185216
*
186-
* If the operation doesn't complete after the maximum number of attempts have
187-
* been made (see `options.maxAttempts` and `options.interval`), an error will
188-
* be provided to your callback with code: `OPERATION_INCOMPLETE`.
217+
* As long as there is one active "complete" listener, the connection is open.
218+
* When there are no more listeners, the polling stops.
189219
*
190-
* @param {object=} options - Configuration object.
191-
* @param {number} options.maxAttempts - Maximum number of attempts to make an
192-
* API request to check if the operation is complete. (Default: `10`)
193-
* @param {number} options.interval - Amount of time in milliseconds between
194-
* each request. (Default: `3000`)
195-
* @param {function} callback - The callback function.
196-
* @param {?error} callback.err - An error returned while making this request.
197-
* @param {object} callback.metadata - The operation's metadata.
198-
*
199-
* @example
200-
* operation.onComplete(function(err, metadata) {
201-
* if (err.code === 'OPERATION_INCOMPLETE') {
202-
* // The operation is not complete yet. You may want to register another
203-
* // `onComplete` listener or queue for later.
204-
* }
205-
*
206-
* if (!err) {
207-
* // Operation complete!
208-
* }
209-
* });
220+
* @private
210221
*/
211-
Operation.prototype.onComplete = function(options, callback) {
222+
Operation.prototype.listenForEvents_ = function() {
212223
var self = this;
213224

214-
if (is.fn(options)) {
215-
callback = options;
216-
options = {};
217-
}
218-
219-
options = extend({
220-
maxAttempts: 10,
221-
interval: 3000
222-
}, options);
223-
224-
var didNotCompleteError = new Error('Operation did not complete.');
225-
didNotCompleteError.code = 'OPERATION_INCOMPLETE';
226-
227-
var numAttempts = 0;
225+
this.on('newListener', function(event) {
226+
if (event === 'complete') {
227+
self.completeListeners++;
228228

229-
function checkMetadata() {
230-
numAttempts++;
229+
if (!self.hasActiveListeners) {
230+
self.hasActiveListeners = true;
231+
self.startPolling_();
232+
}
233+
}
234+
});
231235

232-
if (numAttempts > options.maxAttempts) {
233-
callback(didNotCompleteError, self.metadata);
234-
return;
236+
this.on('removeListener', function(event) {
237+
if (event === 'complete' && --self.completeListeners === 0) {
238+
self.hasActiveListeners = false;
235239
}
240+
});
241+
};
236242

237-
setTimeout(function() {
238-
self.getMetadata(onMetadata);
239-
}, options.interval);
243+
/**
244+
* Poll `getMetadata` to check the operation's status. This runs a loop to ping
245+
* the API on an interval.
246+
*
247+
* Note: This method is automatically called once a "complete" event handler is
248+
* registered on the operation.
249+
*
250+
* @private
251+
*/
252+
Operation.prototype.startPolling_ = function() {
253+
var self = this;
254+
255+
if (!this.hasActiveListeners) {
256+
return;
240257
}
241258

242-
function onMetadata(err, metadata) {
259+
this.getMetadata(function(err, metadata, apiResponse) {
260+
// Parsing the response body will automatically create an ApiError object if
261+
// the operation failed.
262+
var parsedHttpRespBody = util.parseHttpRespBody(apiResponse);
263+
err = err || parsedHttpRespBody.err;
264+
243265
if (err) {
244-
callback(err, metadata);
266+
self.emit('error', err);
245267
return;
246268
}
247269

248270
if (metadata.status !== 'DONE') {
249-
checkMetadata();
271+
setTimeout(self.startPolling_.bind(self), 500);
250272
return;
251273
}
252274

253-
// The operation is complete.
254-
callback(null, metadata);
255-
}
256-
257-
checkMetadata();
275+
self.emit('complete', metadata);
276+
});
258277
};
259278

260279
module.exports = Operation;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
"is": "^3.0.1",
9797
"methmeth": "^1.0.0",
9898
"mime-types": "^2.0.8",
99+
"modelo": "^4.2.0",
99100
"once": "^1.3.1",
100101
"prop-assign": "^1.0.0",
101102
"propprop": "^0.3.0",

system-test/compute.js

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ describe('Compute', function() {
6060
before(function(done) {
6161
address.create(function(err, disk, operation) {
6262
assert.ifError(err);
63-
operation.onComplete(done);
63+
64+
operation
65+
.on('error', done)
66+
.on('complete', function() {
67+
done();
68+
});
6469
});
6570
});
6671

@@ -110,7 +115,12 @@ describe('Compute', function() {
110115

111116
disk.create(config, function(err, disk, operation) {
112117
assert.ifError(err);
113-
operation.onComplete(done);
118+
119+
operation
120+
.on('error', done)
121+
.on('complete', function() {
122+
done();
123+
});
114124
});
115125
});
116126

@@ -154,7 +164,12 @@ describe('Compute', function() {
154164

155165
disk.snapshot(generateName()).create(function(err, snapshot, operation) {
156166
assert.ifError(err);
157-
operation.onComplete(getOperationOptions(MAX_TIME_ALLOWED), done);
167+
168+
operation
169+
.on('error', done)
170+
.on('complete', function() {
171+
done();
172+
});
158173
});
159174
});
160175
});
@@ -192,7 +207,12 @@ describe('Compute', function() {
192207

193208
firewall.create(CONFIG, function(err, firewall, operation) {
194209
assert.ifError(err);
195-
operation.onComplete(getOperationOptions(MAX_TIME_ALLOWED), done);
210+
211+
operation
212+
.on('error', done)
213+
.on('complete', function() {
214+
done();
215+
});
196216
});
197217
});
198218

@@ -239,7 +259,12 @@ describe('Compute', function() {
239259
before(function(done) {
240260
network.create(CONFIG, function(err, network, operation) {
241261
assert.ifError(err);
242-
operation.onComplete(done);
262+
263+
operation
264+
.on('error', done)
265+
.on('complete', function() {
266+
done();
267+
});
243268
});
244269
});
245270

@@ -402,7 +427,12 @@ describe('Compute', function() {
402427

403428
vm.create(config, function(err, vm, operation) {
404429
assert.ifError(err);
405-
operation.onComplete(done);
430+
431+
operation
432+
.on('error', done)
433+
.on('complete', function() {
434+
done();
435+
});
406436
});
407437
});
408438

@@ -420,7 +450,11 @@ describe('Compute', function() {
420450
return;
421451
}
422452

423-
operation.onComplete(getOperationOptions(MAX_TIME_ALLOWED), done);
453+
operation
454+
.on('error', done)
455+
.on('complete', function() {
456+
done();
457+
});
424458
});
425459
});
426460

@@ -460,6 +494,7 @@ describe('Compute', function() {
460494

461495
it('should attach and detach a disk', function(done) {
462496
var name = generateName();
497+
var disk = zone.disk(name);
463498

464499
// This test waits on a lot of operations.
465500
this.timeout(90000);
@@ -468,22 +503,29 @@ describe('Compute', function() {
468503
createDisk,
469504
attachDisk,
470505
detachDisk
471-
], done);
506+
], function(err) {
507+
if (err) {
508+
done(err);
509+
return;
510+
}
511+
512+
disk.delete(execAfterOperationComplete(done));
513+
});
472514

473515
function createDisk(callback) {
474516
var config = {
475517
os: 'ubuntu'
476518
};
477519

478-
zone.createDisk(name, config, execAfterOperationComplete(callback));
520+
disk.create(config, execAfterOperationComplete(callback));
479521
}
480522

481523
function attachDisk(callback) {
482-
vm.attachDisk(zone.disk(name), execAfterOperationComplete(callback));
524+
vm.attachDisk(disk, execAfterOperationComplete(callback));
483525
}
484526

485527
function detachDisk(callback) {
486-
vm.detachDisk(zone.disk(name), execAfterOperationComplete(callback));
528+
vm.detachDisk(disk, execAfterOperationComplete(callback));
487529
}
488530
});
489531

@@ -523,8 +565,7 @@ describe('Compute', function() {
523565
var MAX_TIME_ALLOWED = 90000 * 2;
524566
this.timeout(MAX_TIME_ALLOWED);
525567

526-
var options = getOperationOptions(MAX_TIME_ALLOWED);
527-
vm.stop(execAfterOperationComplete(options, done));
568+
vm.stop(execAfterOperationComplete(done));
528569
});
529570
});
530571

@@ -624,26 +665,20 @@ describe('Compute', function() {
624665
});
625666
}
626667

627-
function getOperationOptions(maxTimeAllowed) {
628-
var interval = 10000;
629-
630-
return {
631-
maxAttempts: maxTimeAllowed / interval,
632-
interval: interval
633-
};
634-
}
635-
636-
function execAfterOperationComplete(options, callback) {
637-
callback = callback || options;
638-
668+
function execAfterOperationComplete(callback) {
639669
return function(err) {
640670
if (err) {
641671
callback(err);
642672
return;
643673
}
644674

645675
var operation = arguments[arguments.length - 2]; // [..., op, apiResponse]
646-
operation.onComplete(options || {}, callback);
676+
677+
operation
678+
.on('error', callback)
679+
.on('complete', function() {
680+
callback();
681+
});
647682
};
648683
}
649684
});

0 commit comments

Comments
 (0)