问题
I'm getting data from an async service inside my controller like this:
myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
$scope.getData = function(query) {
return AsyncService.query(query).then(function(response) {
// Got success response, return promise
return response;
}, function(reason) {
// Got error, query again in one second
// ???
});
}
}]);
My questions:
- How to query the service again when I get error from service without returning the promise.
- Would it be better to do this in my service?
Thanks!
回答1:
You can retry the request in the service itself, not the controller.
So, AsyncService.query
can be something like:
AsyncService.query = function() {
var counter = 0
var queryResults = $q.defer()
function doQuery() {
$http({method: 'GET', url: 'https://example.com'})
.success(function(body) {
queryResults.resolve(body)
})
.error(function() {
if (counter < 3) {
doQuery()
counter++
}
})
}
return queryResults.promise
}
And you can get rid of your error function in the controller:
myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
$scope.getData = function(query) {
return AsyncService.query(query).then(function(response) {
// Got success response
return response;
});
}
}
]);
回答2:
This actually works:
angular.module('retry_request', ['ng'])
.factory('RetryRequest', ['$http', '$q', function($http, $q) {
return function(path) {
var MAX_REQUESTS = 3,
counter = 1,
results = $q.defer();
var request = function() {
$http({method: 'GET', url: path})
.success(function(response) {
results.resolve(response)
})
.error(function() {
if (counter < MAX_REQUESTS) {
request();
counter++;
} else {
results.reject("Could not load after multiple tries");
}
});
};
request();
return results.promise;
}
}]);
Then just an example of using it:
RetryRequest('/api/token').then(function(token) {
// ... do something
});
You have to require it when declaring your module:
angular.module('App', ['retry_request']);
And in you controller:
app.controller('Controller', function($scope, RetryRequest) {
...
});
If someone wants to improve it with some kind of backoff or random timing to retry the request, that will be even better. I wish one day something like that will be in Angular Core
回答3:
I wrote an implementation with exponential backoff that doesn't use recursion (which would created nested stack frames, correct?) The way it's implemented has the cost of using multiple timers and it always creates all the stack frames for the make_single_xhr_call (even after success, instead of only after failure). I'm not sure if it's worth it (especially if the average case is a success) but it's food for thought.
I was worried about a race condition between calls but if javascript is single-threaded and has no context switches (which would allow one $http.success to be interrupted by another and allow it to execute twice), then we're good here, correct?
Also, I'm very new to angularjs and modern javascript so the conventions may be a little dirty also. Let me know what you think.
var app = angular.module("angular", []);
app.controller("Controller", ["$scope", "$http", "$timeout",
function($scope, $http, $timeout) {
/**
* Tries to make XmlHttpRequest call a few times with exponential backoff.
*
* The way this works is by setting a timeout for all the possible calls
* to make_single_xhr_call instantly (because $http is asynchronous) and
* make_single_xhr_call checks the global state ($scope.xhr_completed) to
* make sure another request was not already successful.
*
* With sleeptime = 0, inc = 1000, the calls will be performed around:
* t = 0
* t = 1000 (+1 second)
* t = 3000 (+2 seconds)
* t = 7000 (+4 seconds)
* t = 15000 (+8 seconds)
*/
$scope.repeatedly_xhr_call_until_success = function() {
var url = "/url/to/data";
$scope.xhr_completed = false
var sleeptime = 0;
var inc = 1000;
for (var i = 0, n = 5 ; i < n ; ++i) {
$timeout(function() {$scope.make_single_xhr_call(url);}, sleeptime);
sleeptime += inc;
inc = (inc << 1); // multiply inc by 2
}
};
/**
* Try to make a single XmlHttpRequest and do something with the data.
*/
$scope.make_single_xhr_call = function(url) {
console.log("Making XHR Request to " + url);
// avoid making the call if it has already been successful
if ($scope.xhr_completed) return;
$http.get(url)
.success(function(data, status, headers) {
// this would be later (after the server responded)-- maybe another
// one of the calls has already completed.
if ($scope.xhr_completed) return;
$scope.xhr_completed = true;
console.log("XHR was successful");
// do something with XHR data
})
.error(function(data, status, headers) {
console.log("XHR failed.");
});
};
}]);
回答4:
Following this article Promises in AngularJS, Explained as a Cartoon
you need to retry only when the response comes under 5XX category
I have written a service called http which can be called by passing all http configs as
var params = {
method: 'GET',
url: URL,
data: data
}
then call the service method as follows:
<yourDefinedAngularFactory>.http(params, function(err, response) {});
http: function(config, callback) {
function request() {
var counter = 0;
var queryResults = $q.defer();
function doQuery(config) {
$http(config).success(function(response) {
queryResults.resolve(response);
}).error(function(response) {
if (response && response.status >= 500 && counter < 3) {
counter++;
console.log('retrying .....' + counter);
setTimeout(function() {
doQuery(config);
}, 3000 * counter);
} else {
queryResults.reject(response);
}
});
}
doQuery(config);
return queryResults.promise;
}
request(config).then(function(response) {
if (response) {
callback(response.errors, response.data);
} else {
callback({}, {});
}
}, function(response) {
if (response) {
callback(response.errors, response.data);
} else {
callback({}, {});
}
});
}
回答5:
I ended up doing this a lot so I wrote a library to help address this problem : )
https://www.npmjs.com/package/reattempt-promise-function
In this example you could do something like
myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
var dogsQuery = { family: canine };
$scope.online = true;
$scope.getDogs = function() {
return reattempt(AsyncService.query(dogsQuery)).then(function(dogs) {
$scope.online = true;
$scope.dogs = dogs;
}).catch(function() {
$scope.online = false;
});
}
}]);
来源:https://stackoverflow.com/questions/19595886/angularjs-service-retry-when-promise-is-rejected