Connecting to the Avid Platform using the Avid Connector API

Connecting to the Avid Platform using the Avid Connector API

The Avid Connector API connects to the Avid Platform over the Avid Secure Gateway. To connect your service to the Avid Platform, the Avid Secure Gateway must be running on a known host and port. The default port is 9900.

Default Connection Settings

By default the Avid Connector API connects using the following default options:

  • Gateway Port: 9900
  • Gateway Host: 127.0.0.1
  • Query Timeout (ms): 10,000
  • Connection Lost Threshold: 2000ms
  • Reconnection Delay: 2000ms
  • Initial Connection Attempts: -1 (infinite)
  • Reconnection Attempts: -1 (infinite)
  • Bus exponential back-off reconnection duration for unlicensed connection (ms): 600000
  • Bus exponential back-off reconnection jitter for unlicensed connection: true

When connecting to Avid Platform, authentication provider may be specified with valid ClientId and ClientSecret. If service is started from trusted IP address, it may be started w/o authentication provider. In this case authentication provider must be null:

To connect to the Avid Platform using the default settings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var bal = require('proxy-bal');

var access = bal.createAccess();
access.connect();

access.on('connected', function() {
console.log("I'm on the Avid Platform!");
});

process.on('SIGINT', function() {
console.log("Disconnecting due to process exit!");
access.disconnect();
process.exit(0);
});

To connect to the Avid Platform with authentication provider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var bal = require('proxy-bal');
var auth = require('proxy-bal').auth;

var config = {
gateway: {
auth: new auth("620f8ca60b0e11e6b5123e1d05defe78", "2078884c3d6311e6ac619e71128cae77")
}
};
var access = bal.createAccess(config);
access.connect();

access.on('connected', function() {
console.log("I'm on the Avid Platform!");
});

process.on('SIGINT', function() {
console.log("Disconnecting due to process exit!");
access.disconnect();
process.exit(0);
});

Overriding Default Settings with Environment Variables

Some of the default connection settings can be overridden using environment variables, as indicated in the following table:

Environment Variable Description
ACS_BUS_QUERY_TIMEOUT The default timeout (in ms) for queries. Default is 10000 ms.
ACS_GATEWAY_HOST Gateway connection host (Default 127.0.0.1)
ACS_GATEWAY_PORT Gateway connection port (Default 9900)
ACS_GATEWAY_UNSECURE_PORT Port for not secured connection. Default is 9966
ACS_GATEWAY_PROTOCOL_SEQUENCE Sequence of protocols (or just one protocol) separated by coma ‘,’ in which Avid Connector API will try to establish connection to Secure Gateway. Default is ‘wss’. Allowed protocols are ‘wss’ and ‘ws’. Possible combinations are ‘wss,ws’, ‘ws’, ‘wss’ or ‘ws,wss’.
ACS_PLATFORM_IDENTIFIER Unique node identifier, where wrapper is running, provided by target platform, i.e. AWS, Open Stack etc. (Default ‘unknown’)
ACS_SERVICE_BUILD_NUMBER RPM or any other binary version of the service, hosted by the wrapper (Default ‘unknown’)
ACS_ENVIRONMENT_IDENTIFIER Environment identifier is basically chef generalized identifier for any collection of nodes (Default ‘unknown’)
ACS_GATEWAY_CONNECTION_LOST_THRESHOLD Amount of time, after which connection to Gateway considered as broken if we didn’t get ping from gateway. (Default 5000 ms)
ACS_BUS_INITIAL_CONNECTION_ATTEMPTS Number of initial connection attempts to Secure Gateway. Default is -1, means infinite number of connections.
ACS_GATEWAY_CONNECTION_LOST_THRESHOLD Amount of time, after which connection to Gateway considered as broken if we didn’t get ping from gateway. (Default 5000 ms)
ACS_BUS_RECONNECTION_ATTEMPTS Number of subsequent reconnections to Secure Gateway. Default is -1, means infinite number of reconnections.
ACS_BUS_RECONNECTION_DELAY Delay in ms between connection attempts to Secure Gateway. This delay same both for initial connection attempts and reconnections attempts. Default is 1000 ms.
ACS_BUS_MAX_BACK_OFF_DURATION Maximum duration of back-off re-connection attempts for unlicensed connection. (Default 600000 ms)
ACS_BUS_BACK_OFF_JITTER Back-off reconnection jitter for unlicensed connection. (Default true)
ACS_SECURITY_TRUST_SELF_SIGNED Whether to trust (true) or not trust (false) to self signed certificates (Default true)
ACS_METRICS_TAG Metric tag used by metrics to make metric unique. Default value is “default”.
ACS_METRICS_ENABLED Whether or not metrics are enabled. By default metrics are enabled.
ACS_METRICS_REPORT_INTERVAL Defines metrics reporter interval in seconds (default 10)
ACS_WEBSOCKET_HANDSHAKE_TIMEOUT Timeout in milliseconds for the handshake request
ACS_WEBSOCKET_KEEPALIVE_INTERVAL Keepalive interval in milliseconds for sending ping message
ACS_HTTP_SERVER_ENABLED Enables Http Server (now only Liveness Probe reporting functionality). Defaults to false.
ACS_HTTP_SERVER_LISTEN_HOST Ip address or host to bind HTTP server to. Defaults to 0.0.0.0 .
ACS_HTTP_SERVER_LISTEN_PORT Port for HTTP liveness server to listen to. Defaults to 9991.

Overriding Default Settings in Code

You can override the default settings directly in your code by passing an options object when you create the Access object. The following example shows the possible settings:

1
2
3
4
5
6
7
var access = bus.createAccess({
gateway: {
'port': 9900,
'host': '127.0.0.1'
}
});
access.connect();

Note that you only need to pass in the settings you wish to override. For example:

1
2
3
4
5
6
var access = bus.createAccess({
gateway: {
'port': 9300
}
});
access.connect();

Debugging the Gateway Connection

The Avid Connector API and the gateway have an internal failover logic that validates the connection between them. If you are debugging your service in an IDE or on the command line, a breakpoint can block your service from receiving the information it needs to know that it is still connected to the Secure Gateway. To prevent the service from thinking it has lost its connection to the Secure Gateway, and thus failing over into reconnection mode, you should set the variable ACS_GATEWAY_CONNECTION_LOST_THRESHOLD=600000 (10 minutes). This should prevent the service from thinking it has lost its connection while giving you enough time to inspect the information you need at the breakpoint.

Threading Model

Due to the nature of the Node.js (javascript), everything is executed in the event loop and it is not possible to configure number of threads for processing incoming or outgoing messages.

Alternatively you may think about multiprocess execution of the Node.js services to distribute load, or start service on several nodes in cluster.

Using the Avid Connector API as a Client

The Avid Connector API can be used to query services, send to services, and broadcast to services. Messages are a simple JavaScript object of the following format:

