Execute multiple tasks asynchronously and return f

2019-02-13 21:41发布

问题:

I have to write a javaScript function that return some data to the caller.

In that function I have multiple ways to retrieve data i.e.,

  1. Lookup from cache
  2. Retrieve from HTML5 LocalStorage
  3. Retrieve from REST Backend (bonus: put the fresh data back into cache)

Each option may take its own time to finish and it may succeed or fail.

What I want to do is, to execute all those three options asynchronously/parallely and return the result whoever return first.

I understand that parallel execution is not possible in JavaScript since it is single threaded, but I want to at least execute them asynchronously and cancel the other tasks if one of them return successfully result.

I have one more question.

Early return and continue executing the remaining task in a JavaScript function.

Example pseudo code:

function getOrder(id) {

    var order;

    // early return if the order is found in cache.
    if (order = cache.get(id)) return order;

    // continue to get the order from the backend REST API.
    order = cache.put(backend.get(id));

    return order;
}

Please advice how to implement those requirements in JavaScript.

Solutions discovered so far:

  1. Fastest Result

    JavaScript ES6 solution

    Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Promise.race(iterable)

Returns a promise that resolves when the first promise in the iterable resolves.

var p1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, "one"); });
var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "two"); });
Promise.race([p1, p2]).then(function(value) {
  // value == "two"
});

Java/Groovy solution

Ref: http://gpars.org/1.1.0/guide/guide/single.html

import groovyx.gpars.dataflow.Promise
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Demonstrates the use of dataflow tasks and selects to pick the fastest result of concurrently run calculations.
 * It shows a waz to cancel the slower tasks once a result is known
 */

final group = new DefaultPGroup()
final done = new AtomicBoolean()

group.with {
    Promise p1 = task {
        sleep(1000)
        if (done.get()) return
        10 * 10 + 1
    }
    Promise p2 = task {
        sleep(1000)
        if (done.get()) return
        5 * 20 + 2
    }
    Promise p3 = task {
        sleep(1000)
        if (done.get()) return
        1 * 100 + 3
    }

    final alt = new Select(group, p1, p2, p3, Select.createTimeout(500))
    def result = alt.select()
    done.set(true)
    println "Result: " + result
}
  1. Early Return and Interactive Function

    Angular Promises combined with ES6 generators???

    angular.module('org.common')
    .service('SpaceService', function ($q, $timeout, Restangular, $angularCacheFactory) {
    
    
    var _spacesCache = $angularCacheFactory('spacesCache', {
        maxAge: 120000, // items expire after two min
        deleteOnExpire: 'aggressive',
        onExpire: function (key, value) {
            Restangular.one('organizations', key).getList('spaces').then(function (data) {
                _spacesCache.put(key, data);
            });
        }
    });
    /**
     * @class SpaceService
     */
    return {
        getAllSpaces: function (orgId) {
            var deferred = $q.defer();
            var spaces;
            if (spaces = _spacesCache.get(orgId)) {
                deferred.resolve(spaces);
            } else {
                Restangular.one('organizations', orgId).getList('spaces').then(function (data) {
                    _spacesCache.put(orgId, data);
                    deferred.resolve(data);
                } , function errorCallback(err) {
                    deferred.reject(err);
                });
            }
            return deferred.promise;
        },
        getAllSpaces1: function (orgId) {
            var deferred = $q.defer();
            var spaces;
            var timerID = $timeout(
                Restangular.one('organizations', orgId).getList('spaces').then(function (data) {
                    _spacesCache.put(orgId, data);
                    deferred.resolve(data);
                }), function errorCallback(err) {
                    deferred.reject(err);
                }, 0);
            deferred.notify('Trying the cache now...'); //progress notification
            if (spaces = _spacesCache.get(orgId)) {
                $timeout.cancel(timerID);
                deferred.resolve(spaces);
            }
            return deferred.promise;
        },
        getAllSpaces2: function (orgId) {
            // set up a dummy canceler
            var canceler = $q.defer();
            var deferred = $q.defer();
            var spaces;
    
            $timeout(
                Restangular.one('organizations', orgId).withHttpConfig({timeout: canceler.promise}).getList('spaces').then(function (data) {
                    _spacesCache.put(orgId, data);
                    deferred.resolve(data);
                }), function errorCallback(err) {
                    deferred.reject(err);
                }, 0);
    
    
            if (spaces = _spacesCache.get(orgId)) {
                canceler.resolve();
                deferred.resolve(spaces);
            }
    
            return deferred.promise;
        },
        addSpace: function (orgId, space) {
            _spacesCache.remove(orgId);
            // do something with the data
            return '';
        },
        editSpace: function (space) {
            _spacesCache.remove(space.organization.id);
            // do something with the data
            return '';
        },
        deleteSpace: function (space) {
            console.table(space);
            _spacesCache.remove(space.organization.id);
            return space.remove();
        }
    };
    });
    

