Skip to content

Commit 8dc23a4

Browse files
authored
[@azure/web-pubsub-client] Use ping/pong as an accurate keepalive (#36751)
### Packages impacted by this PR @azure/web-pubsub-client ### Issues associated with this PR ### Describe the problem that is addressed by this PR Web PubSub follows the WebSocket (WS) ping-pong spec and periodically sends a ping frame to the client, the client should respond with a pong frame. This mechanism enables Web PubSub to detect client liveness quickly. But sometimes when there is network interruptions, Web PubSub detects the client offline quickly, however, client lacks a way to detect if itself is offline, client might consider itself as still online for quite some time and fails to reconnect quickly. In some clients, for example, in the browser, WebSocket ping/pong frames are quite low-level, and the client does not have access to there ping/pong frames or events. Here we provide a ping-pong mechanism in the application layer, providing the clients a way to detect the client liveness. ### What are the possible designs available to address the problem? If there are more than one possible design, why was the one in this PR chosen? ### Are there test cases added in this PR? _(If not, why?)_ ### Provide a list of related PRs _(if any)_ ### Command used to generate this PR:**_(Applicable only to SDK release request PRs)_ ### Checklists - [ ] Added impacted package name to the issue description - [ ] Does this PR needs any fixes in the SDK Generator?** _(If so, create an Issue in the [Autorest/typescript](https://github.com/Azure/autorest.typescript) repository and link it here)_ - [ ] Added a changelog (if necessary)
1 parent 5dea764 commit 8dc23a4

File tree

6 files changed

+190
-21
lines changed

6 files changed

+190
-21
lines changed

sdk/web-pubsub/web-pubsub-client/review/web-pubsub-client-node.api.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export type DownstreamMessageType =
4141
*/
4242
"ack"
4343
/**
44+
* Type for PongMessage
45+
*/
46+
| "pong"
47+
/**
4448
* Type for ConnectedMessage
4549
*/
4650
| "connected"
@@ -133,6 +137,16 @@ export interface OnServerDataMessageArgs {
133137
export interface OnStoppedArgs {
134138
}
135139

140+
// @public
141+
export interface PingMessage extends WebPubSubMessageBase {
142+
readonly kind: "ping";
143+
}
144+
145+
// @public
146+
export interface PongMessage extends WebPubSubMessageBase {
147+
readonly kind: "pong";
148+
}
149+
136150
// @public
137151
export type RetryMode = "Exponential" | "Fixed";
138152

@@ -224,7 +238,11 @@ export type UpstreamMessageType =
224238
/**
225239
* Type for SequenceAckMessage
226240
*/
227-
| "sequenceAck";
241+
| "sequenceAck"
242+
/**
243+
* Type for PingMessage
244+
*/
245+
| "ping";
228246

229247
// @public
230248
export class WebPubSubClient {
@@ -259,6 +277,8 @@ export interface WebPubSubClientCredential {
259277
export interface WebPubSubClientOptions {
260278
autoReconnect?: boolean;
261279
autoRejoinGroups?: boolean;
280+
keepAliveIntervalInMs?: number;
281+
keepAliveTimeoutInMs?: number;
262282
messageRetryOptions?: WebPubSubRetryOptions;
263283
protocol?: WebPubSubClientProtocol;
264284
reconnectRetryOptions?: WebPubSubRetryOptions;
@@ -298,7 +318,7 @@ export const WebPubSubJsonProtocol: () => WebPubSubClientProtocol;
298318
export const WebPubSubJsonReliableProtocol: () => WebPubSubClientProtocol;
299319

300320
// @public
301-
export type WebPubSubMessage = GroupDataMessage | ServerDataMessage | JoinGroupMessage | LeaveGroupMessage | ConnectedMessage | DisconnectedMessage | SendToGroupMessage | SendEventMessage | SequenceAckMessage | AckMessage;
321+
export type WebPubSubMessage = GroupDataMessage | ServerDataMessage | JoinGroupMessage | LeaveGroupMessage | ConnectedMessage | DisconnectedMessage | SendToGroupMessage | SendEventMessage | SequenceAckMessage | PingMessage | AckMessage | PongMessage;
302322

303323
// @public
304324
export interface WebPubSubMessageBase {

sdk/web-pubsub/web-pubsub-client/samples-dev/helloworld.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import type {
99
WebPubSubClientCredential,
1010
GetClientAccessUrlOptions,
11+
WebPubSubClientOptions,
1112
} from "@azure/web-pubsub-client";
1213
import { WebPubSubClient } from "@azure/web-pubsub-client";
1314
import { WebPubSubServiceClient } from "@azure/web-pubsub";
@@ -17,6 +18,10 @@ import "dotenv/config";
1718
async function main(): Promise<void> {
1819
const hubName = "sample_chat";
1920
const groupName = "testGroup";
21+
const options: WebPubSubClientOptions = {
22+
keepAliveTimeoutInMs: 500,
23+
keepAliveIntervalInMs: 100,
24+
};
2025
const serviceClient = new WebPubSubServiceClient(
2126
process.env.WPS_ENDPOINT!,
2227
new DefaultAzureCredential(),
@@ -30,9 +35,12 @@ async function main(): Promise<void> {
3035
})
3136
).url;
3237
};
33-
const client = new WebPubSubClient({
34-
getClientAccessUrl: fetchClientAccessUrl,
35-
} as WebPubSubClientCredential);
38+
const client = new WebPubSubClient(
39+
{
40+
getClientAccessUrl: fetchClientAccessUrl,
41+
} as WebPubSubClientCredential,
42+
options,
43+
);
3644

3745
client.on("connected", (e) => {
3846
console.log(`Connection ${e.connectionId} is connected.`);
@@ -77,7 +85,12 @@ async function main(): Promise<void> {
7785
"binary",
7886
);
7987
await delay(1000);
88+
await client.sendToGroup(groupName, "hello world after ping/pong", "text", {
89+
fireAndForget: true,
90+
});
91+
await delay(200);
8092
client.stop();
93+
console.log("Client stopped");
8194
}
8295

8396
main().catch((e) => {

sdk/web-pubsub/web-pubsub-client/src/models/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ export interface WebPubSubClientOptions {
2929
* The retry options for reconnection. Only available when autoReconnect is true.
3030
*/
3131
reconnectRetryOptions?: WebPubSubRetryOptions;
32+
/**
33+
* The idle timeout in milliseconds used to detect half-open connections when no data or pong has
34+
* been received. Default is 120000ms (120 seconds). Set to 0 to disable this timeout check. Must
35+
* be greater than or equal to 0. We recommend keeping this value comfortably larger than
36+
* `keepAliveIntervalInMs` (for example 3x) so that probes have time to run before the timeout
37+
* closes the socket.
38+
*/
39+
keepAliveTimeoutInMs?: number;
40+
/**
41+
* The interval in milliseconds at which to send keep-alive ping messages to the runtime. Default
42+
* is 20000ms (20 seconds). Set to 0 to disable client-initiated keep-alive pings. Must be greater
43+
* than or equal to 0. We recommend choosing a value that is lower than `keepAliveTimeoutInMs`
44+
* (again, about 3x lower) so the timeout only triggers when multiple pings fail.
45+
*/
46+
keepAliveIntervalInMs?: number;
3247
}
3348

3449
/**

sdk/web-pubsub/web-pubsub-client/src/models/messages.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export type WebPubSubMessage =
1616
| SendToGroupMessage
1717
| SendEventMessage
1818
| SequenceAckMessage
19-
| AckMessage;
19+
| PingMessage
20+
| AckMessage
21+
| PongMessage;
2022

2123
/**
2224
* The common of web pubsub message
@@ -33,6 +35,10 @@ export type DownstreamMessageType =
3335
* Type for AckMessage
3436
*/
3537
| "ack"
38+
/**
39+
* Type for PongMessage
40+
*/
41+
| "pong"
3642
/**
3743
* Type for ConnectedMessage
3844
*/
@@ -73,7 +79,11 @@ export type UpstreamMessageType =
7379
/**
7480
* Type for SequenceAckMessage
7581
*/
76-
| "sequenceAck";
82+
| "sequenceAck"
83+
/**
84+
* Type for PingMessage
85+
*/
86+
| "ping";
7787

7888
/**
7989
* The ack message
@@ -97,6 +107,16 @@ export interface AckMessage extends WebPubSubMessageBase {
97107
error?: AckMessageError;
98108
}
99109

110+
/**
111+
* The pong message
112+
*/
113+
export interface PongMessage extends WebPubSubMessageBase {
114+
/**
115+
* Message type
116+
*/
117+
readonly kind: "pong";
118+
}
119+
100120
/**
101121
* Error detail in AckMessage
102122
*/
@@ -305,6 +325,16 @@ export interface SequenceAckMessage extends WebPubSubMessageBase {
305325
sequenceId: number;
306326
}
307327

328+
/**
329+
* Ping message
330+
*/
331+
export interface PingMessage extends WebPubSubMessageBase {
332+
/**
333+
* Message type
334+
*/
335+
readonly kind: "ping";
336+
}
337+
308338
/**
309339
* The data type
310340
*/

sdk/web-pubsub/web-pubsub-client/src/protocols/jsonProtocolBase.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import type {
55
AckMessage,
6+
PongMessage,
67
ConnectedMessage,
78
DisconnectedMessage,
89
GroupDataMessage,
@@ -59,6 +60,8 @@ export function parseMessages(input: string): WebPubSubMessage | null {
5960
}
6061
} else if (typedMessage.type === "ack") {
6162
returnMessage = { ...parsedMessage, kind: "ack" } as AckMessage;
63+
} else if (typedMessage.type === "pong") {
64+
returnMessage = { ...parsedMessage, kind: "pong" } as PongMessage;
6265
} else {
6366
// Forward compatible
6467
return null;
@@ -102,6 +105,10 @@ export function writeMessage(message: WebPubSubMessage): string {
102105
data = { type: "sequenceAck", sequenceId: message.sequenceId } as SequenceAckData;
103106
break;
104107
}
108+
case "ping": {
109+
data = { type: "ping" } as PingData;
110+
break;
111+
}
105112
default: {
106113
throw new Error(`Unsupported type: ${message.kind}`);
107114
}
@@ -144,6 +151,10 @@ interface SequenceAckData {
144151
sequenceId: number;
145152
}
146153

154+
interface PingData {
155+
readonly type: "ping";
156+
}
157+
147158
function getPayload(data: JSONTypes | ArrayBuffer, dataType: WebPubSubDataType): any {
148159
switch (dataType) {
149160
case "text": {

0 commit comments

Comments
 (0)