Skip to content

Commit f0c9dfe

Browse files
SessionManager SIP OPTIONS ping feature
1 parent 9219aa1 commit f0c9dfe

File tree

3 files changed

+201
-7
lines changed

3 files changed

+201
-7
lines changed

docs/TODO.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
## Next Release
44

5-
- implement Session Timers or OPTIONS Ping to detect network failure
65
- review and remove everything that was deprecated
76
- complete more work in progress
87
- more documentation

src/platform/web/session-manager/session-manager-options.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,41 @@ export interface SessionManagerOptions {
115115
*/
116116
media?: SessionManagerMedia;
117117

118+
/**
119+
* If defined, SIP OPTIONS pings will be sent separated by this interval in seconds.
120+
* @remarks
121+
* When this is defined, the user agent will periodically send an OPTIONS request to the destination
122+
* to determine its reachability and will disconnect the transport if the destination is unreachable.
123+
* A destination is considered to be "out of service" if it fails to respond to an OPTIONS request,
124+
* if it sends a Service Unavailable (503) response or Request Timeout (408) response. The overall
125+
* state is considered to be "in service" when a response other than a 408 or 503 is received.
126+
*
127+
* There is currently no Javascript API to send WebSocket Ping frames or receive Pong frames.
128+
* A Ping frame may serve either as a keepalive or as a means to verify that the remote endpoint
129+
* is still responsive. It is either supported by your browser, or not. There is also no API to
130+
* enable, configure or detect whether the browser supports and is using ping/pong frames.
131+
* As such, if a keepalive and/or a means to verify that the remote endpoint is responsive is
132+
* desired, an alternative approach is needed. The intention of sending SIP OPTIONS pings
133+
* herein is to provide an application level alternative.
134+
*
135+
* There is no golden rule or best practice here. For example, too low and these messages clutter
136+
* log files and make more work for the system than is useful (10 seconds is arguably too low).
137+
* Too high and it may take longer than expected to detect a server or otherwise unreachable
138+
* (120 seconds is arguably too high). So choose a value that is reasonable for your environment.
139+
* @defaultValue `undefined`
140+
*/
141+
optionsPingInterval?: number;
142+
143+
/**
144+
* The request URI to use for SIP OPTIONS pings.
145+
* @remarks
146+
* If this is not defined but the aor option has been defined, the aor host portion of
147+
* the aor will be used to form the request URI (the assumption is this will target the
148+
* registrar server assoicated with the AOR).
149+
* @defaultValue `undefined`
150+
*/
151+
optionsPingRequestURI?: string;
152+
118153
/**
119154
* Maximum number of times to attempt to reconnection.
120155
* @remarks

src/platform/web/session-manager/session-manager.ts

Lines changed: 166 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { UserAgent } from "../../../api/user-agent.js";
2222
import { UserAgentOptions } from "../../../api/user-agent-options.js";
2323
import { UserAgentState } from "../../../api/user-agent-state.js";
2424
import { Logger } from "../../../core/log/logger.js";
25+
import { OutgoingRequest } from "../../../core/messages/outgoing-request.js";
26+
import { URI } from "../../../grammar/uri.js";
2527
import { SessionDescriptionHandler } from "../session-description-handler/session-description-handler.js";
2628
import { SessionDescriptionHandlerOptions } from "../session-description-handler/session-description-handler-options.js";
2729
import { Transport } from "../transport/transport.js";
@@ -47,9 +49,13 @@ export class SessionManager {
4749
private attemptingReconnection = false;
4850
private logger: Logger;
4951
private options: Required<SessionManagerOptions>;
52+
private optionsPingFailure = false;
53+
private optionsPingRequest?: OutgoingRequest;
54+
private optionsPingRunning = false;
55+
private optionsPingTimeout?: ReturnType<typeof setTimeout>;
5056
private registrationAttemptTimeout?: ReturnType<typeof setTimeout>;
51-
private registerer: Registerer | undefined;
52-
private registererOptions: RegistererOptions | undefined;
57+
private registerer?: Registerer;
58+
private registererOptions?: RegistererOptions;
5359
private registererRegisterOptions: RegistererRegisterOptions;
5460
private shouldBeConnected = false;
5561
private shouldBeRegistered = false;
@@ -74,6 +80,8 @@ export class SessionManager {
7480
managedSessionFactory: defaultManagedSessionFactory(),
7581
maxSimultaneousSessions: 2,
7682
media: {},
83+
optionsPingInterval: -1,
84+
optionsPingRequestURI: "",
7785
reconnectionAttempts: 3,
7886
reconnectionDelay: 4,
7987
registrationRetry: false,
@@ -127,21 +135,36 @@ export class SessionManager {
127135
if (this.delegate && this.delegate.onServerConnect) {
128136
this.delegate.onServerConnect();
129137
}
130-
// Attempt to register if we are supposed to be registered.
138+
// Attempt to register if we are supposed to be registered
131139
if (this.shouldBeRegistered) {
132140
this.register();
133141
}
142+
// Start OPTIONS pings if we are to be pinging
143+
if (this.options.optionsPingInterval > 0) {
144+
this.optionsPingStart();
145+
}
134146
},
135147
// Handle connection with server lost
136148
onDisconnect: async (error?: Error): Promise<void> => {
137149
this.logger.log(`Disconnected`);
150+
151+
// Stop OPTIONS ping if need be.
152+
let optionsPingFailure = false;
153+
if (this.options.optionsPingInterval > 0) {
154+
optionsPingFailure = this.optionsPingFailure;
155+
this.optionsPingFailure = false;
156+
this.optionsPingStop();
157+
}
158+
159+
// Let delgate know we have disconnected
138160
if (this.delegate && this.delegate.onServerDisconnect) {
139161
this.delegate.onServerDisconnect(error);
140162
}
163+
141164
// If the user called `disconnect` a graceful cleanup will be done therein.
142165
// Only cleanup if network/server dropped the connection.
143166
// Only reconnect if network/server dropped the connection
144-
if (error) {
167+
if (error || optionsPingFailure) {
145168
// There is no transport at this point, so we are not expecting to be able to
146169
// send messages much less get responses. So just dispose of everything without
147170
// waiting for anything to succeed.
@@ -266,12 +289,25 @@ export class SessionManager {
266289
}
267290
});
268291

269-
// Before unload, clean up and disconnect.
292+
// NOTE: The autoStop option does not currently work as one likley expects.
293+
// This code is here because the "autoStop behavior" and this assoicated
294+
// implemenation has been a recurring request. So instead of removing
295+
// the implementation again (because it doesn't work) and then having
296+
// to explain agian the issue over and over again to those who want it,
297+
// we have included it here to break that cycle. The implementation is
298+
// harmless and serves to provide an explaination for those interested.
270299
if (this.options.autoStop) {
300+
// Standard operation workflow will resume after this callback exits, meaning
301+
// that any asynchronous operations are likely not going to be finished, especially
302+
// if they are guaranteed to not be executed in the current tick (promises fall
303+
// under this category, they will never be resolved synchronously by design).
271304
window.addEventListener("beforeunload", async () => {
272305
this.shouldBeConnected = false;
273306
this.shouldBeRegistered = false;
274-
await this.userAgent.stop();
307+
if (this.userAgent.state !== UserAgentState.Stopped) {
308+
// The stop() method returns a promise which will not resolve before the page unloads.
309+
await this.userAgent.stop();
310+
}
275311
});
276312
}
277313
}
@@ -1167,6 +1203,130 @@ export class SessionManager {
11671203
};
11681204
}
11691205

1206+
/**
1207+
* Periodically send OPTIONS pings and disconnect when a ping fails.
1208+
* @param requestURI - Request URI to target
1209+
* @param fromURI - From URI
1210+
* @param toURI - To URI
1211+
*/
1212+
private optionsPingRun(requestURI: URI, fromURI: URI, toURI: URI): void {
1213+
// Guard against nvalid interval
1214+
if (this.options.optionsPingInterval < 1) {
1215+
throw new Error("Invalid options ping interval.");
1216+
}
1217+
// Guard against sending a ping when there is one outstanading
1218+
if (this.optionsPingRunning) {
1219+
return;
1220+
}
1221+
this.optionsPingRunning = true;
1222+
1223+
// Setup next ping to run in future
1224+
this.optionsPingTimeout = setTimeout(() => {
1225+
this.optionsPingTimeout = undefined;
1226+
1227+
// If ping succeeds...
1228+
const onPingSuccess = () => {
1229+
// record success or failure
1230+
this.optionsPingFailure = false;
1231+
// if we are still running, queue up the next ping
1232+
if (this.optionsPingRunning) {
1233+
this.optionsPingRunning = false;
1234+
this.optionsPingRun(requestURI, fromURI, toURI);
1235+
}
1236+
};
1237+
1238+
// If ping fails...
1239+
const onPingFailure = () => {
1240+
this.logger.error("OPTIONS ping failed");
1241+
// record success or failure
1242+
this.optionsPingFailure = true;
1243+
// stop running
1244+
this.optionsPingRunning = false;
1245+
// disconnect the transport
1246+
this.userAgent.transport.disconnect().catch((error) => this.logger.error(error));
1247+
};
1248+
1249+
// Create an OPTIONS request message
1250+
const core = this.userAgent.userAgentCore;
1251+
const message = core.makeOutgoingRequestMessage("OPTIONS", requestURI, fromURI, toURI, {});
1252+
1253+
// Send the request message
1254+
this.optionsPingRequest = core.request(message, {
1255+
onAccept: () => {
1256+
this.optionsPingRequest = undefined;
1257+
onPingSuccess();
1258+
},
1259+
onReject: (response) => {
1260+
this.optionsPingRequest = undefined;
1261+
// Ping fails on following responses...
1262+
// - 408 Request Timeout (no response was received)
1263+
// - 503 Service Unavailable (a transport layer error occured)
1264+
if (response.message.statusCode === 408 || response.message.statusCode === 503) {
1265+
onPingFailure();
1266+
} else {
1267+
onPingSuccess();
1268+
}
1269+
}
1270+
});
1271+
}, this.options.optionsPingInterval * 1000);
1272+
}
1273+
1274+
/**
1275+
* Start sending OPTIONS pings.
1276+
*/
1277+
private optionsPingStart(): void {
1278+
this.logger.log(`OPTIONS pings started`);
1279+
1280+
// Create the URIs needed to send OPTIONS pings
1281+
let requestURI, fromURI, toURI;
1282+
if (this.options.optionsPingRequestURI) {
1283+
// Use whatever specific RURI is provided.
1284+
requestURI = UserAgent.makeURI(this.options.optionsPingRequestURI);
1285+
if (!requestURI) {
1286+
throw new Error("Failed to create Request URI.");
1287+
}
1288+
// Use the user agent's contact URI for From and To URIs
1289+
fromURI = this.userAgent.contact.uri.clone();
1290+
toURI = this.userAgent.contact.uri.clone();
1291+
} else if (this.options.aor) {
1292+
// Otherwise use the AOR provided to target the assocated registrar server.
1293+
const uri = UserAgent.makeURI(this.options.aor);
1294+
if (!uri) {
1295+
throw new Error("Failed to create URI.");
1296+
}
1297+
requestURI = uri.clone();
1298+
requestURI.user = undefined; // target the registrar server
1299+
fromURI = uri.clone();
1300+
toURI = uri.clone();
1301+
} else {
1302+
this.logger.error(
1303+
"You have enabled sending OPTIONS pings and as such you must provide either " +
1304+
"a) an AOR to register, or b) an RURI to use for the target of the OPTIONS ping requests. "
1305+
);
1306+
return;
1307+
}
1308+
1309+
// Send the OPTIONS pings
1310+
this.optionsPingRun(requestURI, fromURI, toURI);
1311+
}
1312+
1313+
/**
1314+
* Stop sending OPTIONS pings.
1315+
*/
1316+
private optionsPingStop(): void {
1317+
this.logger.log(`OPTIONS pings stopped`);
1318+
this.optionsPingRunning = false;
1319+
this.optionsPingFailure = false;
1320+
if (this.optionsPingRequest) {
1321+
this.optionsPingRequest.dispose();
1322+
this.optionsPingRequest = undefined;
1323+
}
1324+
if (this.optionsPingTimeout) {
1325+
clearTimeout(this.optionsPingTimeout);
1326+
this.optionsPingTimeout = undefined;
1327+
}
1328+
}
1329+
11701330
/** Helper function to init send then send invite. */
11711331
private async sendInvite(
11721332
inviter: Inviter,

0 commit comments

Comments
 (0)