From 6d0f2b922db20b9153b0dfba382623cb4ed4b348 Mon Sep 17 00:00:00 2001 From: Olivier Poitrey Date: Thu, 29 Mar 2012 18:25:58 +0200 Subject: [PATCH] Add automatic exponential backoff retry on quota and temporary errors (fix #2) --- lib/c2dm.js | 89 ++++++++++++++++++++++++++++++++++++++-------------- package.json | 5 ++- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/lib/c2dm.js b/lib/c2dm.js index 908c9ae..e836b80 100644 --- a/lib/c2dm.js +++ b/lib/c2dm.js @@ -2,6 +2,7 @@ var util = require('util'); var https = require('https'); var querystring = require('querystring'); var emitter = require('events').EventEmitter; +var retry = require('retry'); function C2DM(config) { if (config) { @@ -76,33 +77,73 @@ C2DM.prototype.send = function(packet, cb) { var self = this; if (cb) this.once('sent', cb); - var postData = querystring.stringify(packet); - var headers = { - //'Connection': 'keep-alive', - 'Host': 'android.apis.google.com', - 'Authorization': 'GoogleLogin ' + this.token, - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-length': postData.length - }; - this.c2dmOptions.headers = headers; - if (this.keepAlive) - headers.Connection = 'keep-alive'; + var operation = retry.operation(); - var request = https.request(this.c2dmOptions, function(res) { - var data = ''; - function respond() { - var idx = data.indexOf('id='); - if (idx < 0) { - self.emit('sent', data, null); - } else { - self.emit('sent', null, data.substring(idx)); + operation.attempt(function(currentAttempt) { + var postData = querystring.stringify(packet); + var headers = { + //'Connection': 'keep-alive', + 'Host': 'android.apis.google.com', + 'Authorization': 'GoogleLogin ' + self.token, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-length': postData.length + }; + self.c2dmOptions.headers = headers; + if (self.keepAlive) + headers.Connection = 'keep-alive'; + + var request = https.request(self.c2dmOptions, function(res) { + var data = ''; + + if (res.statusCode == 503) { + // If the server is temporary unavailable, the C2DM spec requires that we implement exponential backoff + // and respect any Retry-After header + if (res.headers['retry-after']) { + var retrySeconds = res.headers['retry-after'] * 1; // force number + if (isNaN(retrySeconds)) { + // The Retry-After header is a HTTP-date, try to parse it + retrySeconds = new Date(res.headers['retry-after']).getTime() - new Date().getTime(); + } + if (!isNaN(retrySeconds) && retrySeconds > 0) { + operation._timeouts['minTimeout'] = retrySeconds; + } + } + if (!operation.retry('TemporaryUnavailable')) { + self.emit('sent', operation.mainError(), null); + } + // Ignore all subsequent events for this request + return; } - } - res.on('data', function(chunk) { + + function respond() { + var error = null, id = null; + + if (data.indexOf('Error=') === 0) { + error = data.substring(6).trim(); + } + else if (data.indexOf('id=') === 0) { + id = data.substring(3).trim(); + } + else { + // No id nor error? + error = 'InvalidServerResponse'; + } + + // Only retry if error is QuotaExceeded or DeviceQuotaExceeded + if (operation.retry(['QuotaExceeded', 'DeviceQuotaExceeded', 'InvalidServerResponse'].indexOf(error) >= 0 ? error : null)) { + return; + } + + // Success, return message id (without id=) + self.emit('sent', error, id); + } + + res.on('data', function(chunk) { data += chunk; + }); + res.on('end', respond); + res.on('close', respond); }); - res.on('end', respond); - res.on('close', respond); + request.end(postData); }); - request.end(postData); }; diff --git a/package.json b/package.json index 911e3d4..30adb4d 100644 --- a/package.json +++ b/package.json @@ -13,5 +13,8 @@ "url": "http://github.com/SpeCT/node-c2dm.git", "web" : "http://github.com/SpeCT/node-c2dm" }, - "engines": { "node": ">= 0.2.0" } + "engines": { "node": ">= 0.2.0" }, + "dependencies": { + "retry": ">=0.6.0" + } }