- 
                Notifications
    
You must be signed in to change notification settings  - Fork 638
 
fix(middleware-sdk-transcribe-streaming): close sockets on WebSocketHandler.destroy() #4400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0ea26a3
              0b5b9fb
              445c315
              d277a54
              a08cc00
              c54d772
              e1fedfd
              e1a8c01
              9bf9f81
              bec0b0e
              87a0ebc
              517b75b
              b75951f
              fc82816
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -22,98 +22,143 @@ export class WebSocketHandler implements HttpHandler { | |
| handlerProtocol: "websocket", | ||
| }; | ||
| private readonly connectionTimeout: number; | ||
| private readonly sockets: Record<string, WebSocket[]> = {}; | ||
| 
     | 
||
| constructor({ connectionTimeout }: WebSocketHandlerOptions = {}) { | ||
| this.connectionTimeout = connectionTimeout || 2000; | ||
| } | ||
| 
     | 
||
| destroy(): void {} | ||
| /** | ||
| * Destroys the WebSocketHandler. | ||
| * Closes all sockets from the socket pool. | ||
| */ | ||
| destroy(): void { | ||
| for (const [key, sockets] of Object.entries(this.sockets)) { | ||
| for (const socket of sockets) { | ||
| socket.close(1000, `Socket closed through destroy() call`); | ||
| } | ||
| delete this.sockets[key]; | ||
| } | ||
| } | ||
| 
     | 
||
| /** | ||
| * Removes all closing/closed sockets from the socket pool for URL. | ||
| */ | ||
| private removeNotUsableSockets(url: string): void { | ||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rename to  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was originally named   | 
||
| this.sockets[url] = this.sockets[url].filter( | ||
| (socket) => ![WebSocket.CLOSING, WebSocket.CLOSED].includes(socket.readyState) | ||
| ); | ||
| } | ||
| 
     | 
||
| async handle(request: HttpRequest): Promise<{ response: HttpResponse }> { | ||
| const url = formatUrl(request); | ||
| const socket: WebSocket = new WebSocket(url); | ||
| 
     | 
||
| // Add socket to sockets pool | ||
| if (!this.sockets[url]) { | ||
| this.sockets[url] = []; | ||
| } | ||
| this.sockets[url].push(socket); | ||
| 
     | 
||
| socket.binaryType = "arraybuffer"; | ||
| await waitForReady(socket, this.connectionTimeout); | ||
| await this.waitForReady(socket, this.connectionTimeout); | ||
| 
     | 
||
| const { body } = request; | ||
| const bodyStream = getIterator(body); | ||
| const asyncIterable = connect(socket, bodyStream); | ||
| const asyncIterable = this.connect(socket, bodyStream); | ||
| const outputPayload = toReadableStream(asyncIterable); | ||
| 
     | 
||
| return { | ||
| response: new HttpResponse({ | ||
| statusCode: 200, // indicates connection success | ||
| body: outputPayload, | ||
| }), | ||
| }; | ||
| } | ||
| } | ||
| 
     | 
||
| const waitForReady = (socket: WebSocket, connectionTimeout: number): Promise<void> => | ||
| new Promise((resolve, reject) => { | ||
| const timeout = setTimeout(() => { | ||
| reject({ | ||
| $metadata: { | ||
| httpStatusCode: 500, | ||
| }, | ||
| }); | ||
| }, connectionTimeout); | ||
| socket.onopen = () => { | ||
| clearTimeout(timeout); | ||
| resolve(); | ||
| }; | ||
| }); | ||
| 
     | 