1
2
3
4
5
6
7
8
9
10
{
"serviceType": "com.avid.calculator",
"serviceRealm": "global",
"serviceVersion": 0,
"op": "add",
"paramSet" : {
"num1": 40,
"num2": 2
}
}

Providing Message Options

With each message operation (e.g query, send and broadcast) you may provide message options. The following options are available:

  • timeout - Specifies a message timeout in ms for query operations. Default is 10000ms.
  • durable - Sets whether message should be durable or not. Default is false. NOTE: This option is currently not implemented, and will be revised in the future releases.
  • anyCompatibleVersion - Set whether the message should be delivered to any compatible version of the service or to an exact version of the service. Default is true.
1
2
3
4
5
var opt = {
durable: true,
timeout: 40000,
anyCompatibleVersion: false
}

Querying Services

Service queries are messages that invoke a service operation and expect a response. Response and timeouts are returned via separate callbacks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var m = {
serviceType: 'avid.acs.calculator',
serviceRealm: 'global',
serviceVersion: 3,
op: 'add',
'paramSet' : {
'num1': 40,
'num2': 2
}
}

access.query(m, function(reply) {
if (reply.errorSet) {
console.error("Received errors:", reply.errorSet);
} else {
console.log("The meaning of life is ", reply.resultSet.sum);
}
}, function() {
console.log("Timeout");
});

// or with options

var opt = {
durable: true,
timeout: 40000,
anyCompatibleVersion: false
}

access.advancedQuery(m, opt, function(reply) {
if (reply.errorSet) {
console.error("Received errors:", reply.errorSet);
} else {
console.log("The meaning of life is ", reply.resultSet.sum);
}
}, function() {
console.log("Timeout");
}, function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Message sent successfully");
}
});

Sending to Services

Sending to a service is a one-way communication from the client to the service. There is no response. In comparison to a broadcast (described below), the client is also guaranteed that no more than one service instance will process the message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var m = {
serviceType: 'avid.acs.calculator',
serviceRealm: 'global',
serviceVersion: 3,
op: 'add',
'paramSet' : {
'num1': 40,
'num2': 2
}
}

var callback = function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Message sent successfully");
}
};

access.send(m, null, callback);

// or with options

var opt = {
anyCompatibleVersion: false
}

access.send(m, opt, callback);

Broadcasting to Services

Broadcasting to a service is a one-way communication from the client to all instances of a given service. There is no response. As opposed to a send, the message is processed by every available instance of the service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var m = {
serviceType: 'avid.acs.calculator',
serviceRealm: 'global',
serviceVersion: 3,
op: 'add',
'paramSet' : {
'num1': 40,
'num2': 2
}
};

var callback = function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Message sent successfully");
}
};

access.broadcast(m, null, callback);

// or with options

var opt = {
anyCompatibleVersion: false
}

access.broadcast(m, opt, callback);

Remote Zone and Multi-Zone Communications

The default behavior of the Avid Connector API is to communicate within its own local zone. All of the example above use this default behavior. If the local zone has been initialized in a multi-zone environment, however, it is possible to also communicate with services in other zones.

Zone-Specific Communications

To communicate with a specific remote zone, use the access.zone(zoneID) object. The following are examples of communications with a specific remote zone:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var m = {
serviceType: 'avid.acs.calculator',
serviceRealm: 'global',
serviceVersion: 3,
op: 'add',
'paramSet' : {
'num1': 40,
'num2': 2
}
}

access.zone("85a6af75-5fa1-46d7-bb09-66f4cc1cf9f7").query(m, function(reply) {
// handle reply
}, function() {
// handle timeout
});
access.zone("85a6af75-5fa1-46d7-bb09-66f4cc1cf9f7").send(m, null, callback);
access.zone("85a6af75-5fa1-46d7-bb09-66f4cc1cf9f7").broadcast(m, null, callback);

In all the above cases, only services in the remote zone with an ID of 85a6af75-5fa1-46d7-bb09-66f4cc1cf9f7 are invoked. In addition, only the service instances registered with a scope of multi-zone are considered.

Muti-Zone Communications

To communicate across multiple zones, use the access.multiZone() object. The following are examples of multi-zone communications:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var m = {
serviceType: 'avid.acs.calculator',
serviceRealm: 'global',
serviceVersion: 3,
op: 'add',
'paramSet' : {
'num1': 40,
'num2': 2
}
}

access.multiZone().query(m, function(reply) {
// handle reply
});
access.multiZone().send(m);
access.multiZone().broadcast(m);

Note that access.multiZone().query and access.multiZone().send only send to one service instance in one zone. If there is a service instance in the local zone, it sends to that one. Otherwise it sends to a service instance in a remote zone (if one is available and registered with the multi-zone scope). This is particularly useful if you know the service is in a zone, but aren’t sure which one.

access.multiZone().broadcast broadcasts to all matching service instances in all zones.

Local Zone Communications

Note that there is also an access.localZone() object. Invoking query, send, and broadcast on this object is functionally equivalent to invoking the same methods on the base access object.

Wildcard Usage in the Realm

If multiple realms of the same service are registered, you can use wildcards to address any service instance satisfying the wildcard expression. Wildcards can substitute any letters/digits between dots.

For example, consider an Avid Platform service registered with the following realms:

1
2
3
4
5
6
montreal.workgroup1;
montreal.workgroup2;
montreal.workgroup1.id1;
montreal.workgroup1.id2;
montreal.workgroup2.id1;
montreal.workgroup2.id2;

In the above case, you can address the services by supplying the following wildcarded realms in the request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var m = {
serviceType: 'example.service',
serviceRealm: '*.*',
serviceVersion: 0,
op: 'doSomething'
}

var m = {
serviceType: 'example.service',
serviceRealm: '*.*.*',
serviceVersion: 0,
op: 'doSomething'
}

var m = {
serviceType: 'example.service',
serviceRealm: 'montreal.*',
serviceVersion: 0,
op: 'doSomething'
}

var m = {
serviceType: 'example.service',
serviceRealm: 'montreal.workgroup1.id*',
serviceVersion: 0,
op: 'doSomething'
}

var m = {
serviceType: 'example.service',
serviceRealm: 'montreal.workgroup*.id2*',
serviceVersion: 0,
op: 'doSomething'
}

var m = {
serviceType: 'example.service',
serviceRealm: 'montreal.*group*.id2',
serviceVersion: 0,
op: 'doSomething'
}

// etc...

In contrast, the following wildcarded realms won’t match any instance in the above example:

*
*.*.*.*
montreal.workgroup*.id3

etc...

Using the Avid Connector API to Host Services

You can use the Avid Connector API to host services on the Avid Platform.

Anatomy of a Node.js Platform Service

