Patterns - Fan-out/Fan-in

Overview

Fan-out/fan-in lets services invoke an arbitrary number of other services providing a set of callback services to be executed once all of the target services complete.

Optionally, additional callbacks can be invoked each time a service from the original target list finished execution.

Target services run in parallel, asynchronously, and are not notified of each other’s execution progress. Any callbacks configured also run asynchronously without blocking each other.

The diagram below depicts the feature in its full scope. A source service fans out to 2 target services, each with a list of their completion callbacks. Only when both target services finish execution, the flow of processing fans in to a final list of callbacks that are guaranteed that the targets have all been already invoked.

../../_images/fan-out-fan-in.png

Usage examples and API

Common usage

In the most common case, the usage will be as follows. A dictionary mapping targets and their requests along with a list of callbacks is provided to self.patterns.fanout.invoke. All of the targets are invoked in background and, once they all complete, each of the callbacks is invoked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from zato.server.service import Service

class MyService(Service):

    def handle(self):

        # A dictionary of services to invoke along with requests they receive
        targets = {
            'service1': {'hello':'from-fan-out1'},
            'service2': {'hello':'from-fan-out2'},
        }

        # Callbacks to invoke when both services above finish
        callbacks = ['my.callback1', 'my.callback2']

        # Call's Correlation ID is returned on output
        cid = self.patterns.fanout.invoke(targets, callbacks)

On-target-completion callbacks

Here additional callbacks are executed after each of the targets completes. This is in addition to the final callbacks that are executed regardless of whether target callbacks are used or not.

Note that target callbacks are always the same for each target, there are no means to specify distinct target callbacks for targets individually. However, callbacks can tell target services apart from the metadata they receive on input which contains the target's name.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from zato.server.service import Service

class MyService(Service):

    def handle(self):

        # A dictionary of services to invoke along with requests they receive
        targets = {
            'service1': {'hello':'from-fan-out1'},
            'service2': {'hello':'from-fan-out2'},
        }

        # Callbacks to invoke each time one of the services above completes
        target_callbacks = ['my.target.callback1', 'my.target.callback2']

        # Callbacks to invoke when both services above finish
        final_callbacks = ['my.callback1', 'my.callback2']

        # Call's Correlation ID is returned on output
        cid = self.patterns.fanout.invoke(targets, final_callbacks, target_callbacks)

User-defined Correlation IDs

Users can provide their own Correlation IDs to invoke the services with. It is user's responsibility to ensure the IDs are sufficiently unique and there will be no duplicates, should that happen - results are undefined.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from zato.server.service import Service

class MyService(Service):

    def handle(self):

        # A dictionary of services to invoke along with requests they receive
        targets = {
            'service1': {'hello':'from-fan-out1'},
            'service2': {'hello':'from-fan-out2'},
        }

        # Callbacks to invoke when both services above finish
        callbacks = ['my.callback1', 'my.callback2']

        # User-provided Correlation ID
        cid = '2963-2645-9715-1719'

        # CID returned is the one received on input
        cid = self.patterns.fanout.invoke(targets, callbacks, cid=cid)

Authoring callbacks

A callback is a regular Zato service that is invoked in specific moments of a fan-out/fan-in call. The same service can serve as a final and target callback. Each callback is invoked asynchronously, in isolation from any other callbacks defined.

Upon execution, each callback's self.channel attribute will be set to either FANOUT_ON_FINAL or FANOUT_ON_TARGET from zato.common.CHANNEL.

On input, a final callback's self.request.payload attribute will be a Python dictionary with the contents in the format as below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
 'source': 'fanout1.my-service',
 'req_ts_utc': '2015-01-19 19:55:46',
 'on_target': '',
 'on_final': ['my.cb1', 'zato.helpers.input-logger'],
 'data':
  {'my.service1': {
    'source': 'fanout1.my-service',
    'target': 'my.service1',
    'response': 'My response',
    'req_ts_utc': '2015-01-19 19:55:45',
    'resp_ts_utc': '2015-01-19 19:55:46',
    'ok': True,
    'exception': None,
    'cid': 'K05129BXK5ZJJABJ41CGPEMZG4J6'},
  'my.service2': {
    'source': 'fanout1.my-service',
    'target': 'my.service2',
    'response': 'My response2',
    'req_ts_utc': '2015-01-19 19:55:45',
    'resp_ts_utc': '2015-01-19 19:55:46',
    'ok': True,
    'exception': None,
    'cid': 'K05129BXK5ZJJABJ41CGPEMZG4J6'}}}
Key Notes
source Name of the original service issuing the fan-out/fan-in call
req_ts_utc When was the initial fan-out call executed, in UTC
on_target A list, if any, of callbacks to execute each time a target service completes
on_final A list of callbacks to invoke when all the targets complete
data

A dictionary of data describing responses from target services keyed by names of targets:

  • source - name of the original service issuing the fan-out/fan-in call (repeated)
  • target - name of a service, one of the targets (repeated)
  • response - response the service produced
  • req_ts_utc - request timestamp in UTC (repeated)
  • resp_ts_utc - when was the response created, in UTC
  • ok - True/False depending on whether the call was successful (raised no exceptions) or not
  • exception - exception's traceback as string, if ok is not True
  • cid - Correlation ID the service was given to its self.wsgi_environ's zato.request_ctx.fanout_cid key - this is the same CID the source service was given on output from self.patterns.fanout.invoke

On-target callbacks receive self.request.payload equal to an individual entry in the 'data' key above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  'source': 'fanout1.my-service',
  'target': 'my.service1',
  'response': 'My response',
  'req_ts_utc': '2015-01-19 19:55:45',
  'resp_ts_utc': '2015-01-19 19:55:46',
  'ok': True,
  'exception': None,
  'cid': 'K05129BXK5ZJJABJ41CGPEMZG4J6'
}

Similarities to Parallel Execution

The feature is very similar to the Parallel Execution pattern - the major difference being only that in Fan-out/Fan-in it is the final callbacks that are required while on-target ones are optional while in Parallel Execution there are no final callbacks while on-target are required.