mirror of
https://github.com/jambonz/sbc-sip-sidecar.git
synced 2026-10-04 02:04:20 +00:00
* fix: expire FS/RTP roster members on shared last-seen, not one SBC's local view The active-fs, fs-service-url and active-rtp redis sets are shared by every SBC in the cluster, but the expiry sweep in lib/options.js removed members based solely on when THIS sidecar last received an OPTIONS ping from them. An SBC that was taken out of active-sip (so FS/RTP servers stopped pinging it) therefore deleted every feature server and rtpengine from the shared rosters 60s later, rejecting all inbound calls until the other SBC re-added them on its next ping cycle. Each SBC now records the last ping it received per member in a shared redis hash (<setName>:lastseen, member -> epoch ms) and the sweep removes a member only when that shared timestamp is older than EXPIRES_INTERVAL. Members with no shared timestamp are never expired by the sweep, so a parked or mixed-version SBC cannot remove members the others are still hearing from. Adds a unit test that reproduces the failure against the old code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: make sbc_addresses keepalive recurring instead of a single setTimeout addSbcAddress() refreshes the row's last_updated and cleanSbcAddresses() deletes rows older than DEAD_SBC_IN_SECOND (default 3600s), but app.js only re-called addSbcAddress once, 15 minutes after connecting. The row then went stale, and the next sidecar to start anywhere in the cluster deleted the healthy SBC's row, so new sip realms were provisioned with one SBC IP instead of two. Run one recurring timer (SBC_PUBLIC_ADDRESS_KEEP_ALIVE_IN_MILISECOND, default 15 min) that refreshes this SBC's rows and only then reaps stale ones, so the cleaner never runs ahead of this process's own keepalive. The timer is unref'd and replaced (not stacked) on drachtio reconnect. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
65 lines
2.5 KiB
JavaScript
65 lines
2.5 KiB
JavaScript
const test = require('tape');
|
|
const clearModule = require('clear-module');
|
|
|
|
/* Unit test for the expiry sweep in lib/options.js.
|
|
The active-fs / fs-service-url / active-rtp sets are shared by every SBC in the cluster,
|
|
so a member must only be expired when the SHARED last-seen timestamp is stale, never
|
|
because this particular SBC has stopped receiving pings. */
|
|
|
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
test('options: expiry sweep uses shared last-seen, not local view', async(t) => {
|
|
clearModule.all();
|
|
process.env.EXPIRES_INTERVAL = '1000';
|
|
process.env.CHECK_EXPIRES_INTERVAL = '300';
|
|
|
|
const logger = require('pino')({level: 'silent'});
|
|
const rdb = require('@jambonz/realtimedb-helpers')({}, logger);
|
|
const {client, addToSet, removeFromSet, isMemberOfSet, retrieveSet} = rdb;
|
|
const stats = {gauge: () => {}};
|
|
const srf = {locals: {stats, addToSet, removeFromSet, isMemberOfSet, retrieveSet, realtimeDbHelpers: {client}}};
|
|
|
|
const setName = 'default:active-fs';
|
|
const lastSeen = `${setName}:lastseen`;
|
|
const fresh = '10.0.0.1:5060'; // pinged another SBC recently
|
|
const stale = '10.0.0.2:5060'; // nobody has heard from it
|
|
const legacy = '10.0.0.3:5060'; // in the set with no shared timestamp
|
|
|
|
const cleanup = async() => {
|
|
await client.del(setName, lastSeen);
|
|
};
|
|
|
|
try {
|
|
await cleanup();
|
|
await client.sadd(setName, fresh, stale, legacy);
|
|
await client.hset(lastSeen, stale, Date.now() - 5000);
|
|
|
|
/* start the options handler: this SBC has never been pinged by any of these members */
|
|
require('../lib/options')({srf, logger});
|
|
|
|
/* simulate another SBC continuing to hear from `fresh` */
|
|
const refresher = setInterval(() => client.hset(lastSeen, fresh, Date.now()), 200);
|
|
|
|
await wait(1500);
|
|
clearInterval(refresher);
|
|
|
|
const members = (await retrieveSet(setName)).sort();
|
|
t.ok(members.includes(fresh), 'member still being pinged by another SBC is kept');
|
|
t.notOk(members.includes(stale), 'member nobody has heard from within EXPIRES_INTERVAL is expired');
|
|
t.ok(members.includes(legacy), 'member with no shared last-seen is left alone');
|
|
t.equal(await client.hexists(lastSeen, stale), 0, 'expired member removed from shared last-seen hash');
|
|
|
|
await cleanup();
|
|
client.quit();
|
|
t.end();
|
|
} catch (err) {
|
|
await cleanup();
|
|
client.quit();
|
|
t.end(err);
|
|
} finally {
|
|
delete process.env.EXPIRES_INTERVAL;
|
|
delete process.env.CHECK_EXPIRES_INTERVAL;
|
|
clearModule.all();
|
|
}
|
|
});
|