In its simplest form, a Node.js Avid Platform service is simply an extension of the Service prototype with functions for each operation. The following is a basic example of an Avid Platform service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
var util = require('util');
var bal = require('proxy-bal');

var CalculatorService = function(access, options) {

var apiInfo = {
serviceType: 'avid.acs.calculator',
serviceRealm: 'global',
serviceVersion: 3,
description: "Calculator",
ops: {
add: {
description: "Adds 2 values",
examples: {
'Add': {
paramSet: {
num1: 5,
num2: 3
}
}
},
rest: {
path: "calculator/add",
queryParams: ["num1", "num2"],
method: "GET"
}
},
divide: {
description: "Divides 2 values",
examples: {
'Divide': {
paramSet: {
num1: 12,
num2: 3
}
},
'Divide By Zero': {
paramSet: {
num1: 3,
num2: 0
}
}
}
}
}
}

CalculatorService.super_.call(this, access, apiInfo, options);
};
util.inherits(CalculatorService, bus.Service);

CalculatorService.prototype.onInitialize = function(init_proceed) {
// do some initialization here and call init_proceed when done
init_proceed();
};

CalculatorService.prototype.onStop = function(stop_procees) {
// do some stop/release resources here and call init_proceed when done
init_proceed();
};

CalculatorService.prototype.add = function(m, operationContext) {
operationContext.reply({
resultSet: {
sum: parseInt(m.paramSet.num1) + parseInt(m.paramSet.num2)
}
});
}

CalculatorService.prototype.divide = function(m, operationContext) {
if (0 === parseInt(m.paramSet.num2)) {
operationContext.error({ code: "BAD_REQUEST", details: 'Cannot divide by zero' });
} else {
operationContext.reply({
resultSet: {
quotient: parseInt(m.paramSet.num1) / parseInt(m.paramSet.num2)
}
});
}
}

module.exports = function(access, options) { return new CalculatorService(access, options); }

The API Info Object

In the above example, the apiInfo object defines the external API of the Avid Platform service. This external API consists of the service type, realm, and version info, as well as the set of operations it supports. The operations in apiInfo can also inline examples that will be available via the ACS Monitor web page.

The bal.Service Super Class

In the above example, the calculator service inherits from bal.Service. The bal.Service class is defined by the Node.js Bus Access Layer (BAL) and provides much of the base functionality needed to host the service on the Avid Platform.

Note that the service’s super-constructor is called with four arguments: i) a reference to the service instance, ii) a reference to the access instance, iii) the api info, and iv) the options object.

The onInitialize Function

When you register a service on the Avid Platform, its onInitialize function is invoked with a single argument: the init_proceed argument. This allows your service to do any configuration or initialization that is needed before it becomes available for remote invocations. Once initialization is done, the service should call the init_proceed callback.

If a service does not require initialization, it does not need to implement the onInitialize function. It is implemented in the example above for demonstration purposes only.

The onStop Function

When a service is stopped by the wrapper, its onStop function is invoked with a single argument: the stop_proceed argument. This allows the service to do any resource releasing before completely stopping the service. Once the work associated with stopping is done, it should call the stop_proceed callback.

NOTE: Don’t exit process in the onStop implementation. The API’s implementation of this method is subject to revision. Do not depend on it in this manner.

The Operation Implementations

For each operation in the service’s register API (see apiInfo), there must be a function of the same name. For example, if the apiInfo lists a multiply operation, then CalculatorService.prototype.multiply must be defined.

Each function that supports an Avid Platform service operation must have the following signature: function(m, operationContext). In this case, m is the incoming request message and operationContext is the object used to send back the reply or errors.

Using the OperationContext object

Service operations all have the signature function(m, operationContext) and the operationContext provides methods for handling the incoming request. The OperationContext has taken the place of the responder (which has been deprecated) and extended to include additional methods for use when forwarding transaction information to other services while in a service operation. Like the responder, the OperationContext will only accept calls to reply() or error() once. These calls send the response back to the callee and therefore cannot be called again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
MyService.Prototype.example = function(m, operationContext) {

////////////////////////////////////////////////////////////////////////////
// Sends a manual acknowledgment that the message was recieved
operationContext.ack();

////////////////////////////////////////////////////////////////////////////
// Respond to the callee with only servcie generated resultSet
operationContext.reply({
example : 'response'
});

////////////////////////////////////////////////////////////////////////////
// Respond to the callee with resultSet and additional context information
operationContext.reply({
resultSet : {
example : 'response'
},
context : {
id : 1234,
}
});

////////////////////////////////////////////////////////////////////////////
// Respond to the callee with a structured error (#structured-errors)
operationContext.error({
code : 'NOT_FOUND',
params : {
info : 'Could not find the specified identity',
identity : {
id : '51423',
user : 'Joe'
}
},
details : 'The identity for the user could not be found in the database'
});

////////////////////////////////////////////////////////////////////////////
// Respond to the callee with an array of errors
operationContext.error([{
code : 'NOT_FOUND',
params : {
info : 'Could not find the specified identity',
identity : {
id : '51423',
user : 'Joe'
}
},
details : 'The identity for the user could not be found in the database'
}, {
code : 'DB_CONNECTION',
params : {
identity : {
id : '51423',
user : 'Joe'
}
},
details : 'The connection to the database was lost'
}]);

////////////////////////////////////////////////////////////////////////////
// Get the full original message sent by the calee
const msg = operationContext.getMessage();

////////////////////////////////////////////////////////////////////////////
// Get a proxy Access object which can be used to forward the transaction
// information when calling other services. This should be called by service writers
// prior to making any calls to other services to preserve the original transaction
// information.
// This proxy will update calls to send(), broadcast(), query(), advancedQuery(),
// and postToChannel with the incoming transaction info in the context.

const contextBusAccess = operationContext.getBusAccess();

const msg = {
serviceType : 'other.service',
serviceRealm : 'global',
serviceVersion : 2,
op : greet,
paramSet : {
greeting : 'Hello there!'
}
};

// Now use that to pass the information along to the next service
// The new access object will add the transaction object from the context in the
// received message to update it like this.
// {
// context : {
// sender : {
// transaction : {
// id : '79333266-cc0d-435a-a032-59795a9c52bd',
// origin : 'client'
// }
// }
// },
// serviceType : 'other.service',
// serviceRealm : 'global',
// serviceVersion : 2,
// op : greet,
// paramSet : {
// greeting : 'Hello there!'
// }
// }

contextBusAccess.advancedQuery(msg, {}, function(reply) {
// Tell listeners that the greeting was sent, again the
// transaction information will be forwarded along.
contextBusAccess.postToChannel({
channel : 'greet.channel',
subject : 'hello',
dataSet : {
greetingSent : true
}
}, function(err) {
// Handle postToChannel operation was successful or not
});
}, function() {
// Timeout
}, function(err) {
// Handle message send ok/error
}
}

Registering the Service

NOTE: registerService method arguments are changed. Now 2-nd argument is options and third is callback. Previously second (last) argument was callback. Backward compatibility kept. If two arguments passed, and second one is function the old semantic is used.

Once you define your service, the next step is registering it on the Avid Platform. There are two ways to register on the Avid Platform:

Registering Using access.registerService

The first way to register a service is to construct the service instance yourself and then register it on a connected access:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var bal     = require('proxy-bal');
var calc = require('./calculator-service');

var access = bal.createAccess();
access.connect();

access.on('connected', function() {
var onRegistered = function(err, service) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Service is registered successfully with id: " + service.info.id);
}
};

var myCalc = calc(access, {});

access.registerService(myCalc, {}, onRegistered);
});