回答1:

Personally, I would try the three asynchronous retrievals sequentially, starting with the least expensive and ending with the most expensive. However, responding to the first of three parallel retrievals is an interesting problem.

You should be able to exploit the characteristic of $q.all(promises), by which :

  • as soon as any of the promises fails then the returned promise is rejected
  • if all promises are successful then the returned promise is resolved.

But you want to invert the logic such that :

  • as soon as any of the promises is successful then the returned promise is resolved
  • if all promises fail then the returned promise is rejected.

This should be achievable with an invert() utility which converts success to failure and vice versa.

function invert(promise) {
    return promise.then(function(x) {
        return $q.defer().reject(x).promise;
    }, function(x) {
        return $q.defer().resolve(x).promise;
    });
}

And a first() utility, to give the desired behaviour :

function first(arr) {
    return invert($q.all(arr.map(invert)));
}

Notes:

  • the input arr is an array of promises
  • a native implementation of array.map() is assumed (otherwise you can explicitly loop to achieve the same effect)
  • the outer invert() in first() restores the correct sense of the promise it returns
  • I'm not particularly experienced in angular, so I may have made syntactic errors - however I think the logic is correct.

Then getOrder() will be something like this :

function getOrder(id) {
    return first([
        cache.get(id),
        localStorage.get(id).then(cache.put),
        backend.get(id).then(cache.put).then(localStorage.put)
    ]);
}

Thus, getOrder(id) should return a Promise of an order (not the order directly).



回答2:

The problem in your example getOrder lies in that if the 3 lookup functions are going to be asynchronous, you won't get the order back from them right away and as they are not blocking, the getOrder would return null; You would be better off defining a callback function which takes action on the first returned order data and simply ignores the rest of them.

var doSomethingWithTheOrder = function CallBackOnce (yourResult) {
    if (!CallBackOnce.returned) {
        CallBackOnce.returned = true;
        // Handle the returned data
        console.log('handle', yourResult);
    } else {
        // Ignore the rest
        console.log('you are too late');
    }
}

Make your data lookup functions accept a callback

function cacheLookUp(id, callback) {
    // Make a real lookup here
    setTimeout(function () {
        callback('order data from cache');
    }, 3000);    
}

function localeStorageLookUp(id, callback) {
    // Make a real lookup here
    setTimeout(function () {
        callback('order data from locale storage');
    }, 1500);    
}

function restLookUp(id, callback) {
    // Make a real lookup here
    setTimeout(function () {
        callback('order data from rest');
    }, 5000);    
}

And pass the callback function to each of them

function getOrder(id) {
    cacheLookUp(id, doSomethingWithTheOrder);
    localeStorageLookUp(id, doSomethingWithTheOrder);
    restLookUp(id, doSomethingWithTheOrder);
}


回答3:

Create a broadcast event in your api calls, then create $scope.$on to listen to those broadcast, when $on get's activated do the function that refreshes those objects.

So in your service have a function that makes an ajax calls to your rest api. You would have 3 ajax calls. And 3 listeners. Each of them would look something like this.

This is just sudo code, but this format is how you do it something like

 $http({
        method: "GET",
        url: url_of_api,
    }).success(function(data, *args, *kwargs){
        $rooteScope.$braodcast('success', data)
    })

In your controller have a listener something like this

$scope.$on('success', function(event, args){
 // Check the state of the other objects, have they been refreshed - you probably want to set flags to check 
  if (No Flags are Set):
      $scope.data = args // which would be the returned data adn $scope.data would be what you're trying to refresh.
}