Hook up LibSignalWebsocketResource.forceKeepAlive

This commit is contained in:
Jordan Rose 2024-09-23 16:24:24 -07:00 committed by GitHub
parent f7420e0512
commit ba6e11614e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 100 additions and 44 deletions

View file

@ -187,6 +187,7 @@ export class SocketManager extends EventListener {
this.queueOrHandleRequest(req); this.queueOrHandleRequest(req);
}, },
receiveStories: !this.hasStoriesDisabled, receiveStories: !this.hasStoriesDisabled,
keepalive: { path: '/v1/keepalive' },
}) })
: this.connectResource({ : this.connectResource({
name: AUTHENTICATED_CHANNEL_NAME, name: AUTHENTICATED_CHANNEL_NAME,
@ -628,6 +629,7 @@ export class SocketManager extends EventListener {
process = connectUnauthenticatedLibsignal({ process = connectUnauthenticatedLibsignal({
libsignalNet: this.libsignalNet, libsignalNet: this.libsignalNet,
name: UNAUTHENTICATED_CHANNEL_NAME, name: UNAUTHENTICATED_CHANNEL_NAME,
keepalive: { path: '/v1/keepalive' },
}); });
} else { } else {
process = this.connectResource({ process = this.connectResource({
@ -749,6 +751,7 @@ export class SocketManager extends EventListener {
const shadowingConnection = connectUnauthenticatedLibsignal({ const shadowingConnection = connectUnauthenticatedLibsignal({
libsignalNet: this.libsignalNet, libsignalNet: this.libsignalNet,
name: options.name, name: options.name,
keepalive: options.keepalive ?? {},
}); });
const shadowWrapper = async () => { const shadowWrapper = async () => {
// if main connection results in an error, // if main connection results in an error,

View file

@ -326,7 +326,7 @@ export interface IWebSocketResource extends IResource {
shutdown(): void; shutdown(): void;
close(): void; close(code?: number, reason?: string): void;
localPort(): number | undefined; localPort(): number | undefined;
} }
@ -340,9 +340,11 @@ const UNEXPECTED_DISCONNECT_CODE = 3001;
export function connectUnauthenticatedLibsignal({ export function connectUnauthenticatedLibsignal({
libsignalNet, libsignalNet,
name, name,
keepalive,
}: { }: {
libsignalNet: Net.Net; libsignalNet: Net.Net;
name: string; name: string;
keepalive: KeepAliveOptionsType;
}): AbortableProcess<LibsignalWebSocketResource> { }): AbortableProcess<LibsignalWebSocketResource> {
const logId = `LibsignalWebSocketResource(${name})`; const logId = `LibsignalWebSocketResource(${name})`;
const listener: LibsignalWebSocketResourceHolder & ConnectionEventsListener = const listener: LibsignalWebSocketResourceHolder & ConnectionEventsListener =
@ -360,7 +362,8 @@ export function connectUnauthenticatedLibsignal({
return connectLibsignal( return connectLibsignal(
libsignalNet.newUnauthenticatedChatService(listener), libsignalNet.newUnauthenticatedChatService(listener),
listener, listener,
logId logId,
keepalive
); );
} }
@ -370,12 +373,14 @@ export function connectAuthenticatedLibsignal({
credentials, credentials,
handler, handler,
receiveStories, receiveStories,
keepalive,
}: { }: {
libsignalNet: Net.Net; libsignalNet: Net.Net;
name: string; name: string;
credentials: WebAPICredentials; credentials: WebAPICredentials;
handler: (request: IncomingWebSocketRequest) => void; handler: (request: IncomingWebSocketRequest) => void;
receiveStories: boolean; receiveStories: boolean;
keepalive: KeepAliveOptionsType;
}): AbortableProcess<LibsignalWebSocketResource> { }): AbortableProcess<LibsignalWebSocketResource> {
const logId = `LibsignalWebSocketResource(${name})`; const logId = `LibsignalWebSocketResource(${name})`;
const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = { const listener: LibsignalWebSocketResourceHolder & ChatServiceListener = {
@ -424,7 +429,8 @@ export function connectAuthenticatedLibsignal({
listener listener
), ),
listener, listener,
logId logId,
keepalive
); );
} }
@ -435,7 +441,8 @@ function logDisconnectedListenerWarn(logId: string, method: string): void {
function connectLibsignal( function connectLibsignal(
chatService: Net.ChatService, chatService: Net.ChatService,
resourceHolder: LibsignalWebSocketResourceHolder, resourceHolder: LibsignalWebSocketResourceHolder,
logId: string logId: string,
keepalive: KeepAliveOptionsType
): AbortableProcess<LibsignalWebSocketResource> { ): AbortableProcess<LibsignalWebSocketResource> {
const connectAsync = async () => { const connectAsync = async () => {
try { try {
@ -444,7 +451,8 @@ function connectLibsignal(
const resource = new LibsignalWebSocketResource( const resource = new LibsignalWebSocketResource(
chatService, chatService,
IpVersion.fromDebugInfoCode(debugInfo.ipType), IpVersion.fromDebugInfoCode(debugInfo.ipType),
logId logId,
keepalive
); );
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
resourceHolder.resource = resource; resourceHolder.resource = resource;
@ -473,12 +481,21 @@ export class LibsignalWebSocketResource
{ {
closed = false; closed = false;
// Unlike WebSocketResource, libsignal will automatically attempt to keep the
// socket alive using websocket pings, so we don't need a timer-based
// keepalive mechanism. But we still send one-off keepalive requests when
// things change (see forceKeepAlive()).
private keepalive: KeepAliveSender;
constructor( constructor(
private readonly chatService: Net.ChatService, private readonly chatService: Net.ChatService,
private readonly socketIpVersion: IpVersion | undefined, private readonly socketIpVersion: IpVersion | undefined,
private readonly logId: string private readonly logId: string,
keepalive: KeepAliveOptionsType
) { ) {
super(); super();
this.keepalive = new KeepAliveSender(this, this.logId, keepalive);
} }
public localPort(): number | undefined { public localPort(): number | undefined {
@ -538,8 +555,8 @@ export class LibsignalWebSocketResource
this.dispatchEvent(event); this.dispatchEvent(event);
} }
public forceKeepAlive(): void { public forceKeepAlive(timeout?: number): void {
// no-op drop(this.keepalive.send(timeout));
} }
public async sendRequest(options: SendRequestOptions): Promise<Response> { public async sendRequest(options: SendRequestOptions): Promise<Response> {
@ -654,10 +671,10 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
this.main.addEventListener(name, handler); this.main.addEventListener(name, handler);
} }
public close(): void { public close(code = NORMAL_DISCONNECT_CODE, reason?: string): void {
this.main.close(); this.main.close(code, reason);
if (this.shadowing) { if (this.shadowing) {
this.shadowing.close(); this.shadowing.close(code, reason);
this.shadowing = undefined; this.shadowing = undefined;
} else { } else {
this.shadowingConnection.abort(); this.shadowingConnection.abort();
@ -1088,48 +1105,35 @@ const KEEPALIVE_TIMEOUT_MS = 30 * durations.SECOND;
const LOG_KEEPALIVE_AFTER_MS = 500; const LOG_KEEPALIVE_AFTER_MS = 500;
class KeepAlive { /**
private keepAliveTimer: Timers.Timeout | undefined; * References an {@link IWebSocketResource} and a request path that should
* return promptly to determine whether the connection is still alive.
*
* The response to the request must have a 2xx status code but is otherwise
* ignored. A failing response or a timeout results in the socket being closed
* with {@link UNEXPECTED_DISCONNECT_CODE}.
*
* Use the subclass {@link KeepAlive} if you want to send the request at regular
* intervals.
*/
class KeepAliveSender {
private path: string; private path: string;
private wsr: WebSocketResource; protected wsr: IWebSocketResource;
private lastAliveAt: number = Date.now(); protected logId: string;
private logId: string;
constructor( constructor(
websocketResource: WebSocketResource, websocketResource: IWebSocketResource,
name: string, name: string,
opts: KeepAliveOptionsType = {} opts: KeepAliveOptionsType = {}
) { ) {
this.logId = `WebSocketResources.KeepAlive(${name})`; this.logId = `WebSocketResources.KeepAlive(${name})`;
if (websocketResource instanceof WebSocketResource) { this.path = opts.path ?? '/';
this.path = opts.path ?? '/'; this.wsr = websocketResource;
this.wsr = websocketResource;
} else {
throw new TypeError('KeepAlive expected a WebSocketResource');
}
} }
public stop(): void { public async send(timeout = KEEPALIVE_TIMEOUT_MS): Promise<boolean> {
this.clearTimers();
}
public async send(timeout = KEEPALIVE_TIMEOUT_MS): Promise<void> {
this.clearTimers();
const isStale = isOlderThan(this.lastAliveAt, STALE_THRESHOLD_MS);
if (isStale) {
log.info(`${this.logId}.send: disconnecting due to stale state`);
this.wsr.close(
UNEXPECTED_DISCONNECT_CODE,
`Last keepalive request was too far in the past: ${this.lastAliveAt}`
);
return;
}
log.info(`${this.logId}.send: Sending a keepalive message`); log.info(`${this.logId}.send: Sending a keepalive message`);
const sentAt = Date.now(); const sentAt = Date.now();
@ -1148,14 +1152,14 @@ class KeepAlive {
UNEXPECTED_DISCONNECT_CODE, UNEXPECTED_DISCONNECT_CODE,
`keepalive response with ${status} code` `keepalive response with ${status} code`
); );
return; return false;
} }
} catch (error) { } catch (error) {
this.wsr.close( this.wsr.close(
UNEXPECTED_DISCONNECT_CODE, UNEXPECTED_DISCONNECT_CODE,
'No response to keepalive request' 'No response to keepalive request'
); );
return; return false;
} }
const responseTime = Date.now() - sentAt; const responseTime = Date.now() - sentAt;
@ -1166,8 +1170,57 @@ class KeepAlive {
); );
} }
return true;
}
}
/**
* Manages a timer that checks if a particular {@link WebSocketResource} is
* still alive.
*
* The resource must specifically be a {@link WebSocketResource}. Other kinds of
* resource are expected to manage their own liveness checks. If you want to
* manually send keepalive requests to such resources, use the base class
* {@link KeepAliveSender}.
*/
class KeepAlive extends KeepAliveSender {
private keepAliveTimer: Timers.Timeout | undefined;
private lastAliveAt: number = Date.now();
constructor(
websocketResource: WebSocketResource,
name: string,
opts: KeepAliveOptionsType = {}
) {
super(websocketResource, name, opts);
}
public stop(): void {
this.clearTimers();
}
public override async send(timeout = KEEPALIVE_TIMEOUT_MS): Promise<boolean> {
this.clearTimers();
const isStale = isOlderThan(this.lastAliveAt, STALE_THRESHOLD_MS);
if (isStale) {
log.info(`${this.logId}.send: disconnecting due to stale state`);
this.wsr.close(
UNEXPECTED_DISCONNECT_CODE,
`Last keepalive request was too far in the past: ${this.lastAliveAt}`
);
return false;
}
const isAlive = await super.send(timeout);
if (!isAlive) {
return false;
}
// Successful response on time // Successful response on time
this.reset(); this.reset();
return true;
} }
public reset(): void { public reset(): void {