Alternatively you can register a service by passing the service module. This functionality was moved from the load method which has been deprecated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var bal = require('proxy-bal');
var calc = require('./calculator-service');

var access = bal.createAccess();
access.connect();

access.on('connected', function() {
var onRegistered = function(err, service) {
if (err) {
console.err('Error message: ' + error.errMessage);
console.err('Error type: ' + err.errorType);
} else {
console.log('Service is registered successfully with id: ' + service.info.id);
}
};

access.registerService(require('./calculator-service'), {}, onRegistered);

// NOTE : The load method allowed services to be configured as multizone with a simple object. This is not possible when using the registerService API.
// if you wish to register your service as a multizone service you should use the convention used by the rest of the connector API.

// access.multiZone().registerService('./calculator-service'), onRegistered);
});

If you wish to register your service this way, then the service must export a two-argument constructor similar to the following:

1
module.exports = function(access, options) { return new CalculatorService(access, options); }

Registering Using access.load

NOTE : The load method has been deprecated and will evenutally be removed from the Avid Connector API, please use the functionality provided by registerService. See above for migration details when using registerService.

Register Service With Custom Options

You may configure following service options for service registration:

  • requestServiceConfiguration, whether to request service configuration from Service Manager or not. Default is true;
  • startSuspended, whether to start service in suspended mode or not. Default is false. Note, if service developer start service in suspended mode, it’s service developer responsibility to change service state to OK or any other appropriate according to service business logic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var bal     = require('proxy-bal');
var calc = require('./calculator-service');

var access = bal.createAccess();
access.connect();

access.on('connected', function() {
var options = {
requestServiceConfiguration: true,
startSuspended: true
};

var onRegistered = function(err, service) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Service is registered successfully with id: " + service.info.id);
}
};

var myCalc = calc(access, {});

access.registerService(myCalc, options, onRegistered);
});

For information how to subscribe for configuration notifications look Getting Configuration Information from a Service Manager

Unregistering the Service

To unregister a service:

1
2
3
4
5
6
7
8
access.unregisterService(function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
}

// Do something when the service is unregistered
});

Structured Errors

Declaration of the structured errors in the service contract

A service must declare its complete list of possible error codes in the errorCodes object of the service contract JSON. Example of the service with errors definition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var CalculatorService = function(access, options) {

var apiInfo = {
serviceType: 'avid.acs.calculator',
serviceRealm: 'global',
serviceVersion: 3,
description: "Calculator",
ops: {
power: {
description: "Returns the power of two numbers.",
examples: { 'power': { num1: 2, num2: 3 } },
rest: {
path: "calculator/power",
queryParams: ["num1", "num2"],
method: "GET"
}
},
divide: {
description: "Divides 2 values",
examples: {
'Divide': {
paramSet: {
num1: 12,
num2: 3
}
},
'Divide By Zero': {
paramSet: {
num1: 3,
num2: 0
}
}
}
}
},
errorCodes: {
"DIVIDE_BY_ZERO": {
"status": 500,
"severity": "ERROR",
"messageTemplate": "Division by zero, %{num1} and %{num2}",
"origin": "SERVICE"
},
"POWER_FAILURE": {
"status": 500,
"severity": "ERROR",
"messageTemplate": "power has thrown an exception, %{expt}, %{num1} and %{num2}",
"origin": "SERVICE"
}
}
}

CalculatorService.super_.call(this, access, apiInfo, options);
};
  • code is the name of the object. This parameter is part of the service’s public API and therefore the format of the code should be a short sentence in uppercase with an underscore symbol used as a separator, e.g. BAD_REQUEST, BULK_DUPLICATION, MISSING_ARGUMENT, QUOTA_EXCEEDED.
  • status parameter is the appropriate corresponding HTTP error code.
  • messageTemplate parameter is text in en_US locale, that may include %{identifier} placeholders for error message parameters. The messageTemplate (and any parameters) should convey meaningful information e.g. “Quota on %{resourceName} exceeded for %{projectName}.”
  • severity parameter is the severity level of the error. Available severity levels are: ‘EMERGENCY’, ‘ALERT’, ‘CRITICAL’, ‘ERROR’, ‘WARNING’, ‘NOTICE’, ‘INFO’, ‘DEBUG’.

Composing response message with structured errors

To compose message with structure errors operationContext must be used as second argument of the service operation implementation. Using above example of error declaration, implementation of two methods, which produce POWER_FAILURE and DIVIDE_BY_ZERO errors may look following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
CalculatorService.prototype.divide = function(m, operationContext) {
if (m.paramSet.num2 === 0) {
operationContext.error({
code: "DIVIDE_BY_ZERO",
params: {
num1: m.paramSet.num1,
num2: m.paramSet.num2
},
details: "Division on zero"
});
} else {
operationContext.reply({
quotient : m.paramSet.num1 / m.paramSet.num2
});
}
};

CalculatorService.prototype.power = function(message, operationContext) {
var self = this;
var params = message.paramSet || {}
var response = undefined;

var exception = {};
try {
response = Math.pow(params.num1, params.num2);
} catch (e) {
exception = e;
response = undefined;
}

if (!!response) {
if (response === Infinity) {
exception = { err: "Infinity" };
response = undefined;
} else if (response === -Infinity) {
exception = { err: "-Infinity" };
response = undefined;
}
}

if (response != undefined) {
operationContext.reply({
power : response
});
} else {
operationContext.error({
code: "POWER_FAILURE",
params: {
expt: JSON.stringify(exception),
num1: params.num1,
num2: params.num2
},
details: "Power has thrown an exception"
});
}
}

Multi-Zone Services

The default behavior of the Avid Connector API is to register services in the local zone scope. This means that by default services only receive requests from clients within the same zone. If the local zone has been initialized in a multi-zone environment, however, it is possible to register a service in multi-zone scope. This allows the service to be invoked by clients in any connected zone.

To register a service in the multi-zone scope, use the access.multiZone() object:

1
2
3
4
5
6
7
8
9
10
11
12
var bal     = require('proxy-bal');
var calc = require('./calculator-service');

var access = bal.createAccess();
access.connect();

