From ab04e4900f1a73324a0ff7de6363421ad579bf26 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 7 Oct 2024 17:24:18 +0200 Subject: [PATCH 01/10] feat: Support for openMediaHotStandby disable heartbeat on secondary when not used --- packages/connector/src/MosConnection.ts | 15 ++++++++++++++- packages/connector/src/api.ts | 6 ++++++ .../src/connection/NCSServerConnection.ts | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/connector/src/MosConnection.ts b/packages/connector/src/MosConnection.ts index f2028ec6..ddb50c27 100644 --- a/packages/connector/src/MosConnection.ts +++ b/packages/connector/src/MosConnection.ts @@ -92,7 +92,7 @@ export class MosConnection extends EventEmitter implements this._debug, this.mosTypes.strict ) - let secondary = null + let secondary: NCSServerConnection | null = null this._ncsConnections[connectionOptions.primary.host] = primary primary.on('rawMessage', (type: string, message: string) => { @@ -174,6 +174,19 @@ export class MosConnection extends EventEmitter implements false ) } + // Handle that openMediaHotStandby should not check for heartbeats on + // the secondary connection when the primary is connected + if (connectionOptions.secondary?.openMediaHotStandby) { + primary.on('connectionChanged', () => { + if (secondary) { + if (primary.connected) { + secondary.disableHeartbeats() + } else { + secondary.enableHeartbeats() + } + } + }) + } } return this._registerMosDevice( diff --git a/packages/connector/src/api.ts b/packages/connector/src/api.ts index 6e6f3158..1f9d78dd 100644 --- a/packages/connector/src/api.ts +++ b/packages/connector/src/api.ts @@ -663,5 +663,11 @@ export interface IMOSDeviceConnectionOptions { * Set this to true to not use that port (will cause some methods to stop working) */ dontUseQueryPort?: boolean + + /** (Optional) OpenMedia has a term called hot standby. + * Set this to true to treat the secondary server as OpenMedia hot-standby. + * And thus ignore the standby state unless the primary server is down. + */ + openMediaHotStandby?: boolean } } diff --git a/packages/connector/src/connection/NCSServerConnection.ts b/packages/connector/src/connection/NCSServerConnection.ts index 859d9f6d..f8217f0a 100644 --- a/packages/connector/src/connection/NCSServerConnection.ts +++ b/packages/connector/src/connection/NCSServerConnection.ts @@ -106,6 +106,20 @@ export class NCSServerConnection extends EventEmitter delete this._clients[clientID] } + /** */ + disableHeartbeats(): void { + for (const i in this._clients) { + this._clients[i].useHeartbeats = false + } + } + + /** */ + enableHeartbeats(): void { + for (const i in this._clients) { + this._clients[i].useHeartbeats = true + } + } + connect(): void { for (const i in this._clients) { // Connect client From f550bd54cbd48cda0ab1becab011d4af5ff72658 Mon Sep 17 00:00:00 2001 From: olzzon Date: Tue, 8 Oct 2024 08:25:38 +0200 Subject: [PATCH 02/10] fix: Disable heartbeat on primary if secondary is active in openMediaHotStandby mode. --- packages/connector/src/MosConnection.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/connector/src/MosConnection.ts b/packages/connector/src/MosConnection.ts index ddb50c27..64d883a7 100644 --- a/packages/connector/src/MosConnection.ts +++ b/packages/connector/src/MosConnection.ts @@ -176,13 +176,16 @@ export class MosConnection extends EventEmitter implements } // Handle that openMediaHotStandby should not check for heartbeats on // the secondary connection when the primary is connected + // And disable heartbeats on the primary when the primary is disconnected if (connectionOptions.secondary?.openMediaHotStandby) { primary.on('connectionChanged', () => { if (secondary) { if (primary.connected) { secondary.disableHeartbeats() + primary.enableHeartbeats() } else { secondary.enableHeartbeats() + primary.disableHeartbeats() } } }) From 6b7a9a3b403ecb1603fe9c69e2aa1589c9c73c93 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 21 Oct 2024 08:25:42 +0200 Subject: [PATCH 03/10] chore: rename property to be more generic --- packages/connector/src/MosConnection.ts | 4 ++-- packages/connector/src/api.ts | 10 ++++++---- .../connector/src/connection/NCSServerConnection.ts | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/connector/src/MosConnection.ts b/packages/connector/src/MosConnection.ts index 73290f4f..e39dc458 100644 --- a/packages/connector/src/MosConnection.ts +++ b/packages/connector/src/MosConnection.ts @@ -174,10 +174,10 @@ export class MosConnection extends EventEmitter implements false ) } - // Handle that openMediaHotStandby should not check for heartbeats on + // Handle that .isHotStandby should not check for heartbeats on // the secondary connection when the primary is connected // And disable heartbeats on the primary when the primary is disconnected - if (connectionOptions.secondary?.openMediaHotStandby) { + if (connectionOptions.secondary?.isHotStandby) { primary.on('connectionChanged', () => { if (secondary) { if (primary.connected) { diff --git a/packages/connector/src/api.ts b/packages/connector/src/api.ts index 1f9d78dd..d1602fa8 100644 --- a/packages/connector/src/api.ts +++ b/packages/connector/src/api.ts @@ -664,10 +664,12 @@ export interface IMOSDeviceConnectionOptions { */ dontUseQueryPort?: boolean - /** (Optional) OpenMedia has a term called hot standby. - * Set this to true to treat the secondary server as OpenMedia hot-standby. - * And thus ignore the standby state unless the primary server is down. + /** (Optional) Treat the secondary server as a "hot standby". + * A "hot standby" is a server that is powered down / in standby while the primary server is up. + * Set this to true to defer checking the connection status to the secondary server while the primary is up. + * + * (This was added to support the hot standby feature of OpenMedia NRCS.) */ - openMediaHotStandby?: boolean + isHotStandby?: boolean } } diff --git a/packages/connector/src/connection/NCSServerConnection.ts b/packages/connector/src/connection/NCSServerConnection.ts index f8217f0a..15f8084f 100644 --- a/packages/connector/src/connection/NCSServerConnection.ts +++ b/packages/connector/src/connection/NCSServerConnection.ts @@ -112,7 +112,7 @@ export class NCSServerConnection extends EventEmitter this._clients[i].useHeartbeats = false } } - + /** */ enableHeartbeats(): void { for (const i in this._clients) { From 6b4a03cc703df32514b8ef75ec6e3510216b253f Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Mon, 21 Oct 2024 08:26:41 +0200 Subject: [PATCH 04/10] chore: add unit test for Hot Standby feature --- packages/connector/src/MosDevice.ts | 13 ++ packages/connector/src/__mocks__/socket.ts | 6 + .../src/__tests__/MosConnection.spec.ts | 150 ++++++++++++++++++ .../src/connection/NCSServerConnection.ts | 6 + 4 files changed, 175 insertions(+) diff --git a/packages/connector/src/MosDevice.ts b/packages/connector/src/MosDevice.ts index ab493dd1..babf1364 100644 --- a/packages/connector/src/MosDevice.ts +++ b/packages/connector/src/MosDevice.ts @@ -231,6 +231,19 @@ export class MosDevice implements IMOSDevice { return this._secondaryConnection ? this._secondaryConnection.id : null } + /** @deprecated This is for unit tests only */ + get connections(): { + primary: NCSServerConnection | null + secondary: NCSServerConnection | null + current: NCSServerConnection | null + } { + return { + primary: this._primaryConnection, + secondary: this._secondaryConnection, + current: this._currentConnection, + } + } + connect(): void { if (this._primaryConnection) this._primaryConnection.connect() if (this._secondaryConnection) this._secondaryConnection.connect() diff --git a/packages/connector/src/__mocks__/socket.ts b/packages/connector/src/__mocks__/socket.ts index da55a5ea..d6afcd93 100644 --- a/packages/connector/src/__mocks__/socket.ts +++ b/packages/connector/src/__mocks__/socket.ts @@ -51,6 +51,7 @@ export class SocketMock extends EventEmitter implements Socket { private _responses: Array = [] private _autoReplyToHeartBeat = true + public mockConnectCount = 0 constructor() { super() @@ -102,6 +103,7 @@ export class SocketMock extends EventEmitter implements Socket { } // @ts-expect-error mock connect(port: number, host: string): this { + this.mockConnectCount++ this.connectedPort = port this.connectedHost = host @@ -197,6 +199,10 @@ export class SocketMock extends EventEmitter implements Socket { this.emit('connect') } + mockEmitClose(): void { + this.emit('close') + } + mockSentMessage0(data: unknown, encoding: string): void { if (this._autoReplyToHeartBeat) { const str: string = typeof data === 'string' ? data : this.decode(data as any) diff --git a/packages/connector/src/__tests__/MosConnection.spec.ts b/packages/connector/src/__tests__/MosConnection.spec.ts index 8e110027..ae5ba427 100644 --- a/packages/connector/src/__tests__/MosConnection.spec.ts +++ b/packages/connector/src/__tests__/MosConnection.spec.ts @@ -532,6 +532,156 @@ describe('MosDevice: General', () => { expect(onError).toHaveBeenCalledTimes(0) expect(onWarning).toHaveBeenCalledTimes(0) + await mos.dispose() + }) + test('Hot standby', async () => { + const mos = new MosConnection({ + mosID: 'jestMOS', + acceptsConnections: true, + profiles: { + '0': true, + '1': true, + }, + }) + const onError = jest.fn((e) => console.log(e)) + const onWarning = jest.fn((e) => console.log(e)) + mos.on('error', onError) + mos.on('warning', onWarning) + + expect(mos.acceptsConnections).toBe(true) + await initMosConnection(mos) + expect(mos.isListening).toBe(true) + + const mosDevice = await mos.connect({ + primary: { + id: 'primary', + host: '192.168.0.1', + timeout: 200, + }, + secondary: { + id: 'secondary', + host: '192.168.0.2', + timeout: 200, + isHotStandby: true, + }, + }) + + expect(mosDevice).toBeTruthy() + expect(mosDevice.idPrimary).toEqual('jestMOS_primary') + + expect(mosDevice.connections.primary).toBeTruthy() + expect(mosDevice.connections.secondary).toBeTruthy() + mosDevice.connections.primary?.setAutoReconnectInterval(300) + mosDevice.connections.secondary?.setAutoReconnectInterval(300) + + const onConnectionChange = jest.fn() + mosDevice.onConnectionChange((connectionStatus: IMOSConnectionStatus) => { + onConnectionChange(connectionStatus) + }) + + expect(SocketMock.instances).toHaveLength(7) + expect(SocketMock.instances[1].connectedHost).toEqual('192.168.0.1') + expect(SocketMock.instances[1].connectedPort).toEqual(10540) + expect(SocketMock.instances[2].connectedHost).toEqual('192.168.0.1') + expect(SocketMock.instances[2].connectedPort).toEqual(10541) + expect(SocketMock.instances[3].connectedHost).toEqual('192.168.0.1') + expect(SocketMock.instances[3].connectedPort).toEqual(10542) + + // TODO: Perhaps the hot-standby should not be connected at all at this point? + expect(SocketMock.instances[4].connectedHost).toEqual('192.168.0.2') + expect(SocketMock.instances[4].connectedPort).toEqual(10540) + expect(SocketMock.instances[5].connectedHost).toEqual('192.168.0.2') + expect(SocketMock.instances[5].connectedPort).toEqual(10541) + expect(SocketMock.instances[6].connectedHost).toEqual('192.168.0.2') + expect(SocketMock.instances[6].connectedPort).toEqual(10542) + + // Simulate primary connected: + for (const i of SocketMock.instances) { + if (i.connectedHost === '192.168.0.1') i.mockEmitConnected() + } + // Wait for the primary to be initially connected: + await waitFor(() => mosDevice.getConnectionStatus().PrimaryConnected, 1000) + + // Check that the connection status is as we expect: + expect(mosDevice.getConnectionStatus()).toMatchObject({ + PrimaryConnected: true, + PrimaryStatus: 'Primary: Connected', + SecondaryConnected: true, // Is a hot standby, so we pretend that it is connected + SecondaryStatus: 'Secondary: Is hot Standby', + }) + expect(onConnectionChange).toHaveBeenCalled() + expect(onConnectionChange).toHaveBeenLastCalledWith({ + PrimaryConnected: true, + PrimaryStatus: 'Primary: Connected', + SecondaryConnected: true, // Is a hot standby, so we pretend that it is connected + SecondaryStatus: 'Secondary: Is hot Standby', + }) + onConnectionChange.mockClear() + + // Simulate primary disconnect, secondary hot standby takes over: + for (const i of SocketMock.instances) { + i.mockConnectCount = 0 + if (i.connectedHost === '192.168.0.1') i.mockEmitClose() + if (i.connectedHost === '192.168.0.2') i.mockEmitConnected() + } + + // Wait for the secondary to be connected: + await waitFor(() => mosDevice.getConnectionStatus().SecondaryConnected, 1000) + + // Check that the connection status is as we expect: + expect(mosDevice.getConnectionStatus()).toMatchObject({ + PrimaryConnected: false, + PrimaryStatus: expect.stringContaining('Primary'), + SecondaryConnected: true, + SecondaryStatus: 'Secondary: Connected', + }) + expect(onConnectionChange).toHaveBeenCalled() + expect(onConnectionChange).toHaveBeenLastCalledWith({ + PrimaryConnected: false, + PrimaryStatus: expect.stringContaining('Primary'), + SecondaryConnected: true, + SecondaryStatus: 'Secondary: Connected', + }) + onConnectionChange.mockClear() + + // Simulate that the primary comes back online: + for (const i of SocketMock.instances) { + if (i.connectedHost === '192.168.0.1') { + expect(i.mockConnectCount).toBeGreaterThanOrEqual(1) // should have tried to reconnect + i.mockEmitConnected() + } + + if (i.connectedHost === '192.168.0.2') i.mockEmitClose() + } + + // Wait for the primary to be connected: + await waitFor(() => mosDevice.getConnectionStatus().PrimaryConnected, 1000) + + // Check that the connection status is as we expect: + expect(mosDevice.getConnectionStatus()).toMatchObject({ + PrimaryConnected: true, + PrimaryStatus: 'Primary: Connected', + SecondaryConnected: true, // Is a hot standby, so we pretend that it is connected + SecondaryStatus: 'Secondary: Is hot Standby', + }) + expect(onConnectionChange).toHaveBeenCalled() + expect(onConnectionChange).toHaveBeenLastCalledWith({ + PrimaryConnected: true, + PrimaryStatus: 'Primary: Connected', + SecondaryConnected: true, // Is a hot standby, so we pretend that it is connected + SecondaryStatus: 'Secondary: Is hot Standby', + }) + await mos.dispose() }) }) +async function waitFor(fcn: () => boolean, timeout: number): Promise { + const startTime = Date.now() + + while (Date.now() - startTime < timeout) { + await delay(10) + + if (fcn()) return + } + throw new Error('Timeout in waitFor') +} diff --git a/packages/connector/src/connection/NCSServerConnection.ts b/packages/connector/src/connection/NCSServerConnection.ts index 15f8084f..1e44314c 100644 --- a/packages/connector/src/connection/NCSServerConnection.ts +++ b/packages/connector/src/connection/NCSServerConnection.ts @@ -120,6 +120,12 @@ export class NCSServerConnection extends EventEmitter } } + setAutoReconnectInterval(interval: number): void { + for (const i in this._clients) { + this._clients[i].client.autoReconnectInterval = interval + } + } + connect(): void { for (const i in this._clients) { // Connect client From c2b35bec5db5c85ab91e97f09db2d33bb104589e Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 28 Oct 2024 10:25:41 +0100 Subject: [PATCH 05/10] fix: hotStandby - set heartbeats state no matter secondary's state --- packages/connector/src/MosConnection.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/connector/src/MosConnection.ts b/packages/connector/src/MosConnection.ts index 64d883a7..8973eee5 100644 --- a/packages/connector/src/MosConnection.ts +++ b/packages/connector/src/MosConnection.ts @@ -178,15 +178,16 @@ export class MosConnection extends EventEmitter implements // the secondary connection when the primary is connected // And disable heartbeats on the primary when the primary is disconnected if (connectionOptions.secondary?.openMediaHotStandby) { + // Initially disable heartbeats on secondary since primary should be attempted first + secondary.disableHeartbeats() + primary.on('connectionChanged', () => { - if (secondary) { - if (primary.connected) { - secondary.disableHeartbeats() - primary.enableHeartbeats() - } else { - secondary.enableHeartbeats() - primary.disableHeartbeats() - } + if (primary.connected) { + secondary?.disableHeartbeats() + primary.enableHeartbeats() + } else { + secondary?.enableHeartbeats() + primary.disableHeartbeats() } }) } From 5985b191fcaea9d54d2bec73d4abf87b02f8bd26 Mon Sep 17 00:00:00 2001 From: olzzon Date: Mon, 28 Oct 2024 10:26:14 +0100 Subject: [PATCH 06/10] feat: Add tests for openMediaHotStandby --- .../src/__tests__/OpenMediaHotStandby.spec.ts | 122 ++++++++++++++++++ .../src/connection/NCSServerConnection.ts | 10 +- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts diff --git a/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts b/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts new file mode 100644 index 00000000..79c15ab8 --- /dev/null +++ b/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts @@ -0,0 +1,122 @@ +import { MosConnection } from "../MosConnection"; +import { getMosConnection, setupMocks } from "./lib"; +import { NCSServerConnection } from "../connection/NCSServerConnection"; + +describe('Hot Standby Feature', () => { + let mosConnection: MosConnection; + let primary: NCSServerConnection | null; + let secondary: NCSServerConnection | null; + + beforeAll(() => { + setupMocks(); + }); + + beforeEach(async () => { + mosConnection = await getMosConnection({ + '0': true, + '1': true, + }, false); + + const device = await mosConnection.connect({ + primary: { + id: 'primary', + host: '127.0.0.1', + }, + secondary: { + id: 'secondary', + host: '127.0.0.2', + openMediaHotStandby: true + } + }); + + // Wait for connections to be established + await new Promise(resolve => setTimeout(resolve, 100)); + + primary = device['_primaryConnection']; + secondary = device['_secondaryConnection']; + }); + + test('should disable secondary heartbeats when primary is connected', async () => { + expect(primary).toBeTruthy(); + expect(secondary).toBeTruthy(); + + if (primary && secondary) { + expect(primary.isHearbeatEnabled()).toBe(true); + expect(secondary.isHearbeatEnabled()).toBe(false); + } + }); + + test('should enable secondary heartbeats when primary disconnects', async () => { + expect(primary).toBeTruthy(); + expect(secondary).toBeTruthy(); + + if (primary && secondary) { + // Simulate primary disconnect + await primary.dispose(); + + // Wait for primary to disconnect + await new Promise(resolve => setTimeout(resolve, 100)); + + // Verify heartbeat states switched correctly + expect(secondary.isHearbeatEnabled()).toBe(true); + expect(primary.isHearbeatEnabled()).toBe(false); + } + }); + + test('should disable primary heartbeasts when secondary is connected and primary is disconnected', async () => { + expect(primary).toBeTruthy(); + expect(secondary).toBeTruthy(); + + if (primary && secondary) { + // Simulate primary disconnect + await primary.dispose(); + + // Wait for primary to disconnect + await new Promise(resolve => setTimeout(resolve, 100)); + + // Wait for secondary to connect + await new Promise(resolve => setTimeout(resolve, 100)); + + // Verify heartbeat states switched correctly + expect(secondary.isHearbeatEnabled()).toBe(true); + expect(primary.isHearbeatEnabled()).toBe(false); + } + }) + + test('should handle rapid primary connection changes', async () => { + expect(primary).toBeTruthy(); + expect(secondary).toBeTruthy(); + + if (primary && secondary) { + const connectionStates: boolean[] = []; + + // Rapidly toggle primary connection + for (let i = 0; i < 5; i++) { + await primary.dispose(); + await new Promise(resolve => setTimeout(resolve, 50)); + primary.connect(); + await new Promise(resolve => setTimeout(resolve, 50)); + + connectionStates.push( + secondary.connected, + primary.connected + ); + } + + // Verify states remained consistent + connectionStates.forEach((state, i) => { + if (i % 2 === 0) { + expect(state).toBe(false); // Secondary should be disabled + } else { + expect(state).toBe(true); // Primary should be enabled + } + }); + } + }); + + afterEach(async () => { + if (mosConnection) { + await mosConnection.dispose(); + } + }); +}); \ No newline at end of file diff --git a/packages/connector/src/connection/NCSServerConnection.ts b/packages/connector/src/connection/NCSServerConnection.ts index f8217f0a..ae2d76c4 100644 --- a/packages/connector/src/connection/NCSServerConnection.ts +++ b/packages/connector/src/connection/NCSServerConnection.ts @@ -112,7 +112,7 @@ export class NCSServerConnection extends EventEmitter this._clients[i].useHeartbeats = false } } - + /** */ enableHeartbeats(): void { for (const i in this._clients) { @@ -120,6 +120,14 @@ export class NCSServerConnection extends EventEmitter } } + /** */ + isHearbeatEnabled(): boolean { + for (const i in this._clients) { + if (this._clients[i].useHeartbeats) return true + } + return false + } + connect(): void { for (const i in this._clients) { // Connect client From 248a4f8e8e8bf550dde35e06ce86689298d57256 Mon Sep 17 00:00:00 2001 From: olzzon Date: Tue, 12 Nov 2024 09:13:59 +0100 Subject: [PATCH 07/10] fix: rename new test --- packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts b/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts index 79c15ab8..94cafc1a 100644 --- a/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts +++ b/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts @@ -25,7 +25,7 @@ describe('Hot Standby Feature', () => { secondary: { id: 'secondary', host: '127.0.0.2', - openMediaHotStandby: true + isHotStandby: true } }); From 68fe627219d5c5aaa8a8f921c9260c1b999dbd39 Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Tue, 12 Nov 2024 10:54:24 +0100 Subject: [PATCH 08/10] chore: fix unit test - remove deprecated method --- packages/connector/src/MosDevice.ts | 13 ------- .../src/__tests__/MosConnection.spec.ts | 37 ++++++++++++------- packages/connector/src/__tests__/lib.ts | 16 ++++++++ 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/packages/connector/src/MosDevice.ts b/packages/connector/src/MosDevice.ts index babf1364..ab493dd1 100644 --- a/packages/connector/src/MosDevice.ts +++ b/packages/connector/src/MosDevice.ts @@ -231,19 +231,6 @@ export class MosDevice implements IMOSDevice { return this._secondaryConnection ? this._secondaryConnection.id : null } - /** @deprecated This is for unit tests only */ - get connections(): { - primary: NCSServerConnection | null - secondary: NCSServerConnection | null - current: NCSServerConnection | null - } { - return { - primary: this._primaryConnection, - secondary: this._secondaryConnection, - current: this._currentConnection, - } - } - connect(): void { if (this._primaryConnection) this._primaryConnection.connect() if (this._secondaryConnection) this._secondaryConnection.connect() diff --git a/packages/connector/src/__tests__/MosConnection.spec.ts b/packages/connector/src/__tests__/MosConnection.spec.ts index ae5ba427..a3a0d516 100644 --- a/packages/connector/src/__tests__/MosConnection.spec.ts +++ b/packages/connector/src/__tests__/MosConnection.spec.ts @@ -1,5 +1,15 @@ /* eslint-disable @typescript-eslint/unbound-method */ -import { clearMocks, decode, delay, encode, getMessageId, getXMLReply, initMosConnection, setupMocks } from './lib' +import { + clearMocks, + decode, + delay, + encode, + getConnectionsFromDevice, + getMessageId, + getXMLReply, + initMosConnection, + setupMocks, +} from './lib' import { SocketMock } from '../__mocks__/socket' import { ServerMock } from '../__mocks__/server' import { xmlData, xmlApiData } from '../__mocks__/testData' @@ -569,10 +579,11 @@ describe('MosDevice: General', () => { expect(mosDevice).toBeTruthy() expect(mosDevice.idPrimary).toEqual('jestMOS_primary') - expect(mosDevice.connections.primary).toBeTruthy() - expect(mosDevice.connections.secondary).toBeTruthy() - mosDevice.connections.primary?.setAutoReconnectInterval(300) - mosDevice.connections.secondary?.setAutoReconnectInterval(300) + const connections = getConnectionsFromDevice(mosDevice) + expect(connections.primary).toBeTruthy() + expect(connections.secondary).toBeTruthy() + connections.primary?.setAutoReconnectInterval(300) + connections.secondary?.setAutoReconnectInterval(300) const onConnectionChange = jest.fn() mosDevice.onConnectionChange((connectionStatus: IMOSConnectionStatus) => { @@ -606,15 +617,15 @@ describe('MosDevice: General', () => { expect(mosDevice.getConnectionStatus()).toMatchObject({ PrimaryConnected: true, PrimaryStatus: 'Primary: Connected', - SecondaryConnected: true, // Is a hot standby, so we pretend that it is connected - SecondaryStatus: 'Secondary: Is hot Standby', + SecondaryConnected: false, // This is expected behaviour from a hot standby - we leave it up to the library consumer to decide if this is bad or not + SecondaryStatus: 'Secondary: No heartbeats on port query', }) expect(onConnectionChange).toHaveBeenCalled() expect(onConnectionChange).toHaveBeenLastCalledWith({ PrimaryConnected: true, PrimaryStatus: 'Primary: Connected', - SecondaryConnected: true, // Is a hot standby, so we pretend that it is connected - SecondaryStatus: 'Secondary: Is hot Standby', + SecondaryConnected: false, // This is expected from a hot standby + SecondaryStatus: 'Secondary: No heartbeats on port query', }) onConnectionChange.mockClear() @@ -661,15 +672,15 @@ describe('MosDevice: General', () => { expect(mosDevice.getConnectionStatus()).toMatchObject({ PrimaryConnected: true, PrimaryStatus: 'Primary: Connected', - SecondaryConnected: true, // Is a hot standby, so we pretend that it is connected - SecondaryStatus: 'Secondary: Is hot Standby', + SecondaryConnected: false, // This is expected from a hot standby + SecondaryStatus: 'Secondary: No heartbeats on port query', }) expect(onConnectionChange).toHaveBeenCalled() expect(onConnectionChange).toHaveBeenLastCalledWith({ PrimaryConnected: true, PrimaryStatus: 'Primary: Connected', - SecondaryConnected: true, // Is a hot standby, so we pretend that it is connected - SecondaryStatus: 'Secondary: Is hot Standby', + SecondaryConnected: false, // This is expected from a hot standby + SecondaryStatus: 'Secondary: No heartbeats on port query', }) await mos.dispose() diff --git a/packages/connector/src/__tests__/lib.ts b/packages/connector/src/__tests__/lib.ts index a48ad19d..c7808c57 100644 --- a/packages/connector/src/__tests__/lib.ts +++ b/packages/connector/src/__tests__/lib.ts @@ -9,6 +9,7 @@ import { Socket, Server } from 'net' import { xml2js } from 'xml-js' import * as iconv from 'iconv-lite' +import { NCSServerConnection } from '../connection/NCSServerConnection' iconv.encodingExists('utf16-be') // breaks net.Server, disabled for now @@ -284,3 +285,18 @@ function fixSnapshotInner(data: any): [boolean, any] { } return [changed, data] } + +export function getConnectionsFromDevice(device: MosDevice): { + primary: NCSServerConnection | null + secondary: NCSServerConnection | null + current: NCSServerConnection | null +} { + return { + // @ts-expect-error private property + primary: device._primaryConnection, + // @ts-expect-error private property + secondary: device._secondaryConnection, + // @ts-expect-error private property + current: device._currentConnection, + } +} From 4e9f71924f7d0e6cd011217fa7dc18efc1485e1e Mon Sep 17 00:00:00 2001 From: Mint de Wit Date: Tue, 12 Nov 2024 10:54:32 +0100 Subject: [PATCH 09/10] chore: update doc --- packages/connector/src/api.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/connector/src/api.ts b/packages/connector/src/api.ts index d1602fa8..167ba8ef 100644 --- a/packages/connector/src/api.ts +++ b/packages/connector/src/api.ts @@ -666,7 +666,9 @@ export interface IMOSDeviceConnectionOptions { /** (Optional) Treat the secondary server as a "hot standby". * A "hot standby" is a server that is powered down / in standby while the primary server is up. - * Set this to true to defer checking the connection status to the secondary server while the primary is up. + * When a server is a hot standby it is expected to be disconnected and hence we will not send + * heartbeat messages. The connection status will still be reported as disconnected however as we + * do not pretend to be connected to something that is powered down. * * (This was added to support the hot standby feature of OpenMedia NRCS.) */ From 562d74badae2a17feceddbe43b590390c7ac8015 Mon Sep 17 00:00:00 2001 From: olzzon Date: Thu, 21 Nov 2024 14:29:38 +0100 Subject: [PATCH 10/10] fix: revert renaming to isHotStandby to avoid conflict with MOS-Gateway --- packages/connector/src/MosConnection.ts | 4 ++-- packages/connector/src/__tests__/MosConnection.spec.ts | 2 +- packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts | 2 +- packages/connector/src/api.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/connector/src/MosConnection.ts b/packages/connector/src/MosConnection.ts index 605d0847..3db18111 100644 --- a/packages/connector/src/MosConnection.ts +++ b/packages/connector/src/MosConnection.ts @@ -174,10 +174,10 @@ export class MosConnection extends EventEmitter implements false ) } - // Handle that .isHotStandby should not check for heartbeats on + // Handle that .openMediaHotStandby should not check for heartbeats on // the secondary connection when the primary is connected // And disable heartbeats on the primary when the primary is disconnected - if (connectionOptions.secondary?.isHotStandby) { + if (connectionOptions.secondary?.openMediaHotStandby) { // Initially disable heartbeats on secondary since primary should be attempted first secondary.disableHeartbeats() diff --git a/packages/connector/src/__tests__/MosConnection.spec.ts b/packages/connector/src/__tests__/MosConnection.spec.ts index a3a0d516..832e6445 100644 --- a/packages/connector/src/__tests__/MosConnection.spec.ts +++ b/packages/connector/src/__tests__/MosConnection.spec.ts @@ -572,7 +572,7 @@ describe('MosDevice: General', () => { id: 'secondary', host: '192.168.0.2', timeout: 200, - isHotStandby: true, + openMediaHotStandby: true, }, }) diff --git a/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts b/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts index 94cafc1a..79c15ab8 100644 --- a/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts +++ b/packages/connector/src/__tests__/OpenMediaHotStandby.spec.ts @@ -25,7 +25,7 @@ describe('Hot Standby Feature', () => { secondary: { id: 'secondary', host: '127.0.0.2', - isHotStandby: true + openMediaHotStandby: true } }); diff --git a/packages/connector/src/api.ts b/packages/connector/src/api.ts index 167ba8ef..10df731a 100644 --- a/packages/connector/src/api.ts +++ b/packages/connector/src/api.ts @@ -672,6 +672,6 @@ export interface IMOSDeviceConnectionOptions { * * (This was added to support the hot standby feature of OpenMedia NRCS.) */ - isHotStandby?: boolean + openMediaHotStandby?: boolean } }