mirror of
https://github.com/jambonz/sbc-inbound.git
synced 2026-10-04 02:04:22 +00:00
Compare commits
16
Commits
v0.9.6-rc1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0d8ba93b2 | ||
|
|
3f729e232e | ||
|
|
509fdf6c37 | ||
|
|
b7b707cc2e | ||
|
|
ccbebc015f | ||
|
|
320a30e328 | ||
|
|
cd2fde360d | ||
|
|
e353b751c0 | ||
|
|
53b46f7e51 | ||
|
|
6608286e8c | ||
|
|
6ddfbc9373 | ||
|
|
cd48675499 | ||
|
|
cecdbdccef | ||
|
|
6586919c86 | ||
|
|
c919438af5 | ||
|
|
ab39525467 |
@@ -155,6 +155,17 @@ srf.locals = {
|
||||
};
|
||||
const activeCallIds = srf.locals.activeCallIds;
|
||||
|
||||
/* report our call count to redis so a draining process can count calls across
|
||||
all sbc-inbound and sbc-outbound processes on this server */
|
||||
if (!process.env.K8S && 'test' !== process.env.NODE_ENV) {
|
||||
srf.locals.callCountReporter = require('./lib/call-count-reporter')({
|
||||
logger,
|
||||
addKey,
|
||||
addToSet,
|
||||
getCount: () => activeCallIds.size
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
initLocals,
|
||||
handleSipRec,
|
||||
@@ -270,7 +281,7 @@ srf.invite((req, res) => {
|
||||
session.connect();
|
||||
});
|
||||
|
||||
srf.use((req, res, next, err) => {
|
||||
srf.use((err, req, res, next) => {
|
||||
logger.error(err, 'hit top-level error handler');
|
||||
res.send(500);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"ICE": "default",
|
||||
"SDES": "off",
|
||||
"flags": ["generate mid", "SDES-no", "port latching"],
|
||||
"rtcp-mux": ["require"]
|
||||
"rtcp-mux": ["offer"]
|
||||
},
|
||||
"teams": {
|
||||
"transport-protocol": "RTP/SAVP",
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ module.exports = [
|
||||
// Variables
|
||||
'no-delete-var': 2,
|
||||
'no-undef': 2,
|
||||
'no-unused-vars': [2, {args: 'none'}],
|
||||
'no-unused-vars': [2, {args: 'none', ignoreRestSiblings: true}],
|
||||
|
||||
// Node.js and CommonJS
|
||||
'no-mixed-requires': 2,
|
||||
|
||||
@@ -23,23 +23,55 @@ module.exports = (logger) => {
|
||||
const {srf} = require('..');
|
||||
const {activeCallIds, removeFromRedis} = srf.locals;
|
||||
|
||||
/* reject new INVITEs with 503 so senders fail over to another SBC */
|
||||
srf.locals.dryUpCalls = true;
|
||||
|
||||
/* remove our private IP from the set of active SBCs so rtp and fs know we are gone */
|
||||
removeFromRedis();
|
||||
|
||||
/* if we have zero calls, we can complete the scale-in right now */
|
||||
const calls = activeCallIds.size;
|
||||
if (0 === calls) {
|
||||
logger.info('scale-in can complete immediately as we have no calls in progress');
|
||||
lifecycleEmitter.completeScaleIn();
|
||||
}
|
||||
else {
|
||||
logger.info(`${calls} calls in progress; scale-in will complete when they are done`);
|
||||
}
|
||||
/* count calls in progress across all sbc-inbound and sbc-outbound
|
||||
processes on this server, if they are reporting; otherwise
|
||||
fall back to counting only our own */
|
||||
const countServerCalls = async() => {
|
||||
const reporter = srf.locals.callCountReporter;
|
||||
if (!reporter) return activeCallIds.size;
|
||||
const {retrieveSet, retrieveKey} = srf.locals.realtimeDbHelpers;
|
||||
const keys = await retrieveSet(reporter.setName);
|
||||
let count = 0;
|
||||
for (const key of keys) {
|
||||
count += parseInt(await retrieveKey(key), 10) || 0;
|
||||
}
|
||||
return Math.max(count, activeCallIds.size);
|
||||
};
|
||||
|
||||
/* poll until calls have dried up, then complete the scale-in;
|
||||
require two consecutive zero readings since reported counts
|
||||
may be up to 15s stale */
|
||||
let consecutiveZeroCounts = 0;
|
||||
const timer = setInterval(async() => {
|
||||
try {
|
||||
const calls = await countServerCalls();
|
||||
if (0 === calls) {
|
||||
if (++consecutiveZeroCounts >= 2) {
|
||||
clearInterval(timer);
|
||||
logger.info('scale-in complete now that calls have dried up');
|
||||
lifecycleEmitter.completeScaleIn();
|
||||
}
|
||||
}
|
||||
else {
|
||||
consecutiveZeroCounts = 0;
|
||||
logger.info(`${calls} calls in progress on this server; scale-in will complete when they are done`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error counting calls in progress during scale-in');
|
||||
}
|
||||
}, 20000);
|
||||
})
|
||||
.on(LifeCycleEvents.StandbyEnter, () => {
|
||||
lifecycleEmitter.dryUpCalls = true;
|
||||
const {srf} = require('..');
|
||||
const {removeFromRedis} = srf.locals;
|
||||
srf.locals.dryUpCalls = true;
|
||||
removeFromRedis();
|
||||
|
||||
logger.info('AWS enter pending state notification: begin drying up calls');
|
||||
@@ -48,6 +80,7 @@ module.exports = (logger) => {
|
||||
lifecycleEmitter.dryUpCalls = false;
|
||||
const {srf} = require('..');
|
||||
const {addToRedis} = srf.locals;
|
||||
srf.locals.dryUpCalls = false;
|
||||
addToRedis();
|
||||
|
||||
logger.info('AWS exit pending state notification: re-enable calls');
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const os = require('os');
|
||||
|
||||
/**
|
||||
* Periodically report this process's count of calls in progress to redis.
|
||||
* A server may host several sbc-inbound and sbc-outbound processes; when one
|
||||
* of them handles an autoscale drain it needs to know when the entire server
|
||||
* has no calls in progress, not just its own process. Each process writes
|
||||
* its own count under a per-pid key (with a short expiry, so keys from dead
|
||||
* processes evaporate) and registers that key in a per-host set that the
|
||||
* draining process can enumerate.
|
||||
*/
|
||||
const REPORT_INTERVAL = 15000;
|
||||
const KEY_EXPIRY_SECS = 120;
|
||||
|
||||
module.exports = ({logger, addKey, addToSet, getCount}) => {
|
||||
const prefix = process.env.JAMBONES_CLUSTER_ID || 'default';
|
||||
const setName = `${prefix}:call-count-keys:${os.hostname()}`;
|
||||
const key = `${prefix}:call-count:${os.hostname()}:${process.pid}`;
|
||||
|
||||
const report = () => {
|
||||
addKey(key, `${getCount()}`, KEY_EXPIRY_SECS)
|
||||
.catch((err) => logger.error({err}, 'call-count-reporter: error writing call count'));
|
||||
};
|
||||
|
||||
addToSet(setName, key)
|
||||
.catch((err) => logger.error({err}, `call-count-reporter: error adding ${key} to ${setName}`));
|
||||
setInterval(report, REPORT_INTERVAL);
|
||||
report();
|
||||
|
||||
return {key, setName};
|
||||
};
|
||||
+6
-1
@@ -109,7 +109,11 @@ class CallSession extends Emitter {
|
||||
|
||||
async connect() {
|
||||
const {sdp} = this.req.locals;
|
||||
const is3pcc = this.req.body?.length === 0;
|
||||
// use the parsed SDP from middleware (req.locals.sdp), not req.body:
|
||||
// for multipart (SIPREC) or IWF-originated bodies req.body may be empty
|
||||
// or not represent the actual offer, which would wrongly send an
|
||||
// SDP-bearing call down the no-offer 3pcc path.
|
||||
const is3pcc = !sdp || sdp.length === 0;
|
||||
this.logger.info(`inbound ${is3pcc ? '3pcc ' : ''}call accepted for routing`);
|
||||
const engine = this.getRtpEngine();
|
||||
if (!engine) {
|
||||
@@ -1105,6 +1109,7 @@ Duration=${payload.duration} `
|
||||
this.rtpEngineResource.destroy();
|
||||
this.activeCallIds.delete(this.req.get('Call-ID'));
|
||||
uac.other.destroy();
|
||||
this._stopRecording();
|
||||
this.srf.endSession(this.req);
|
||||
});
|
||||
|
||||
|
||||
+14
-4
@@ -394,8 +394,13 @@ module.exports = (srf, logger) => {
|
||||
}
|
||||
}
|
||||
if (r.length > 1) {
|
||||
logger.info({r},
|
||||
'multiple carriers with the same gateway have the same number provisioned for the same account'
|
||||
const all = r.map(({account_sid, voip_carrier_sid}) => ({account_sid, voip_carrier_sid}));
|
||||
logger.info({
|
||||
number: r[0].number,
|
||||
total: all.length,
|
||||
matches: all.slice(0, 5)
|
||||
},
|
||||
'multiple carriers with the same gateway have the same number provisioned for the same account'
|
||||
+ ' -- cannot determine which one to use');
|
||||
return {
|
||||
fromCarrier: true,
|
||||
@@ -601,8 +606,13 @@ module.exports = (srf, logger) => {
|
||||
}
|
||||
}
|
||||
else if (r.length > 1) {
|
||||
logger.info({r},
|
||||
'multiple accounts have added this carrier with default routing -- cannot determine which to use');
|
||||
const all = r.map(({account_sid, voip_carrier_sid}) => ({account_sid, voip_carrier_sid}));
|
||||
logger.info({
|
||||
number: r[0].number,
|
||||
total: all.length,
|
||||
matches: all.slice(0, 5)
|
||||
},
|
||||
'multiple accounts have added this carrier with default routing -- cannot determine which to use');
|
||||
return {
|
||||
fromCarrier: true,
|
||||
error: 'Multiple accounts are attempting to route the same phone number from the same carrier'
|
||||
|
||||
+11
-1
@@ -34,6 +34,15 @@ module.exports = function(srf, logger) {
|
||||
|
||||
const initLocals = (req, res, next) => {
|
||||
const callId = req.get('Call-ID');
|
||||
|
||||
/* if we are drying up calls prior to scale-in, reject new INVITEs so the
|
||||
sender fails over to another SBC; allow INVITE with Replaces through
|
||||
since it targets a call already in progress on this server */
|
||||
if (srf.locals.dryUpCalls && !req.has('Replaces')) {
|
||||
logger.info({callId}, 'rejecting INVITE with 503 as we are drying up calls before scale-in');
|
||||
return res.send(503);
|
||||
}
|
||||
|
||||
req.locals = req.locals || {callId};
|
||||
req.locals.nudge = 0;
|
||||
|
||||
@@ -130,7 +139,8 @@ module.exports = function(srf, logger) {
|
||||
logger.info('identifyAccount: rejecting call from carrier because DID has not been provisioned');
|
||||
return res.send(404, 'Number Not Provisioned');
|
||||
}
|
||||
logger.info({gateway}, 'identifyAccount: incoming call from gateway');
|
||||
const {register_password, ...gatewayForLog} = gateway;
|
||||
logger.info({gateway: gatewayForLog}, 'identifyAccount: incoming call from gateway');
|
||||
const appSidHeader = req.get('x-application-sid');
|
||||
if (appSidHeader && appSidHeader == application_sid) {
|
||||
logger.info({callId}, 'Loop Detected, x-application-sid header on incoming call matches applicationSid');
|
||||
|
||||
Generated
+318
-294
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sbc-inbound",
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.11",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 20.0.0"
|
||||
@@ -32,7 +32,7 @@
|
||||
"@jambonz/db-helpers": "^0.9.18",
|
||||
"@jambonz/digest-utils": "^0.0.9",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.18",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.21",
|
||||
"@jambonz/rtpengine-utils": "^0.4.4",
|
||||
"@jambonz/siprec-client-utils": "^0.2.10",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
@@ -41,7 +41,7 @@
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.4.3",
|
||||
"drachtio-fn-b2b-sugar": "0.2.1",
|
||||
"drachtio-srf": "^5.0.17",
|
||||
"drachtio-srf": "^5.0.27",
|
||||
"express": "^4.21.2",
|
||||
"pino": "^10.1.0",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
|
||||
Reference in New Issue
Block a user