access.on('connected', function() {
var myCalc = calc(access, {});
access.multiZone().registerService(myCalc, function(err, svc) {
// registered
});
});

If you wish to use the access.load() method to register your service you must provide the appropriate options to make sure your service is registered as a multizone service.

1
2
3
4
5
6
7
8
9
10
11
12
13
var bal = require('proxy-bal');
var calc = require('./calculator-service');

var access = bal.createAccess();
access.connect();

access.on('connected', function() {
access.load(
require('./calculator-service'),
{
multiZone : true
});
});

Local Zone Scope

Registering a service using the access.localZone() object is functionally equivalent to registering it using the base access object. The service is only accessible within the local zone.

Getting Configuration Information from a Service Manager

To intercept configuration provided by a Service Manager, the service must be subscribed to the configured event.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var BusService = function(bus_access, options) {
var self = this;

BusService.super_.call(
this,
bus_access, {
serviceType: "avid.acs.calculator",
serviceRealm: "global",
serviceVersion: 3,
description: "Calculator",
ops: {
}
},
options);

self.on('configured', function (config, service, configurationUpdate, err) {
// process new config here
if (configurationUpdate) {
// this is configuration update
} else {
// this is initial configuration when service registered
}

if (err != null) {
// some errors occurred
}

// update service status
service.setStatus(Status.OK, "Service is configured", function(err) {
if (err) {
self.logger.info("Failed to set status. Error: %s", err, {});
}
});
});
};

util.inherits(BusService, bal.Service);

To set service configuration in the Service Manager, send the following request with your configuration in the serviceConfiguration object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"serviceType": "avid.acs.service.manager",
"serviceVersion": 3,
"serviceRealm": "global",
"op": "registerService",
"paramSet": {
"serviceType": "avid.acs.calculator",
"serviceVersion": "3",
"serviceRealm": "global",
"serviceConfiguration": {
"yourFieldsObjects": "and their values goes here"
}
}
}

Providing Custom Service Health Information

One of the core operations provided by the Avid Connector API is serviceHealth. By default if service receives this operation request it replies back with such message in resultSet:

1
2
3
4
5
6
7
{
"service": {
"instanceId": "955742c3-1fdd-4edd-be71-78aeb837aaa0",
"healthStatus": "ok",
"healthVerifier": "default"
}
}

Service developers may overwrite serviceHealth operation by providing specific healthStatus or additional customHealthInfo. In this case serviceHealth response may look like:

1
2
3
4
5
6
7
8
9
10
11
{
"service": {
"customHealthInfo": {
"str": "some string data",
"flag": true
},
"instanceId": "955742c3-1fdd-4edd-be71-78aeb837aaa0",
"healthStatus": "error",
"healthVerifier": "custom"
}
}

To overwrite serviceHealth operation, overwrite onHealthCheck method in your service and make it taking one callback argument report:

1
2
3
4
5
6
7
8
9
10
11
var HealthStatus  = require('proxy-bal').health.HealthStatus;

SampleService.prototype.onHealthCheck = function(report) {
report(HealthStatus.ERROR, {
str: "Some string data",
flag: true,
nested: {
number: 3
}
})
};

Changing Service Status

Sometimes, resources external to a service will have errors or act in an unexpected way. When this happens services do not need to stop while dealing with the external resource. The service can change its status to give insight into what is wrong. It also allows services to stay registered on the platform while the resource is dealt with appropriatly. The connector API supports 5 status states (OK, WARNING, ERROR, SUSPENDED, OFFLINE). Each state has its own behavior which define whether the service is still visible on the platform, and if it is able to recieve messages. See the table below to see the behavior defined by each status.

Status Visible on Platform Receives Requests Example Use Case
OK Yes Yes Service is fully functional (default state)
WARNING Yes Yes Service is still fully functioning, but want to warn about some resource (ie. high memory useage, large db latency, many timeouts to another service)
ERROR Yes No Service is not functioning properly, needs some intervention to fix (ie. ran out of memory, but wish to keep the process alive for debugging)
SUSPENDED Yes No Service is ok, but cannot function properly due to an external resource (ie. DB connection lost and service cannot proceed without persisting data)
OFFLINE No No Service process should stay alive, but should not be visible or routed to (ie. Keep service process alive during DB migration)

Setting the Service Status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var access = require('proxy-bal').createAccess();
// easily access the available service states
var Status = require('proxy-bal').Service.StatusEnum;
var Service = require('my-service');

var service = new Service();

access.connect();

access.registerService(service, function(err, svc) {
// Oh no we lost connection to our db
if (!DB.isConnected) {
// Suspend our service so that we don't receive new messages with information that must be persisted
service.setStatus(Status.SUSPENDED, 'Connection to Database is lost', function(err) {
if (err) {
console.log('Error trying to set service status')
console.log(err);
}
});
}
});


// At some later time we have re-established our connection to the DB
service.setStatus(Status.OK, 'Service OK', function(err) {
if (err) {
console.log('Error trying to set service status')
console.log(err);
}
});

Providing Compatible Version

Services on the Avid platform provide the concept of ‘Compatible Versions’ to provide a way to remain backwards compatibility with older versions of your service. When you declare that a newer version of your service is compatible with previous versions, it gains the ability to receive messages which were sent to an older version. This way clients who are sending messages to an older version of your service, will get a valid response, even if there are no instances of the old service running. For example, if we declare a new version of our service to be version == 3, but we also declare that it is compatible with versions 2 & 1, then any message which is sent to version 1, 2, or 3 of your service will be routed to version 3 if there is no running instance of your service in the specified version. To declare your service as compatible with other versions we add a comaptibleVersions array to the service info object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var apiInfo = {
serviceType: 'avid.acs.calculator',
serviceRealm: 'global',
serviceVersion: 3,
description: "Calculator",
compatibleVersions: [2, 1], // <- Service is now compatible with requests to version 2 and 1
ops: {
add: {
description: "Adds 2 values",
examples: {
'Add': {
paramSet: {
num1: 5,
num2: 3
}
}
},
rest: {
path: "calculator/add",
queryParams: ["num1", "num2"],
method: "GET"
}
},
divide: {
description: "Divides 2 values",
examples: {
'Divide': {
paramSet: {
num1: 12,
num2: 3
}
},
'Divide By Zero': {
paramSet: {
num1: 3,
num2: 0
}
}
}
}
}
}

Using the Avid Connector API for Channels

The Avid Connector API features channels, which are analogous to JMS Topics. When a message is posted to a channel, it is broadcast as a one-way communication to all the subscribers listening to that channel. It is important to note that channel messages are not persisted, and if you post to a channel that has no subscribers, it will not result in an error.

Channels are identified by their name. Subscribers interested in listening on a channel must either receive the channel name from the service that owns the channel, or use a predefined or well-known channel name.

Channel Messages

