Skip to content

Commit 11ec673

Browse files
feat(dvm): add dvm worker registry settings and process topology (#721)
* feat(dvm): add dvm worker registry settings Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> * feat(dvm): wire WORKER_TYPE=dvm-orchestrator process topology Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> * test(dvm): add dvm-orchestrator-worker unit tests Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> * docs: document dvm.workers settings Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> --------- Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com> Co-authored-by: Ricardo Cabral <me@ricardocabral.io>
1 parent cf5ea4f commit 11ec673

10 files changed

Lines changed: 205 additions & 1 deletion

.changeset/dvm-worker-registry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
feat(dvm): add worker registry settings and dvm-orchestrator process topology

CONFIGURATION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ The settings below are listed in alphabetical order by name. Please keep this ta
133133

134134
| Name | Description |
135135
|---------------------------------------------|-------------------------------------------------------------------------------|
136+
| dvm.workers[].args | Arguments passed to the spawned command. Optional. |
137+
| dvm.workers[].command | Command to spawn for this DVM worker (e.g. an interpreter or executable path). |
138+
| dvm.workers[].kinds | NIP-90 job request kinds (5000-5999) this worker accepts. Optional. |
139+
| dvm.workers[].timeoutMs | Max time in ms to wait for a job result before considering it timed out. Optional. |
136140
| info.banner | Public banner image URL for the relay information document. |
137141
| info.contact | Relay operator's contact. (e.g. mailto:operator@relay-your-domain.com) |
138142
| info.description | Public description of your relay. (e.g. Toronto Bitcoin Group Public Relay) |

resources/default-settings.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ workers:
107107
count: 0
108108
mirroring:
109109
static: []
110+
dvm:
111+
workers: []
110112
limits:
111113
# strategy selection configuration for rate limiting:
112114
rateLimiter:

src/@types/settings.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,21 @@ export interface Mirroring {
246246
static?: Mirror[]
247247
}
248248

249+
export interface DvmWorker {
250+
/** Command to spawn for this worker (e.g. an interpreter or executable path). */
251+
command: string
252+
/** Arguments passed to the spawned command. */
253+
args?: string[]
254+
/** NIP-90 job request kinds (5000-5999) this worker accepts. */
255+
kinds?: number[]
256+
/** Max time in ms to wait for a job result before considering it timed out. */
257+
timeoutMs?: number
258+
}
259+
260+
export interface Dvm {
261+
workers?: DvmWorker[]
262+
}
263+
249264
export type Nip05Mode = 'enabled' | 'passive' | 'disabled'
250265

251266
export interface Nip45Settings {
@@ -330,6 +345,7 @@ export interface Settings {
330345
workers?: Worker
331346
limits?: Limits
332347
mirroring?: Mirroring
348+
dvm?: Dvm
333349
nip05?: Nip05Settings
334350
nip42?: Nip42Settings
335351
nip43?: Nip43Settings

src/app/app.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,18 @@ export class App implements IRunnable {
108108
logCentered(`${mirrors.length} static-mirroring worker started`, width)
109109
}
110110

111+
const dvmWorkers = settings?.dvm?.workers
112+
113+
if (Array.isArray(dvmWorkers) && dvmWorkers.length) {
114+
for (let i = 0; i < dvmWorkers.length; i++) {
115+
createWorker({
116+
WORKER_TYPE: 'dvm-orchestrator',
117+
DVM_WORKER_INDEX: i.toString(),
118+
})
119+
}
120+
logCentered(`${dvmWorkers.length} dvm-orchestrator worker started`, width)
121+
}
122+
111123
logger('settings: %O', settings)
112124

113125
const host = `${hostname()}:${port}`

src/app/dvm-orchestrator-worker.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { path } from 'ramda'
2+
import { IRunnable } from '../@types/base'
3+
import { DvmWorker, Settings } from '../@types/settings'
4+
import { createLogger } from '../factories/logger-factory'
5+
import { shutdownMetricsTelemetry } from '../telemetry/metrics'
6+
7+
const logger = createLogger('dvm-orchestrator-worker')
8+
9+
export class DvmOrchestratorWorker implements IRunnable {
10+
private config: DvmWorker | undefined
11+
12+
public constructor(
13+
private readonly process: NodeJS.Process,
14+
private readonly settings: () => Settings,
15+
) {
16+
this.process
17+
.on('SIGINT', this.onExit.bind(this))
18+
.on('SIGHUP', this.onExit.bind(this))
19+
.on('SIGTERM', this.onExit.bind(this))
20+
.on('uncaughtException', this.onError.bind(this))
21+
.on('unhandledRejection', this.onError.bind(this))
22+
}
23+
24+
public run(): void {
25+
const currentSettings = this.settings()
26+
27+
this.config = path(['dvm', 'workers', this.process.env.DVM_WORKER_INDEX], currentSettings) as DvmWorker | undefined
28+
29+
if (!this.config) {
30+
logger.error('no dvm worker config found for index %s', this.process.env.DVM_WORKER_INDEX)
31+
this.process.exit(1)
32+
return
33+
}
34+
35+
logger.info('dvm-orchestrator worker started for command: %s', this.config.command)
36+
}
37+
38+
private onError(error: Error) {
39+
logger('error: %o', error)
40+
throw error
41+
}
42+
43+
private onExit() {
44+
logger('exiting')
45+
void shutdownMetricsTelemetry().finally(() => {
46+
this.close(() => {
47+
this.process.exit(0)
48+
})
49+
})
50+
}
51+
52+
public close(callback?: () => void) {
53+
logger('closing')
54+
if (typeof callback === 'function') {
55+
callback()
56+
}
57+
}
58+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import process from 'process'
2+
import { DvmOrchestratorWorker } from '../app/dvm-orchestrator-worker'
3+
import { createSettings } from './settings-factory'
4+
5+
export const dvmOrchestratorWorkerFactory = () => {
6+
return new DvmOrchestratorWorker(process, createSettings)
7+
}

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import cluster from 'cluster'
22

33
import { appFactory } from './factories/app-factory'
4+
import { dvmOrchestratorWorkerFactory } from './factories/dvm-orchestrator-worker-factory'
45
import { maintenanceWorkerFactory } from './factories/maintenance-worker-factory'
56
import { staticMirroringWorkerFactory } from './factories/static-mirroring.worker-factory'
6-
import { initializeMetricsTelemetry } from './telemetry/metrics'
77
import { workerFactory } from './factories/worker-factory'
8+
import { initializeMetricsTelemetry } from './telemetry/metrics'
89

910
export const getRunner = () => {
1011
if (cluster.isPrimary) {
@@ -17,6 +18,8 @@ export const getRunner = () => {
1718
return maintenanceWorkerFactory()
1819
case 'static-mirroring':
1920
return staticMirroringWorkerFactory()
21+
case 'dvm-orchestrator':
22+
return dvmOrchestratorWorkerFactory()
2023
default:
2124
throw new Error(`Unknown worker: ${process.env.WORKER_TYPE}`)
2225
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import chai from 'chai'
2+
import EventEmitter from 'events'
3+
import Sinon from 'sinon'
4+
import sinonChai from 'sinon-chai'
5+
6+
import { Settings } from '../../../src/@types/settings'
7+
import { DvmOrchestratorWorker } from '../../../src/app/dvm-orchestrator-worker'
8+
import * as metricsTelemetry from '../../../src/telemetry/metrics'
9+
10+
chai.use(sinonChai)
11+
12+
const { expect } = chai
13+
14+
describe('DvmOrchestratorWorker', () => {
15+
let sandbox: Sinon.SinonSandbox
16+
let fakeProcess: EventEmitter & { exit: Sinon.SinonStub; env: Record<string, string> }
17+
let settings: Sinon.SinonStub
18+
let settingsState: Settings
19+
20+
beforeEach(() => {
21+
sandbox = Sinon.createSandbox()
22+
23+
fakeProcess = Object.assign(new EventEmitter(), {
24+
exit: sandbox.stub(),
25+
env: {},
26+
}) as EventEmitter & { exit: Sinon.SinonStub; env: Record<string, string> }
27+
28+
settingsState = {
29+
dvm: {
30+
workers: [{ command: 'python3', args: ['worker.py'] }],
31+
},
32+
} as any
33+
34+
settings = sandbox.stub().callsFake(() => settingsState)
35+
36+
sandbox.stub(metricsTelemetry, 'shutdownMetricsTelemetry').resolves()
37+
})
38+
39+
afterEach(() => {
40+
sandbox.restore()
41+
})
42+
43+
describe('run', () => {
44+
it('logs startup for the worker config at DVM_WORKER_INDEX', () => {
45+
fakeProcess.env.DVM_WORKER_INDEX = '0'
46+
const worker = new DvmOrchestratorWorker(fakeProcess as any, settings as any)
47+
48+
expect(() => worker.run()).to.not.throw()
49+
expect(fakeProcess.exit).not.to.have.been.called
50+
})
51+
52+
it('exits with code 1 if no worker config exists for the given index', () => {
53+
fakeProcess.env.DVM_WORKER_INDEX = '5'
54+
const worker = new DvmOrchestratorWorker(fakeProcess as any, settings as any)
55+
56+
worker.run()
57+
58+
expect(fakeProcess.exit).to.have.been.calledWith(1)
59+
})
60+
})
61+
62+
describe('signal handling', () => {
63+
it('closes and exits on SIGTERM', async () => {
64+
fakeProcess.env.DVM_WORKER_INDEX = '0'
65+
const worker = new DvmOrchestratorWorker(fakeProcess as any, settings as any)
66+
worker.run()
67+
68+
fakeProcess.emit('SIGTERM')
69+
70+
await Promise.resolve()
71+
await Promise.resolve()
72+
73+
expect(fakeProcess.exit).to.have.been.calledWith(0)
74+
})
75+
})
76+
77+
describe('close', () => {
78+
it('invokes the callback', () => {
79+
const worker = new DvmOrchestratorWorker(fakeProcess as any, settings as any)
80+
const callback = sandbox.stub()
81+
82+
worker.close(callback)
83+
84+
expect(callback).to.have.been.calledOnce
85+
})
86+
})
87+
})
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { expect } from 'chai'
2+
3+
import { DvmOrchestratorWorker } from '../../../src/app/dvm-orchestrator-worker'
4+
import { dvmOrchestratorWorkerFactory } from '../../../src/factories/dvm-orchestrator-worker-factory'
5+
6+
describe('dvmOrchestratorWorkerFactory', () => {
7+
it('returns a DvmOrchestratorWorker', () => {
8+
expect(dvmOrchestratorWorkerFactory()).to.be.an.instanceOf(DvmOrchestratorWorker)
9+
})
10+
})

0 commit comments

Comments
 (0)