Worker and Consumers Design Pattern
 

Worker and Consumers Design Pattern

A design pattern that has cropped up a lot in my Flash / AS3 endeavours is what I call the "Worker and Consumers" pattern.

1. Context

Multiple asynchronous calls are made. The first of these calls takes time to complete, but once complete, the result of the call can be memoized.

1.1. Example

When loading image resources, each image resource can be identified by a URL. Once the image resource has been retrieved and cached, further requests to load from the same URL can be served from the cache.

2. Problem

Managing the calls and recipients of the image data so that a minimum of calls are made.

3. Solution

First, each call is identified by a key. Calls with the same key produce the same results.

Second, we split the processing into two steps - the work step and the consume step. The former does the hard work, the second acts on the memoized result.

Third, we split the callers for each key into two sets: A set containing a single worker, and a set containing the rest, the consumers.

We define the following interface:

interface Caller {
    /**
     * Returns the memoization key for this call
     */
    function getKey () : String;
    
    /**
     * Performs the time-consuming work.
     *
     * @param onComplete a function taking a 
     * single Object parameter, called when the
     * work is completed.
     */
    function work (onComplete:Function) : void;
    
    /**
     * Invoked for all callers, worker and consumers,
     * to process the result.
     */
    function consume (result:Object) : void;
}

We then maintain two maps. One is a map from keys to the single worker: String -> Caller. The other is a map from keys to the vectors of consumers: String -> Vector.<Caller>.

The process is then as follows, in pseudo-AS3 code where some declaration have been left out for brevity:

function call (caller:Caller) : void {
    var key:String = caller.getKey ();
    
    // Do we have a result for that key?
    if (hasResult (key)) {
        // Yes, so consume and return
        caller.consume (getResult (key));
        return;
    }
    
    // Do we have a worker for this key?
    if (workers[key] == null) {
        // Yes, so add caller as a consumer
        if (consumerMap[key] == null) {
            consumerMap[key] = 
                new Vector.<Caller> ();
        }
        consumerMap[key].push (caller);
        return;
    } else {
        // No, this caller becomes the worker
        // for the key.
    
        // Add caller to worker map
        workers[key] = caller;
        
        // Start working
        caller.work (function (result:Object) : void {
            // Save the result
            putResult (key, result);
            
            // Pass the result to the worker
            caller.consume (result);
            
            // Pass the result to the waiting 
            // consumers
            if (consumers.containsKey (key)) {
                var cs:Vector.<Caller> = consumers[key];
                for (var i:int = 0; i < cs.length; ++i) {
                    cs[i].consume (result);
                }
            }

            // Clean up
            
            // Remove worker from worker map
            delete workers[key];
            // Remove all consumers from consumer map
            delete consumers[key];
        });
    }
}

4. Other Uses

I've also used the same pattern to handle Amazon Glacier retrieval requests. The worker starts the retrieval and stores the archive locally. The consumers then access this local copy.