Like service messages, channel messages are simple JavaScript objects:

1
2
3
4
5
6
7
8
9
10
11
12
{
"channel": "com.avid.upload.notifications",
"subject": "status.735109a8-08c7-4f06-8422-7f596842671b",
"dataSet": {
"jobId": "735109a8-08c7-4f06-8422-7f596842671b",
"status": "in progress",
"details": {
"total": 87934895,
"completed": 67867833
}
}
}

Posting to a Channel

The access object provides a simple method for posting to a channel. There is no support for a callback.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var m = {
channel: 'com.avid.upload.notifications',
subject: 'status.735109a8-08c7-4f06-8422-7f596842671b',
dataSet: {
jobId: '735109a8-08c7-4f06-8422-7f596842671b',
status: 'in progress',
details: {
total: 87934895,
completed: 67867833
}
}
}

var callback = function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully post to channel");
}
};

access.postToChannel(m, callback);

NOTE: Once a channel is registered, anyone who knows the channel name can post to it. Although the poster is most often the creator of the channel, this is not enforced in any way. The security model associated with channel publishing/consuming is subject to revision.

Subscribing to a Channel

Clients can subscribe to channels by channel name and bindings. In the following example, the catch-all # binding is used, so the onChannelMessage function receives every message posted to the channel. When subscribing to a channel you must pass in a onChannelMessage callback. This callback will be called with one parameter which is an instance of a ChannelContext object. The ChannelContext provides users with the ability to forward the incoming messages’ transaction information to any future callers. This is achieved by calling the ChannelContext.getBusAccess() which will allow users to call any normal access method, with the transaction information wrapped into the outgoing message. If users are calling other services or posting to channels during their onChannelMessage callback it is prefered that they use this contextualized access object for tracking messages throughout the Avid Platform. Callback method can be used to check whether subscribed successfully or not:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const callback = function(err, subscriberId) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully subscribed to channel with subscriber id: " + subscriberId);
}
};

const onChannelMessage = function (channelContext) {

const contextBusAccess = channelContext.getBusAccess();

contextBusAccess.postToChannel({
channel : 'message.received',
subject : 'test',
dataSet : {
errors : false
}
})
};

access.subscribeToChannel('com.avid.upload.notifications', '#', onChannelMessage, callback);

Clients can also subscribe to shared channels by shared name, channel name and bindings. In this case, when multiple instances are subscribed to the same channel with same shared name, only one instance will receive each message. You can also pass callback method, to check whether subscribed successfully or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const callback = function(err, subscriberId) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully subscribed to channel with subscriber id: " + subscriberId);
}
};

const onChannelMessage = function (channelContext) {
console.log('Received channel message: ', channelContext.getChannelMessage);
};

access.subscribeToSharedChannel('com.avid.upload.notifications', '#', 'shared.name', onChannelMessage, callback);

Using Bindings

Bindings can be used to filter which messages are sent to subscribers. The binding string will filter messages based on their dot-separated subject.

The * wildcard character can be used to match whole words only. For example, the binding status.* matches the subject status.12345, but the binding status.123* does not.

The # wildcard character can be used to match zero or more entire words. For example, the binding status.# matches both the subject status.12345 and status.12345.complete.

The Avid Connector API allows clients to subscribe to the same channel using multiple bindings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const channel = 'com.avid.upload.notifications';
const bindings = ['status.*', 'completion.*', 'alert'];

const callback = function(err, subscriberId) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully subscribed to channel with subscriber id: " + subscriberId);
}
};

const subscriber = function (channelContext) {
console.error('Received channel message: ', channelContext.getChannelMessage());
}

access.subscribeToChannel(channel, bindings, subscriber, callback);

Unsubscribing from a Channel

Clients can unsubscribe from channels, they subscribed earlier by channel name and bindings. In the following example, the catch-all # binding is used. Callback method can be used to check whether unsubscribed successfully or not:

1
2
3
4
5
6
7
8
9
10
var callback = function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully unsubscribed from channel");
}
};

access.unsubscribeFromChannel('com.avid.upload.notifications', '#', callback);

NOTE: This capability is not secured in any way. Anyone who knows the channel name is able to shut it down.

Unsubscribing from a Channel

Clients also have the ability to unsubscribe from specific bindings on a given channel. For example we have a client listening on the channel ‘event.123’ with the bindings ‘start-event’, ‘ping-event’ and ‘done-event’. The client now only wants to know when events are starting but doesn’t need and update on the event, or to know when its done. We can accopmlish this by unsubscribing from those sepcific bindings.

1
2
3
4
5
6
7
8
9
10
11
12
const bindings = ['ping-event', 'done-event'];

const callback = function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully unsubscribed from bindings %s", bindings);
}
};

access.unsubscribeFromBindings('event.123', bindings, callback);

Remote Zone and Multi-Zone Channel Communications

The default behavior of the Avid Connector API is to scope channel communications within the local zone. All the examples given above use this default behavior. If the local zone has been initialized in a multi-zone environment, however, it is possible to communicate using channels across multiple zones.

Interacting with Channel Subscribers in a Specific Remote Zone

To communicate with channel subscribers in a specific remote zone, use the access.zone(String zoneID) object. The following example sends channel events and messages to subscribers in a specific zone only:

1
2
3
4
5
6
7
8
9
10
var callback = function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully post to channel");
}
};

access.zone("5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c").postToChannel(msg, callback);

In the above case, only multi-zone subscribers in the remote zone with ID 5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c receive the channel messages. If there are local scope subscribers listening on the same channel in that zone, they do not receive the messages (since they only receive messages sourced from their local zone).

Interacting with Channel Subscribers in All Zones

To communicate with channel subscribers in all connected zones, use the access.multiZone() object. The following is an example of multi-zone channel communication:

1
2
3
4
5
6
7
8
9
10
var callback = function(err) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully post to channel");
}
};

access.multiZone().postToChannel(msg, callback);

In the above case, all multi-zone subscribers across all connected zones receive the channel messages. The local scope subscribers in the poster’s zone also receive the messages. The local scope subscribers in remote zones, however, do not receive the messages.

Subscribing to Multi-Zone Channels

If the local zone has been initialized in a multi-zone environment, channel subscribers can listen using the multi-zone scope. This means that they can receive channel messages from posters in remote zones.

To subscribe to a channel in the multi-zone scope, use the access.multiZone() object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var callback = function(err, subscriberId) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully subscribed to channel with subscriber id: " + subscriberId);
}
};

const onChannelMessage = function(channelContext) {
// received a message
});

access.multiZone().subscribeToChannel(channel, bindings, onChannelMessage, callback);

Local Zone Communications

Note that there are also channel methods in the access.localZone() object. Invoking channel methods on this object is functionally equivalent to invoking the same methods on the base access object.

Logging

