seneca-balance-client-ng
Version:
Seneca client-side load balancing transport.
399 lines (328 loc) • 9.99 kB
JavaScript
/* MIT License. Copyright (c) 2015-2018, Richard Rodger and other contributors. */
;
const _ = require('lodash');
//const Eraro = require('eraro')
const Jsonic = require('jsonic');
const visigoth = require('@wzrdtales/visigoth');
// const Optioner = require('optioner')
// const Joi = Optioner.Joi
/*
const optioner = Optioner({
model: Joi.alternatives().try(Joi.string(), Joi.func()),
debug: {
client_updates: false
}
})
*/
/*
var error = Eraro({
package: 'seneca',
msgmap: {
'no-target': 'No targets have been registered for message <%=msg%>',
'no-current-target': 'No targets are currently active for message <%=msg%>'
}
})
*/
module.exports = balance_client;
balance_client.defaults = {
debug: {
client_updates: false,
},
};
balance_client.errors = {
'no-target': 'No targets have been registered for message <%=msg%>',
'no-current-target': 'No targets are currently active for message <%=msg%>',
};
// Not as bad as it looks - seneca.id is used at top level to isolate instances.
// Need this here so that preload can reference it.
const global_target_map = {};
var global_options = { debug: {} };
var circuitBreaker;
balance_client.preload = function () {
var seneca = this;
var t = seneca.options().transport || {};
t = t.balance || {};
t = t.circuitBreaker || { closingTimeout: 1000 };
circuitBreaker = t.circuitBreaker;
seneca.options({
transport: {
balance: {
makehandle: function (config) {
global_target_map[seneca.id] = global_target_map[seneca.id] || {
id: seneca.id,
};
var instance_map = global_target_map[seneca.id];
instance_map[config.pg] = instance_map[config.pg] || {
pg: config.pg,
id: Math.random(),
};
var target_map = instance_map[config.pg];
target_map.pg = config.pg;
//return function(pat, action) {
return function (actdef) {
var pat = actdef.client_pattern || actdef.pattern;
add_target(seneca, target_map, config, pat, actdef.func);
};
},
},
},
});
};
function balance_client(options) {
var seneca = this;
var tu = seneca.export('transport/utils');
var modelMap = {
observe: observeModel,
consume: consumeModel,
// legacy
publish: observeModel,
actor: consumeModel,
};
// options = optioner.check(options)
// hack to make add_target debug logging work
// to be fixed when seneca plugin handling is rewritten to not need preload
Object.assign(global_options, options);
seneca.add(
{
role: 'transport',
hook: 'client',
type: 'balance',
},
hook_client
);
seneca.add(
{
role: 'transport',
type: 'balance',
add: 'client',
},
add_client
);
seneca.add(
{
role: 'transport',
type: 'balance',
remove: 'client',
},
remove_client
);
seneca.add(
{
role: 'transport',
type: 'balance',
get: 'target-map',
},
get_client_map
);
function remove_target(target_map, pat, config) {
var action_id = config.id || seneca.util.pattern(config);
var patkey = make_patkey(seneca, pat);
var targetstate = target_map[patkey];
var found = false;
targetstate = targetstate || { index: 0, targets: [] };
target_map[patkey] = targetstate;
for (var i = 0; i < targetstate.targets.length; i++) {
if (action_id === targetstate.targets[i].id) {
break;
}
}
if (i < targetstate.targets.length) {
targetstate.visigoth.remove(targetstate.targets.length - 1);
targetstate.visigoth.lastChoosenIndex$ = -1;
targetstate.targets.splice(i, 1);
targetstate.index = 0;
found = true;
}
if (options.debug.client_updates) {
seneca.log.info('remove', patkey, action_id, found);
}
}
function add_client(msg, done) {
msg.config = msg.config || {};
if (!msg.config.pg) {
msg.config.pg = this.util.pincanon(msg.config.pin || msg.config.pins);
}
this.client(msg.config);
done();
}
function remove_client(msg, done) {
var seneca = this;
msg.config = msg.config || {};
if (!msg.config.pg) {
msg.config.pg = this.util.pincanon(msg.config.pin || msg.config.pins);
}
var instance_map = global_target_map[seneca.id] || {};
var target_map = instance_map[msg.config.pg] || {};
var pins = msg.config.pin ? [msg.config.pin] : msg.config.pins;
_.each(pins, function (pin) {
remove_target(target_map, pin, msg.config);
});
done();
}
function get_client_map(msg, done) {
var seneca = this;
var instance_map = global_target_map[seneca.id] || {};
done(null, null == msg.pg ? instance_map : instance_map[msg.pg]);
}
function hook_client(msg, clientdone) {
var seneca = this.root.delegate();
// console.log('BC', msg)
var type = msg.type;
var client_options = seneca.util.clean(_.extend({}, options[type], msg));
var pg = this.util.pincanon(client_options.pin || client_options.pins);
var instance_map = global_target_map[seneca.id] || {};
var target_map = instance_map[pg] || {};
var model = client_options.model || consumeModel;
model = _.isFunction(model) ? model : modelMap[model] || consumeModel;
// legacy transport
if (tu.make_client) {
var make_send = function (spec, topic, send_done) {
seneca.log.debug(
'client',
'send',
topic + '_res',
client_options,
seneca
);
send_done(null, function (msg, done, meta) {
var patkey = (meta || msg.meta$).pattern;
var targetstate = target_map[patkey];
if (targetstate) {
model(this, msg, targetstate, done, meta);
return;
} else return done(seneca.error('no-target', { msg: msg }));
});
};
tu.make_client(make_send, client_options, clientdone);
} else {
// console.log('BC A ', pg)
var send_msg = function (msg, reply, meta) {
msg = tu.externalize_msg(seneca, msg);
var msg_meta = meta || msg.meta$;
// console.log('BCM', this.util.clean(msg), pg, msg_meta.client_pattern, msg_meta.pattern)
// console.dir(target_map,{depth:null})
var patkey = msg_meta.client_pattern || msg_meta.pattern;
var targetstate = target_map[patkey];
if (targetstate) {
model(this, msg, targetstate, reply, meta);
return;
} else return reply(seneca.error('no-target', { msg: msg }));
};
return clientdone({
config: msg,
send: send_msg,
});
}
seneca.add('role:seneca,cmd:close', function (close_msg, done) {
var closer = this;
closer.prior(close_msg, done);
});
}
function observeModel(seneca, msg, targetstate, done, meta) {
if (0 === targetstate.targets.length) {
return done(seneca.error('no-current-target', { msg: msg }));
}
var first = true;
for (var i = 0; i < targetstate.targets.length; i++) {
var target = targetstate.targets[i];
target.action.call(
seneca,
msg,
function () {
if (first) {
done.apply(seneca, arguments);
first = false;
}
},
meta
);
}
}
function consumeModel(seneca, msg, targetstate, done, meta) {
var targets = targetstate.targets;
var trys = 0;
function try_call() {
targetstate.visigoth.choose(function (err, index, errored, stats) {
if (err || !targets[index]) {
index = targetstate.index = 0;
targetstate.index = 0;
return done(seneca.error('no-current-target', { msg: msg }));
}
if (trys > 0) {
meta.mi += trys;
meta.tx += trys;
meta.id = `${meta.mi}/${meta.tx}`;
}
targets[index].action.call(
seneca,
msg,
function (err) {
seneca.log.error('execute_err', err);
if (err) {
if (err.details.message === 'retry_later_err_overload') {
// only error on controlled overload errors
errored();
if (++trys < 3) {
return setTimeout(
() => try_call(),
circuitBreaker.closingTimeout + 100
);
}
return done(seneca.error('all-targets-overloaded', { msg }));
}
} else {
stats.responseTime = new Date() - meta.start;
}
done.apply(done, arguments);
},
meta
);
if (targets.length === 1) {
targetstate.visigoth.lastChoosenIndex$ = -1;
}
});
}
try_call();
}
}
function add_target(seneca, target_map, config, pat, action) {
var patkey = make_patkey(seneca, pat);
var targetstate = target_map[patkey];
var add = true;
targetstate = targetstate || {
index: 0,
targets: [],
visigoth: visigoth(circuitBreaker),
};
target_map[patkey] = targetstate;
// don't add duplicates
for (var i = 0; i < targetstate.targets.length; ++i) {
if (action.id === targetstate.targets[i].id) {
add = false;
break;
}
}
if (add) {
targetstate.visigoth.add(targetstate.targets.length);
targetstate.targets.push({
action: action,
id: action.id,
config: config,
});
}
if (global_options.debug.client_updates) {
seneca.log.info('add', patkey, action.id, add);
}
}
function make_patkey(seneca, pat) {
if (_.isString(pat)) {
pat = Jsonic(pat);
}
var keys = _.keys(seneca.util.clean(pat)).sort();
var cleanpat = {};
_.each(keys, function (k) {
cleanpat[k] = pat[k];
});
var patkey = seneca.util.pattern(cleanpat);
return patkey;
}