mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-10-04 02:04:12 +00:00
* fix: fire sip:refer actionHook when the far end BYEs before any NOTIFY When a REFER is accepted with 202 the verb waits for a NOTIFY carrying the final status of the referred call, and only fired the actionHook from the 15s timeout callback. If the far end sends BYE straight after the 202 the call session kills the task, awaitTaskDone() resolves, and exec returned without ever performing the action. Perform the action once from exec on every exit path - before the session tears the requestor down - and de-duplicate it against the final-NOTIFY path. Fix #1258 Generated by Ora Studio Vibe coded by ousamabenyounes Co-Authored-By: Ora Agent <noreply@oratelecom.net> * fix: keep final_referred_call_status when a BYE races the final NOTIFY (#1580) A final NOTIFY (status >= 200) whose eventHook round trip is still in flight when the far end BYEs lost final_referred_call_status on the actionHook: kill() woke exec(), which memoised the action via _performReferAction({refer_status}) without the final status, so the NOTIFY path's later call carrying it was deduped away. On main the app received final_referred_call_status here. Record the final status synchronously in _handleNotify before the eventHook await, and include it from exec() so the memoised action carries it regardless of which exit path wins the race. Extract the 200 threshold into a named constant while touching those lines. Addresses davehorton's review on #1580. Generated by Ora Studio Vibe coded by ousamabenyounes Co-Authored-By: Ora Agent <noreply@oratelecom.net> --------- Co-authored-by: Ora Agent <noreply@oratelecom.net> Co-authored-by: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com>
This commit is contained in:
co-authored by
Ora Agent
Ben Younes
parent
a4e7afac83
commit
83db903bc6
+48
-15
@@ -1,7 +1,16 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const {TaskName, TaskPreconditions, KillReason} = require('../utils/constants');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
|
||||
/* how long we wait for a NOTIFY carrying the final status of the referred call */
|
||||
const NOTIFY_TIMEOUT_MS = 15000;
|
||||
|
||||
/* SIP status returned by a far end that accepted the REFER */
|
||||
const REFER_ACCEPTED = 202;
|
||||
|
||||
/* lowest SIP status that counts as the final response of the referred call */
|
||||
const SIP_FINAL_RESPONSE_MIN = 200;
|
||||
|
||||
/**
|
||||
* sends a sip REFER to transfer the existing call
|
||||
*/
|
||||
@@ -15,6 +24,8 @@ class TaskSipRefer extends Task {
|
||||
this.referredByDisplayName = this.data.referredByDisplayName;
|
||||
this.headers = this.data.headers || {};
|
||||
this.eventHook = this.data.eventHook;
|
||||
this._actionPromise = null;
|
||||
this._finalReferredCallStatus = null;
|
||||
}
|
||||
|
||||
get name() { return TaskName.SipRefer; }
|
||||
@@ -47,30 +58,36 @@ class TaskSipRefer extends Task {
|
||||
this.logger.info(`TaskSipRefer:exec - received ${this.referStatus} to REFER`);
|
||||
|
||||
/* if we fail, fall through to next verb. If success, we should get BYE from far end */
|
||||
if (this.referStatus === 202) {
|
||||
if (this.referStatus === REFER_ACCEPTED) {
|
||||
this._notifyTimer = setTimeout(() => {
|
||||
this.logger.info('TaskSipRefer:exec - no NOTIFY received in 15 secs, exiting');
|
||||
this.performAction({refer_status: this.referStatus})
|
||||
.catch((err) => this.logger.error(err, 'TaskSipRefer:exec - error performing action'));
|
||||
this.logger.info(`TaskSipRefer:exec - no NOTIFY received in ${NOTIFY_TIMEOUT_MS} ms, exiting`);
|
||||
this.notifyTaskDone();
|
||||
}, 15000);
|
||||
}, NOTIFY_TIMEOUT_MS);
|
||||
await this.awaitTaskDone();
|
||||
if (this._notifyTimer) {
|
||||
clearTimeout(this._notifyTimer);
|
||||
this._notifyTimer = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
await this.performAction({refer_status: this.referStatus});
|
||||
}
|
||||
/* the far end may send BYE before any NOTIFY arrives, which kills this task while we are
|
||||
awaiting above; performing the action here means the actionHook runs on every exit path -
|
||||
and while the requestor is still up - rather than only when the NOTIFY timer expires.
|
||||
When a final NOTIFY was already parsed (even if its eventHook round trip had not returned
|
||||
before the BYE woke us), _finalReferredCallStatus carries the status so the app still gets
|
||||
final_referred_call_status here rather than losing it to the memoised action */
|
||||
await this._performReferAction({
|
||||
refer_status: this.referStatus,
|
||||
...(this._finalReferredCallStatus && {final_referred_call_status: this._finalReferredCallStatus})
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'TaskSipRefer:exec - error sending REFER');
|
||||
}
|
||||
this.referSpan?.end();
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
async kill(cs, reason) {
|
||||
super.kill(cs);
|
||||
this.killReason = reason || KillReason.Hangup;
|
||||
const {dlg} = cs;
|
||||
dlg.off('notify', this.notifyHandler);
|
||||
this.notifyTaskDone();
|
||||
@@ -87,24 +104,40 @@ class TaskSipRefer extends Task {
|
||||
if (arr) {
|
||||
const status = typeof arr[1] === 'string' ? parseInt(arr[1], 10) : arr[1];
|
||||
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
|
||||
/* record the final status synchronously, before the eventHook round trip below can be
|
||||
interrupted by a BYE, so exec() can include it even if it wins the race to the action */
|
||||
if (status >= SIP_FINAL_RESPONSE_MIN) this._finalReferredCallStatus = status;
|
||||
if (this.eventHook) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
await cs.requestor.request('verb:hook', this.eventHook,
|
||||
{event: 'transfer-status', call_status: status}, httpHeaders);
|
||||
}
|
||||
if (status >= 200) {
|
||||
if (status >= SIP_FINAL_RESPONSE_MIN) {
|
||||
this.referSpan.setAttributes({'refer.finalNotify': status});
|
||||
await this.performAction({refer_status: 202, final_referred_call_status: status})
|
||||
.catch((err) => {
|
||||
this.logger.error(err, 'TaskSipRefer:exec - error performing action finalNotify');
|
||||
});
|
||||
await this._performReferAction({refer_status: REFER_ACCEPTED, final_referred_call_status: status});
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fire the verb's actionHook exactly once, whichever exit path completes the task:
|
||||
* a final NOTIFY, the NOTIFY timeout, or the task being killed by an early BYE
|
||||
*/
|
||||
_performReferAction(results) {
|
||||
if (!this._actionPromise) {
|
||||
/* when the app replaced this verb with new commands, still notify it, but do not let the
|
||||
hook response replace the application a second time (same contract as the dial verb) */
|
||||
this._actionPromise = this.performAction(results, this.killReason !== KillReason.Replaced)
|
||||
.catch((err) => this.logger.error({err}, 'TaskSipRefer:_performReferAction - error performing action'));
|
||||
}
|
||||
/* callers await the shared promise, so exec() cannot return - and let the session close the
|
||||
requestor - while an actionHook started by another exit path is still in flight */
|
||||
return this._actionPromise;
|
||||
}
|
||||
|
||||
_normalizeReferHeaders(cs, dlg) {
|
||||
let {referTo, referredBy, referredByDisplayName} = this;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user