Although not shown here, the bal.Service exposes a method meant for use in logging. The Avid Connector API logging system uses the popular Winston library. For specifics on the logging API, please see the Winston docs.

The following is an example of how the divide operation could be modified to log errors and results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CalculatorService.prototype.divide = function(m, operationContext) {
var self = this;

self.logger.debug("Someone wants to divide %d by %d", m.paramSet.num1, m.paramSet.num2, {});

if (0 === parseInt(m.paramSet.num2)) {
self.logger.error("Divide by zero!");

operationContext.error({ code: "BAD_REQUEST", details: 'Cannot divide by zero' });
} else {
var answer = parseInt(m.paramSet.num1) / parseInt(m.paramSet.num2);

self.logger.info("%d / %d = %d", m.paramSet.num1, m.paramSet.num2, answer, {});

operationContext.reply({
resultSet: {
quotient: answer
}
});
}
}

Using another logger

Although the default logger is sufficient for some uses, some service writers may wish to use their own logger in its place.
To do so you must create a proxy object which implements the methods, info/warn/error/debug and trace. The logger in the bal
will only call these methods and therefore you can drop in your own replacement logger in its place. The logger can be replaced
as soon as you import the proxybal into your code.

1
2
3
4
5
6
7
8
9
10
11
12
13
const MyProxy = {
info: console.log,
warn: my-logger.warn,
error: console.err,
debug: my-logger.debug,
trace: (msg) => {
my-logger.emit('trace', msg);
}
}

// Then you can reset the internal logger to your proxy object
bal.resetLogger(MyProxy);

Accessing Constants

The Avid Connector API provides constants, which represent core Avid Platform service types. To access these constants:

1
2
3
4
5
6
var constants     = require('proxy-bal/lib/constants');

constants.ATTRIBUTES_SERVICE; // serviceType of the Attributes service
constants.FEDERATION_SERVICE; // serviceType of the Federation service
constants.INFRASTRUCTURE_SERVICE; //serviceType of the Infrastructure service
constants.REGISTRY_SERVICE; // serviceType of the Registry service

Exposing Operations for Upstream REST requests

To declare an operation supporting a REST request, add the corresponding JSON object as part of the service operation definition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
serviceType : 'avid.example.calculator',
serviceRealm : 'global',
serviceVersion : 1,
description : "Performs simple arithmetic.",
ops : {
add : {
description : "Returns the sum of two numbers",
examples : {
'add' : {num1 : 4, num2: 5}
},
rest : {
path : 'example/service/add',
queryParams: ['num1' , 'num2'],
method : 'GET',
resultParam : 'sum'
}
}
}
}

Paths can also have templates that match with a parameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
serviceType : 'avid.example.calculator',
serviceRealm : 'global',
serviceVersion : 1,
description : "Performs simple arithmetic.",
ops : {
add : {
description : "Returns the sum of an id and a number",
examples : {
'add' : {id : 33, num1 : 4}
},
rest : {
path : 'example/{id}/add',
queryParams: ['num1'],
method : 'GET',
resultParam : 'idPlusSum'
}
}
}
}

The incoming service message will have the id parameter populated with the value passed in the path of the REST request.

You can also have wildcarded paths, that can be used to match some unknown path which will be spcified by the user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
serviceType : 'avid.example.calculator',
serviceRealm : 'global',
serviceVersion : 1,
description : "Performs simple arithmetic.",
ops : {
add : {
description : "Concatenates some / delimited path",
examples : {
'add' : {path: /my/stuff/is/here}
},
rest : {
path : 'example/{path*}',
method : 'GET',
resultParam : 'concatPath'
}
}
}
}

In this case a user could pass the paths

1
2
/example/path/my/thing
/example/path/something/else

And the incoming message will have a parameter path which will be set to the path (from the start of the wildcard) value.

The REST object has the options:

  • path - required;
  • queryParam - not required;
  • bodyParam - not required, default = *;
  • method - not required, default GET;

IMPORTANT: Service can not expose multiple operations or/and commands with the same HTTP method on the same endpoint.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
serviceType : 'avid.example.service',
serviceRealm : 'global',
serviceVersion : 1,
description : "Simple service example.",
ops : {
myOperation : {
rest : {
path : 'myPath',
method : 'POST'
}
}
},
commands: {
myCommand: {
rest: {
path: 'myPath',
method: 'POST'
}
}
}
}

Example above is invalid, because operation and command are both exposed using the same path and method. During service registration
Avid Connector API will detect conflicting REST endpoints, if any, it will throw DuplicateRestEndpointsError.

For more detailed information about how REST requests are mapped and delivered to your service,
please view the upstream HTTP docs.

Liveness probe

The Avid Connector API has built in functionality to collect and display liveness status of registered BusAccess instances. As well as display these instances together with registered services.
This functionality is available through the endpoints exposed by internally started HTTP server.
Liveness status functionality is disabled by default and can be enabled with environmental variable. Refer to environmental variables list.

The server instance is Singleton, and is configured and started at the first invocation of BusAccess.connect method.
All other connections will use the exactly the same server instance created during the first connection.

Liveness probe server uses only ConnectionOptions and environmental variables for its configuration.

Default liveness probe implementation

The default implementation of Liveness Probe server is available and works out of the box when liveness probe functionality is enabled via environmental variables.
Default bind address is 0.0.0.0 and port 9991.
Default endpoints server exposes :

  • /api/v1/liveness (GET) - returns aggregated health of all registered services and clients
  • /api/v1/instances (GET) - returns list of all registered services and clients
  • /api/v1/instances/{id}/liveness (GET) - returns health of specific instance

/api/v1/liveness Returns text/plain with Status code 200/500 and text ok/error indicating that aggregated status of all instances is healthy or not correspondingly

/api/v1/instances/{id}/liveness Returns text/plain with Status code 200/500 and text ok/error indicating that BusAccess instance is healthy or not correspondingly

  • id - instance id of the BusAccess instance

/api/v1/instances Returns application/json containing description of all registered BusAccess instances and services assigned to them

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

{
"fee67ab5-2504-4e50-8ab8-f534ee86c4fb": {
"clientId": "N/A",
"livenessCode": 200,
"serviceType": "avid.platform.example.liveness-test",
"serviceRealm": "global",
"serviceVersion": 3
},
"c78c1576-2ba4-41af-9b11-a21434549e93": {
"clientId": "123-456-789",
"livenessCode": 500,
"serviceType": "avid.platform.example.liveness-test",
"serviceRealm": "global",
"serviceVersion": 3
},
"7a9e24ad-a71e-4f91-84d0-9ab95479ed03": {
"clientId": "222-456-789",
"livenessCode": 200,
"serviceType": "avid.platform.example.liveness-test",
"serviceRealm": "global",
"serviceVersion": 3
}
}
```

Where clientId is clientId used in authentication. If client authenticated by IP, not by client ID, then clientId is "N/A".

__call to any other endpoint__ Returns _text/plain_ with Status code 404 and text "404 Not Found"

## Settings for build-in HTTP server
The Avid Connector API provides a possibility to define http server host and port not only via environment.
It is possible to add __http__ section to the __liveness__ settings with new parameters __host__ and __port__.

```javascript
const bal = require('proxy-bal');