||
| const connect = (socket: WebSocket, data: AsyncIterable<Uint8Array>): AsyncIterable<Uint8Array> => { | ||
| // To notify output stream any error thrown after response | ||
| // is returned while data keeps streaming. | ||
| let streamError: Error | undefined = undefined; | ||
| const outputStream: AsyncIterable<Uint8Array> = { | ||
| [Symbol.asyncIterator]: () => ({ | ||
| next: () => { | ||
| return new Promise((resolve, reject) => { | ||
| socket.onerror = (error) => { | ||
| socket.onclose = null; | ||
| socket.close(); | ||
| reject(error); | ||
| }; | ||
| socket.onclose = () => { | ||
| if (streamError) { | ||
| reject(streamError); | ||
| } else { | ||
| private waitForReady(socket: WebSocket, connectionTimeout: number): Promise<void> { | ||
| return new Promise((resolve, reject) => { | ||
| const timeout = setTimeout(() => { | ||
| this.removeNotUsableSockets(socket.url); | ||
| reject({ | ||
| $metadata: { | ||
| httpStatusCode: 500, | ||
| }, | ||
| }); | ||
| }, connectionTimeout); | ||
| 
     | 
||
| socket.onopen = () => { | ||
| clearTimeout(timeout); | ||
| resolve(); | ||
| }; | ||
| }); | ||
| } | ||
| 
     | 
||
| private connect(socket: WebSocket, data: AsyncIterable<Uint8Array>): AsyncIterable<Uint8Array> { | ||
| // To notify output stream any error thrown after response | ||
| // is returned while data keeps streaming. | ||
| let streamError: Error | undefined = undefined; | ||
| 
     | 
||
| const outputStream: AsyncIterable<Uint8Array> = { | ||
| [Symbol.asyncIterator]: () => ({ | ||
| next: () => { | ||
| return new Promise((resolve, reject) => { | ||
| // To notify onclose event that error has occurred | ||
| let socketErrorOccurred = false; | ||
| 
     | 
||
| socket.onerror = (error) => { | ||
| socketErrorOccurred = true; | ||
| socket.close(); | ||
| reject(error); | ||
| }; | ||
| 
     | 
||
| socket.onclose = () => { | ||
| this.removeNotUsableSockets(socket.url); | ||
| if (socketErrorOccurred) return; | ||
| 
     | 
||
| if (streamError) { | ||
| reject(streamError); | ||
| } else { | ||
| resolve({ | ||
| done: true, | ||
| value: undefined, | ||
| }); | ||
| } | ||
| }; | ||
| 
     | 
||
| socket.onmessage = (event) => { | ||
| resolve({ | ||
| done: true, | ||
| value: undefined, | ||
| done: false, | ||
| value: new Uint8Array(event.data), | ||
| }); | ||
| } | ||
| }; | ||
| socket.onmessage = (event) => { | ||
| resolve({ | ||
| done: false, | ||
| value: new Uint8Array(event.data), | ||
| }); | ||
| }; | ||
| }); | ||
| }, | ||
| }), | ||
| }; | ||
| }; | ||
| }); | ||
| }, | ||
| }), | ||
| }; | ||
| 
     | 
||
| const send = async (): Promise<void> => { | ||
| try { | ||
| for await (const inputChunk of data) { | ||
| socket.send(inputChunk); | ||
| const send = async (): Promise<void> => { | ||
| try { | ||
| for await (const inputChunk of data) { | ||
| socket.send(inputChunk); | ||
| } | ||
| } catch (err) { | ||
| // We don't throw the error here because the send()'s returned | ||
| // would already be settled by the time sending chunk throws error. | ||
| // Instead, the notify the output stream to throw if there's | ||
| // exceptions | ||
| streamError = err; | ||
| } finally { | ||
| // WS status code: https://tools.ietf.org/html/rfc6455#section-7.4 | ||
| socket.close(1000); | ||
| } | ||
| } catch (err) { | ||
| // We don't throw the error here because the send()'s returned | ||
| // would already be settled by the time sending chunk throws error. | ||
| // Instead, the notify the output stream to throw if there's | ||
| // exceptions | ||
| streamError = err; | ||
| } finally { | ||
| // WS status code: https://tools.ietf.org/html/rfc6455#section-7.4 | ||
| socket.close(1000); | ||
| } | ||
| }; | ||
| send(); | ||
| return outputStream; | ||
| }; | ||
| }; | ||
| 
     | 
||
| send(); | ||
| 
     | 
||
| return outputStream; | ||
| } | ||
| } | ||
| 
     | 
||
| /** | ||
| * Transfer payload data to an AsyncIterable. | ||
| 
        
          
        
         | 
    @@ -123,18 +168,21 @@ const connect = (socket: WebSocket, data: AsyncIterable<Uint8Array>): AsyncItera | |
| */ | ||
| const getIterator = (stream: any): AsyncIterable<any> => { | ||
| // Noop if stream is already an async iterable | ||
| if (stream[Symbol.asyncIterator]) return stream; | ||
| else if (isReadableStream(stream)) { | ||
| if (stream[Symbol.asyncIterator]) { | ||
| return stream; | ||
| } | ||
| 
     | 
||
| if (isReadableStream(stream)) { | ||
| //If stream is a ReadableStream, transfer the ReadableStream to async iterable. | ||
| return readableStreamtoIterable(stream); | ||
| } else { | ||
| //For other types, just wrap them with an async iterable. | ||
| return { | ||
| [Symbol.asyncIterator]: async function* () { | ||
| yield stream; | ||
| }, | ||
| }; | ||
| } | ||
| 
     | 
||
| // For other types, just wrap them with an async iterable. | ||
| return { | ||
| [Symbol.asyncIterator]: async function* () { | ||
| yield stream; | ||
| }, | ||
| }; | ||
| }; | ||
| 
     | 
||
| /** | ||
| 
          
            
          
           | 
    ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on what timing does this transition to
WebSocket.CLOSED?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried timeouts of 1s and 5s, but the state was still closing.
That's why I updated source code to remove sockets with closing state, and verified state to be closing.