From 0353c37f2d8159b0569bafd20029d3b156b74e00 Mon Sep 17 00:00:00 2001 From: Richard Cartwright Date: Thu, 4 Jul 2019 17:31:57 +0100 Subject: [PATCH] feat: support for specifying more than one ISA system to connect to, round-robin on failure --- README.md | 20 +++++---- src/__tests__/test_framework.test.ts | 61 +++++++++++++++++++++++++++- src/index.ts | 29 ++++++++----- src/server.ts | 7 +++- 4 files changed, 97 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 58cc6c4..80082bd 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Build the typescript interface module: This package has automated tests that run with [jest](). Test with: - yarn test + yarn test ### Running @@ -55,7 +55,7 @@ This will start a [nodemon](https://nodemon.io/) watch on the source files and r For production use, a simple server can be run with: - yarn server + yarn server These servers listen on port `3000` by default. @@ -68,7 +68,7 @@ Experiment from the REPL with: Import into an external project with: import { Quantel } from 'tv-automation-quantel-gateway' - const { Quantel } = require('tv-autonation-quantel-gateway') + const { Quantel } = require('tv-automation-quantel-gateway') See the [walkthrough for how to do playback](./doc/plyout_walkthrough.md) with this module as a Node.js API. @@ -82,17 +82,20 @@ To connect to an ISA system on a different host, POST to: /connect/:address -The `:address` should consist of a DNS name or IP address and, optionally, a port number (defaults to 2096), where the CORBA Interoperable Object Reference (IOR) of the Quantel ISA is advertised. For example: +ISA systems are normally deployed in _master_ and _slave_ pairs. The `:address` should consist of a comma separated list of DNS names or IP addresses and, optionally, with port number (defaults to 2096), where the CORBA Interoperable Object Reference (IOR) of the Quantel ISA(s) is/are advertised. For example: - /connect/isa.national.ztv.com:3737 + /connect/isa-master.national.ztv.com:3737,isa-slave.national.ztv.com:3737 A successful request produces a JSON response with the discovered IOR (`isaIOR`) and ISA endpoint address (`href`). Subsequently, the currently configured connection can be queried with a GET request to `/connect`. ### Topology of a Quantel system -Paths are all of the form ... +In general, paths are all of the form ... /:zoneID/server/:serverID/port/:portID + /:zoneID/clip/:clipID(/fragments) + /:zoneID/format/:formatID + /:zoneID/copy/:copyID In most cases and in the current implementation, `:zoneID` is the `default` local zone at the ISA that the gateway connects to. @@ -100,7 +103,10 @@ Types are: * `:zoneID` - zone number or `default`; * `:serverID` - integer number or the string name of the server; -* `:portID` - string name of the port. +* `:portID` - string name of the port; +* `:clipID` - identifier for a clip within a zone; +* `:formatID` - identifier for a specific media format; +* `:copyID` - identifier for a new clip that is a copy of an existing clip. A GET request to the root path `/` lists all available zones. diff --git a/src/__tests__/test_framework.test.ts b/src/__tests__/test_framework.test.ts index a43ac75..bf3a8e0 100644 --- a/src/__tests__/test_framework.test.ts +++ b/src/__tests__/test_framework.test.ts @@ -104,7 +104,7 @@ describe('Error handling when no server running', () => { test('Test failed IOR HTTP connection', async () => { expect.assertions(1) - await expect(Quantel.testConnection()).rejects.toThrow('CORBA subsystem: TRANSIENT') + await expect(Quantel.getServers()).rejects.toThrow('CORBA subsystem: TRANSIENT') }) test('Test fail to get servers 1', async () => { @@ -165,3 +165,62 @@ describe('Error handling when server has failed', () => { await spawn.stop() }) }) + +describe('Error handling when server has failed, two servers', () => { + + let isaIOR: string + + beforeAll(async () => { + isaIOR = await spawn.start() + }) + + test('Default get connection reference and close', async () => { + await expect(Quantel.getISAReference(['http://127.0.0.1:2096', 'http://localhost:2096' ])) + .resolves.toStrictEqual({ + type: 'ConnectionDetails', + href: 'http://127.0.0.1:2096', + isaIOR, + refs: [ 'http://127.0.0.1:2096', 'http://localhost:2096' ], + robin: 2 } as Quantel.ConnectionDetails) + }) + + test('Stopping server', async () => { + await expect(spawn.stop()).resolves.toBeUndefined() + }) + + test('Test fail to get servers 1', async () => { + expect.assertions(1) + await expect(Quantel.getServers()).rejects.toThrow('CORBA subsystem: TRANSIENT') + }) + + test('Test the failed CORBA connection 1', async () => { + expect.assertions(1) + await expect(Quantel.testConnection()).rejects.toThrow('ECONNREFUSED') + }) + + test('Test the failed CORBA connection 2', async () => { + expect.assertions(1) + await expect(Quantel.testConnection()).rejects.toThrow('ECONNREFUSED') + }) + + test('Restart server and get details', async () => { + isaIOR = await spawn.start() + // await new Promise((resolve) => setTimeout(() => resolve(), 1000)) + await expect(Quantel.testConnection()).resolves.toBe('PONG!') + await expect(Quantel.getISAReference()).resolves.toStrictEqual({ + type: 'ConnectionDetails', + href: 'http://localhost:2096', + isaIOR, + refs: [ 'http://127.0.0.1:2096', 'http://localhost:2096' ], + robin: 5 } as Quantel.ConnectionDetails) + }) + + test('Check that get servers now works', async () => { + await expect(Quantel.getServers()).resolves.toBeTruthy() + }) + + afterAll(async () => { + Quantel.destroyOrb() + await spawn.stop() + }) +}) diff --git a/src/index.ts b/src/index.ts index a83712b..2730ee4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -394,6 +394,11 @@ export namespace Quantel { } } + function resetConnection () { + isaIOR = null + robin++ + } + export async function getISAReference (ref?: string | string[], count?: number): Promise { if (typeof ref === 'string') ref = [ ref ] let myCount: number = count ? count + 1 : 1 @@ -403,6 +408,7 @@ export namespace Quantel { isaIOR = isaIOR.then(x => x, (): Promise => new Promise((resolve, reject) => { if (stickyRef[index].endsWith('/')) { stickyRef[index] = stickyRef[index].slice(0, -1) } if (stickyRef[index].indexOf(':') < 0) { stickyRef[index] = stickyRef[index] + ':2096' } + console.log('About to request', index, stickyRef[index]) request({ uri: stickyRef[index] + '/ZoneManager.ior', resolveWithFullResponse: true @@ -415,16 +421,20 @@ export namespace Quantel { `HTTP request for ISA IOR failed with status ${res.statusCode}: ${res.statusMessage}`, res.statusCode)) } else { - robin++ - getISAReference(undefined, myCount).then(x => resolve(x.isaIOR), reject) + console.log('Got in here 1') + resetConnection() + getISAReference(undefined, myCount).catch(reject) + resolve(isaIOR as Promise) } } }, err => { if (myCount >= stickyRef.length) { reject(err) } else { - robin++ - getISAReference(undefined, myCount).then(x => resolve(x.isaIOR), reject) + console.log('Got in here 2') + resetConnection() + getISAReference(undefined, myCount).catch(reject) + resolve(isaIOR as Promise) } }) })) @@ -437,11 +447,6 @@ export namespace Quantel { } } - function resetConnection () { - isaIOR = null - robin++ - } - export function destroyOrb () { quantel.destroyOrb() } @@ -459,11 +464,13 @@ export namespace Quantel { // Resolves to 'PONG!' on success, otherwise rejects with a connection error export async function testConnection (): Promise { try { + console.log('Testing connection', robin) await getISAReference() return await quantel.testConnection(await isaIOR) } catch (err) { - if (err.message.indexOf('TRANSIENT') >= 0) { resetConnection() } - if (err.message.indexOf('OBJECT_NOT_EXIST') >= 0) { + // if (err.message.indexOf('TRANSIENT') >= 0) { resetConnection() } + if (err.message.indexOf('OBJECT_NOT_EXIST') >= 0 || + err.message.indexOf('TRANSIENT') >= 0) { resetConnection() return testConnection() } diff --git a/src/server.ts b/src/server.ts index 50153a7..c780d0e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -60,7 +60,12 @@ router.post('/connect/:addr', async (ctx) => { if (ctx.params.addr.indexOf(':') < 0) { ctx.params.addr += ':2096' } - ctx.body = await Quantel.getISAReference(`http://${ctx.params.addr}`) + if (ctx.params.addr.indexOf(',') >= 0) { + ctx.body = await Quantel.getISAReference( + ctx.params.addr.split(',').map((x: string) => `http://${x}`)) + } else { + ctx.body = await Quantel.getISAReference(`http://${ctx.params.addr}`) + } } catch (err) { if (err.message.indexOf('ENOTFOUND') >= 0) { err.status = 404