const options = {
liveness: {
http: {
host: '0.0.0.0',
port: 8888
}
}
}
const access = bal.createAccess(options);
access.connect();

Custom liveness probe implementation

Custom InstanceLivenessProvider and AggregatedLivenessProvider

The Avid Connector API provides a possibility to re-define AggregatedLivenessProvider and InstanceLivenessProvider, which the server will use to report liveness statuses.

AggregatedLivenessProvider

Synchronous provider.

This provider must return liveness code.

1
2
3
4
5
6
7
8
9
const bal = require('proxy-bal');

const options = {
liveness: {
aggregatedLivenessProvider: livenessStatuses => livenessStatuses[0] || 200;
}
}
const access = bal.createAccess(options);
access.connect();
Asynchronous provider.

This provider must return object Promise. Liveness code must be returned by Promise.resolve.

1
2
3
4
5
6
7
8
9
10
11
12
const bal = require('proxy-bal');

const options = {
liveness: {
aggregatedLivenessProvider: livenessStatuses =>
new Promise(
resolve => setTimeout(_ => resolve(livenessStatuses[0] || 200), 3000)
);
}
}
const access = bal.createAccess(options);
access.connect();

InstanceLivenessProvider

Synchronous provider.

This provider must return liveness code.

1
2
3
4
5
6
7
8
9
const bal = require('proxy-bal');

const options = {
liveness: {
instanceLivenessProvider: instanceId => process.hrtime()[1] & 1 ? 200 : 500
}
}
const access = bal.createAccess(options);
access.connect();
Asynchronous provider.

This provider must return object Promise. Liveness code must be returned by Promise.resolve.

1
2
3
4
5
6
7
8
9
10
11
12
13
const bal = require('proxy-bal');

const options = {
liveness: {
instanceLivenessProvider: instaceId =>
new Promise(
resolve => setTimeout(_ => resolve(process.hrtime()[1] & 1 ? 200 : 500), 3000)
)
}
}
}
const access = bal.createAccess(options);
access.connect();

Custom BusAccessWebServer

The Avid Connector API allows to provide your own implementation of HTTP liveness server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const bal = require('proxy-bal');

const options = {
liveness: {
busAccessWebServer: {
startServer: _ => console.log('startServer'),
stopServer: _ => console.log('stopServer'),
registerInstance: (instanceId, instanceLivenessProvider) => console.log(`registerInstance instanceId = ${instanceId}`),
unregisterInstance: instanceId => console.log(`unregisterInstance instanceId = ${instanceId}`),
registerService: (instanceId, serviceId, signature) => console.log(`registerService instanceId = ${instanceId}, serviceId= ${serviceId}`),
unregisterService: (instanceId, serviceId) => console.log(`unregisterService instanceId = ${instanceId}, serviceId= ${serviceId}`),
registerAggregatedLivenessProvider: instanceId => 200,
registerInstanceLivenessProvider: livenessStatuses => 200
}
}
}
const access = bal.createAccess(options);
access.connect();

In this case implementation is entirely up to you, as well as thirdparty libs used for it.
The Avid Connector API though will still wrap your server as Singleton, startServer() on first connect and stopServer() when no active connections left.

It will still call (providing the default implementation for AggregatedLivenessProvider and InstanceLivenessProvider if no custom is available)

  • server.registerAggregatedLivenessProvider(AggregatedLivenessProvider aggregatedLivenessProvider) for the first call to not deprecated BusAccess.connect
  • server.registerInstance(String instanceId, InstanceLivenessProvider instanceLivenessProvider) for each call to not deprecated BusAccess.connect,
  • server.unregisterInstance(String instanceId) for each call to not deprecated BusAccess.disconnect
  • server.registerService(String instanceId, String serviceId, ServiceSignature signature) for each call to not deprecated BusAccess.registerService
  • server.unregisterService(String instanceId, String serviceId) for each call to not deprecated BusAccess.unregisterService

Use this design doc if you need more information on how to implement server instance.

Error Handling

A service can fail an operation by calling `operationContext.error(…)’. The error function accepts either an object or an array of objects. The error object can have following fields:

  • code - (string/required) This should match a code defined in the service declaration.
  • params - (object/optional) A flat map of values can be used for substituation of messageTemplate Defined in the service declaration.
  • details - (string/optional) A message indicating the error.

Migration Guides

This migration guide is valid for migrating to Avid Connector API version >= 3.8.

Migration to Asynchronous API

Service operations migration to OperationContext

Your service operation might be declared similarly to the following example:

1
2
3
CalculatorService.prototype.add = function(m, responder) {
responder.reply({ sum: m.paramSet.num1 + m.paramSet.num2 });
};

Now, operationContext took place of responder. So service operations looks like:

1
2
3
CalculatorService.prototype.add = function(m, operationContext) {
operationContext.reply({ sum: m.paramSet.num1 + m.paramSet.num2 });
};

NOTE: The following rules must be used

  • to access incoming messages either m argument or operationContext.getMessage() can be used
  • to get instance of BusAccess and perform nested calls with BusAccess use operationContext.getBusAccess()
  • to reply back with result of given type use context.reply()
  • to reply back with error use error()
  • do not use BusAccess instance you have created for the nested calls. Always use BusAccess from context for the nested calls

Channel subscription migration to ChannelMessageHandler

To handle incoming channel messages, instead of two arguments subscriber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var subscriber = function (err, m) {
if (! err) {
console.error('Received channel message: ', m);
}
};
````

one argument callback must be used, with `channelContext` argument:

```javascript
const onChannelMessage = function (channelContext) {
// Access to incoming message
channelContext.getChannelMessage();

// Access to BusAccess for nested calls
const contextBusAccess = channelContext.getBusAccess();

contextBusAccess.postToChannel({
channel : 'message.received',
subject : 'test',
dataSet : {
errors : false
}
})
};

const callback = function(err, subscriberId) {
if (err) {
console.err("Error message: " + err.errorMessage);
console.err("Error type: " + err.errorType);
} else {
console.log("Successfully subscribed to channel with subscriber id: " + subscriberId);
}
};

access.subscribeToChannel('com.avid.upload.notifications', '#', onChannelMessage, callback);

NOTE: The following rules must be used

  • to access incoming messages use channelContext.getChannelMessage()
  • to get instance of BusAccess and perform nested calls with BusAccess use channelContext.getBusAccess()
  • do not use BusAccess instance you have created for the nested calls. Always use BusAccess from context for the nested calls