From e688d227cc947594a1e8be5893c3155db225c1c0 Mon Sep 17 00:00:00 2001 From: the-flagship <114525744+the-flagship@users.noreply.github.com> Date: Fri, 7 Apr 2023 13:30:06 -0700 Subject: [PATCH 1/2] Introduce StaticNodeRegistry This also renames NodeRegistryImpl to DynamicNodeRegistry. The new static version isn't being used just yet. --- src/service-worker/main.ts | 4 +- src/service-worker/src/armada/registry.ts | 16 ++++++- src/service-worker/test/armada/happy_spec.ts | 26 +++++------ .../test/armada/registry_spec.ts | 45 +++++++++++++++---- 4 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/service-worker/main.ts b/src/service-worker/main.ts index 2bfa31d..a235601 100644 --- a/src/service-worker/main.ts +++ b/src/service-worker/main.ts @@ -9,7 +9,7 @@ import {Adapter} from './src/adapter'; import {ArmadaAPIClientImpl, HTTPProtocol} from './src/armada/api'; import {ArmadaDriver as Driver} from './src/armada/driver'; -import {NodeRegistryImpl} from './src/armada/registry'; +import {DynamicNodeRegistry} from './src/armada/registry'; import {CacheDatabase} from './src/db-cache'; const scope = self as unknown as ServiceWorkerGlobalScope; @@ -26,5 +26,5 @@ const apiClient = new ArmadaAPIClientImpl( location.protocol as HTTPProtocol, projectId, ); -const registry = new NodeRegistryImpl(apiClient, bootstrapNodes, contentNodeRefreshIntervalMs); +const registry = new DynamicNodeRegistry(apiClient, bootstrapNodes, contentNodeRefreshIntervalMs); new Driver(scope, adapter, new CacheDatabase(adapter), registry, apiClient, scope.crypto.subtle); diff --git a/src/service-worker/src/armada/registry.ts b/src/service-worker/src/armada/registry.ts index c2d5b0a..d258fbc 100644 --- a/src/service-worker/src/armada/registry.ts +++ b/src/service-worker/src/armada/registry.ts @@ -6,7 +6,21 @@ export interface NodeRegistry { refreshNodesInterval(): void; } -export class NodeRegistryImpl implements NodeRegistry { +export class StaticNodeRegistry implements NodeRegistry { + constructor(protected contentNodes: string[]) {} + + public async allNodes(randomize: boolean): Promise { + const nodes = this.contentNodes.slice(); + if (randomize) { + shuffle(nodes); + } + return nodes; + } + + public refreshNodesInterval() {} +} + +export class DynamicNodeRegistry implements NodeRegistry { private contentNodes: string[] = []; private refreshPending: Promise|null = null; private updateTimer: ReturnType|null = null; diff --git a/src/service-worker/test/armada/happy_spec.ts b/src/service-worker/test/armada/happy_spec.ts index a7be380..ef7d7fd 100644 --- a/src/service-worker/test/armada/happy_spec.ts +++ b/src/service-worker/test/armada/happy_spec.ts @@ -15,7 +15,7 @@ import {webcrypto} from 'crypto'; import {ArmadaAPIClient, ArmadaAPIClientImpl} from '../../src/armada/api'; import {ArmadaDriver, ArmadaDriver as Driver} from '../../src/armada/driver'; -import {NodeRegistryImpl} from '../../src/armada/registry'; +import {DynamicNodeRegistry} from '../../src/armada/registry'; import {CacheDatabase} from '../../src/db-cache'; import {DriverReadyState} from '../../src/driver'; import {AssetGroupConfig, DataGroupConfig, Manifest} from '../../src/manifest'; @@ -297,7 +297,7 @@ describe('Driver', () => { scope = new SwTestHarnessBuilder().withServerState(server).build(); const apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver(scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); }); @@ -557,7 +557,7 @@ describe('Driver', () => { .withServerState(serverUpdate) .build(); const apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver(scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); @@ -616,7 +616,7 @@ describe('Driver', () => { .withServerState(serverUpdate) .build(); const apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver(scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); @@ -688,7 +688,7 @@ describe('Driver', () => { .withServerState(serverUpdate) .build(); const apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver(scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo'); @@ -734,7 +734,7 @@ describe('Driver', () => { // Simulate failing to load the stored state (and thus starting from an empty state). scope.caches.delete('db:control'); const apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver(scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); @@ -1193,7 +1193,7 @@ describe('Driver', () => { const newScope = new SwTestHarnessBuilder('http://localhost/foo/bar/').withServerState(server).build(); const apiClient = new ArmadaAPIClientImpl(newScope, newScope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); new Driver( newScope, newScope, new CacheDatabase(newScope), registry, apiClient, webcrypto.subtle); @@ -1271,7 +1271,7 @@ describe('Driver', () => { .withServerState(serverState) .build(); const apiClient = new ArmadaAPIClientImpl(newScope, newScope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); const newDriver = new Driver( newScope, newScope, new CacheDatabase(newScope), registry, apiClient, webcrypto.subtle); @@ -1398,7 +1398,7 @@ describe('Driver', () => { scope = new SwTestHarnessBuilder().withServerState(brokenServer).build(); (scope.registration as any).scope = 'http://site.com'; const apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver(scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); @@ -1618,7 +1618,7 @@ describe('Driver', () => { // Create initial server state and initialize the SW. scope = new SwTestHarnessBuilder().withServerState(originalServer).build(); let apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - let registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + let registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver( scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); @@ -1637,7 +1637,7 @@ describe('Driver', () => { .withServerState(updatedServer) .build(); apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver( scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); @@ -1671,7 +1671,7 @@ describe('Driver', () => { // Create initial server state and initialize the SW. scope = new SwTestHarnessBuilder().withServerState(originalServer).build(); const apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); driver = new Driver( scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); @@ -1746,7 +1746,7 @@ describe('Driver', () => { const server = serverBuilderBase.withManifest(freshnessManifest).build(); const scope = new SwTestHarnessBuilder().withServerState(server).build(); const apiClient = new ArmadaAPIClientImpl(scope, scope, 'http:', TEST_PROJECT_ID); - const registry = new NodeRegistryImpl(apiClient, [TEST_BOOTSTRAP_NODE], 10000); + const registry = new DynamicNodeRegistry(apiClient, [TEST_BOOTSTRAP_NODE], 10000); const driver = new Driver(scope, scope, new CacheDatabase(scope), registry, apiClient, webcrypto.subtle); diff --git a/src/service-worker/test/armada/registry_spec.ts b/src/service-worker/test/armada/registry_spec.ts index 304c42c..6e6dc0a 100644 --- a/src/service-worker/test/armada/registry_spec.ts +++ b/src/service-worker/test/armada/registry_spec.ts @@ -1,5 +1,5 @@ import {NodesResponse} from '../../src/armada/api'; -import {NodeRegistryImpl} from '../../src/armada/registry'; +import {DynamicNodeRegistry, StaticNodeRegistry} from '../../src/armada/registry'; class StaticAPIClient { public count: number; @@ -35,12 +35,39 @@ class MultiHostStaticAPIClient { } } -describe('NodeRegistryImpl', () => { +describe('StaticNodeRegistry', () => { + describe('returns content nodes', () => { + it('when allNodes() is called', async () => { + const nodes = ['content0', 'content1']; + const registry = new StaticNodeRegistry(nodes); + expect(await registry.allNodes(false)).toEqual(nodes); + }); + + it('randomizes the returned nodes when specified', async () => { + const nodesArr = [...Array(20).keys()].map(i => `content${i}`); + const nodesSet = new Set(nodesArr); + const registry = new StaticNodeRegistry(nodesArr); + + let foundShuffled = false; + for (let i = 0; i < 100 && !foundShuffled; i++) { + const got = await registry.allNodes(true); + expect(got.length).toEqual(nodesArr.length); + expect(new Set(got)).toEqual(nodesSet); + if (!arraysMatch(got, nodesArr)) { + foundShuffled = true; + } + } + expect(foundShuffled).toBeTrue(); + }); + }); +}); + +describe('DynamicNodeRegistry', () => { describe('populates content nodes', () => { it('when allNodes() is called', async () => { const nodes = ['content0', 'content1']; const apiClient = new StaticAPIClient(nodes); - const registry = new NodeRegistryImpl(apiClient, ['topology'], 10000); + const registry = new DynamicNodeRegistry(apiClient, ['topology'], 10000); expect(await registry.allNodes(false)).toEqual(nodes); }); @@ -93,7 +120,7 @@ describe('NodeRegistryImpl', () => { for (let tc of cases) { it(tc.name, async () => { const apiClient = new MultiHostStaticAPIClient(tc.topologyData); - const registry = new NodeRegistryImpl(apiClient, Object.keys(tc.topologyData), 10000); + const registry = new DynamicNodeRegistry(apiClient, Object.keys(tc.topologyData), 10000); expect(await registry.allNodes(false)).toEqual(tc.want); }); } @@ -136,7 +163,7 @@ describe('NodeRegistryImpl', () => { for (let tc of cases) { it(tc.name, async () => { const apiClient = new MultiHostStaticAPIClient(tc.topologyData); - const registry = new NodeRegistryImpl(apiClient, Object.keys(tc.topologyData), 10000); + const registry = new DynamicNodeRegistry(apiClient, Object.keys(tc.topologyData), 10000); await expectAsync(registry.allNodes(false)).toBeRejected(); }); } @@ -144,12 +171,12 @@ describe('NodeRegistryImpl', () => { describe('content node cache', () => { let apiClient: StaticAPIClient; - let registry: NodeRegistryImpl; + let registry: DynamicNodeRegistry; const nodes = ['content0', 'content1']; beforeEach(async () => { apiClient = new StaticAPIClient(nodes); - registry = new NodeRegistryImpl(apiClient, ['topology'], 10000); + registry = new DynamicNodeRegistry(apiClient, ['topology'], 10000); }); it('will hit once populated', async () => { @@ -188,7 +215,7 @@ describe('NodeRegistryImpl', () => { return {hosts: ['content0', 'content1']}; }, }; - const registry = new NodeRegistryImpl(waitingAPIClient, ['topology0'], 10000); + const registry = new DynamicNodeRegistry(waitingAPIClient, ['topology0'], 10000); const fetch1 = registry.allNodes(false); const fetch2 = registry.allNodes(false); @@ -205,7 +232,7 @@ describe('NodeRegistryImpl', () => { const nodesArr = [...Array(20).keys()].map(i => `content${i}`); const nodesSet = new Set(nodesArr); const apiClient = new StaticAPIClient(nodesArr); - const registry = new NodeRegistryImpl(apiClient, ['topology'], 10000); + const registry = new DynamicNodeRegistry(apiClient, ['topology'], 10000); let foundShuffled = false; for (let i = 0; i < 100 && !foundShuffled; i++) { From f4c29e56d368a9d2781b86a534e2b5f157358b74 Mon Sep 17 00:00:00 2001 From: the-flagship <114525744+the-flagship@users.noreply.github.com> Date: Mon, 10 Apr 2023 11:49:34 -0700 Subject: [PATCH 2/2] Choose registry type based on env vars This allows StaticNodeRegistry to be used by setting the CONTENT_NODES environment variable as opposed to BOOTSTRAP_NODES, which will still use DynamicNodeRegistry if set. --- rollup.config.js | 1 + src/service-worker/main.ts | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index 1124421..ffc7173 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -14,6 +14,7 @@ export default { values: { 'process.env.BOOTSTRAP_NODES': JSON.stringify('{{.BootstrapNodes}}'), 'process.env.CONTENT_NODE_REFRESH_INTERVAL_MS': 60 * 60 * 1000, // 1 hour + 'process.env.CONTENT_NODES': JSON.stringify('{{.ContentNodes}}'), 'process.env.PROJECT_ID': JSON.stringify('{{.ProjectID}}'), }, preventAssignment: true, diff --git a/src/service-worker/main.ts b/src/service-worker/main.ts index a235601..37dc9d1 100644 --- a/src/service-worker/main.ts +++ b/src/service-worker/main.ts @@ -9,14 +9,19 @@ import {Adapter} from './src/adapter'; import {ArmadaAPIClientImpl, HTTPProtocol} from './src/armada/api'; import {ArmadaDriver as Driver} from './src/armada/driver'; -import {DynamicNodeRegistry} from './src/armada/registry'; +import {DynamicNodeRegistry, NodeRegistry, StaticNodeRegistry} from './src/armada/registry'; import {CacheDatabase} from './src/db-cache'; const scope = self as unknown as ServiceWorkerGlobalScope; +const envContentNodes = process.env.CONTENT_NODES as string; +const contentNodes = (envContentNodes.trim() !== '') ? envContentNodes.trim().split(',') : []; + const envBootstrapNodes = process.env.BOOTSTRAP_NODES as string; -const bootstrapNodes = envBootstrapNodes.split(','); +const bootstrapNodes = (envBootstrapNodes.trim() !== '') ? envBootstrapNodes.trim().split(',') : []; + const contentNodeRefreshIntervalMs = Number(process.env.CONTENT_NODE_REFRESH_INTERVAL_MS); + const projectId = process.env.PROJECT_ID as string; const adapter = new Adapter(scope.registration.scope, self.caches); @@ -26,5 +31,15 @@ const apiClient = new ArmadaAPIClientImpl( location.protocol as HTTPProtocol, projectId, ); -const registry = new DynamicNodeRegistry(apiClient, bootstrapNodes, contentNodeRefreshIntervalMs); + +let registry: NodeRegistry; +if (bootstrapNodes.length) { + registry = new DynamicNodeRegistry(apiClient, bootstrapNodes, contentNodeRefreshIntervalMs); +} else if (contentNodes.length) { + registry = new StaticNodeRegistry(contentNodes); +} else { + throw new Error( + 'Can\'t initialize node registry: must set env.CONTENT_NODES or env.BOOTSTRAP_NODES'); +} + new Driver(scope, adapter, new CacheDatabase(adapter), registry, apiClient, scope.crypto.subtle);