feat(server): cluster level event system (#9884)

This commit is contained in:
forehalo
2025-01-25 14:51:03 +00:00
parent 0d2c2ea21e
commit 6370f45928
43 changed files with 634 additions and 364 deletions
@@ -0,0 +1,62 @@
import { INestApplication } from '@nestjs/common';
import ava, { TestFn } from 'ava';
import Sinon from 'sinon';
import { EventBus } from '../../base';
import { SocketIoAdapter } from '../../base/websocket';
import { createTestingModule } from '../utils';
import { Listeners } from './provider';
const test = ava as TestFn<{
app1: INestApplication;
app2: INestApplication;
}>;
async function createApp() {
const m = await createTestingModule(
{
providers: [Listeners],
},
false
);
const app = m.createNestApplication({ logger: false });
app.useWebSocketAdapter(new SocketIoAdapter(app));
await app.init();
return app;
}
test.before(async t => {
t.context.app1 = await createApp();
t.context.app2 = await createApp();
});
test.after(async t => {
await t.context.app1.close();
await t.context.app2.close();
});
test('should broadcast event to cluster instances', async t => {
const { app1, app2 } = t.context;
// app 1 for listening
const eventbus1 = app1.get(EventBus);
const listener = Sinon.spy(app1.get(Listeners), 'onTestEvent');
const runtimeListener = Sinon.stub().returns({ count: 2 });
const off = eventbus1.on('__test__.event', runtimeListener);
// app 2 for broadcasting
const eventbus2 = app2.get(EventBus);
eventbus2.broadcast('__test__.event', { count: 0 });
// cause the cross instances broadcasting is asynchronization calling
// we should wait for the event's arriving before asserting
await eventbus1.waitFor('__test__.event');
t.true(listener.calledOnceWith({ count: 0 }));
t.true(runtimeListener.calledOnceWith({ count: 0 }));
off();
});
@@ -0,0 +1,111 @@
import { TestingModule } from '@nestjs/testing';
import ava, { TestFn } from 'ava';
import Sinon from 'sinon';
import { EventBus, metrics } from '../../base';
import { createTestingModule } from '../utils';
import { Listeners } from './provider';
export const test = ava as TestFn<{
module: TestingModule;
eventbus: EventBus;
listener: Sinon.SinonSpy;
}>;
test.before(async t => {
const m = await createTestingModule({
providers: [Listeners],
});
const eventbus = m.get(EventBus);
t.context.module = m;
t.context.eventbus = eventbus;
t.context.listener = Sinon.spy(m.get(Listeners), 'onTestEvent');
});
test.afterEach(() => {
Sinon.reset();
});
test.after(async t => {
await t.context.module.close();
});
test('should register event listener', t => {
const { eventbus } = t.context;
// @ts-expect-error private member
t.true(eventbus.emitter.eventNames().includes('__test__.event'));
eventbus.on('__test__.event2', () => {});
// @ts-expect-error private member
t.true(eventbus.emitter.eventNames().includes('__test__.event2'));
});
test('should dispatch event listener', t => {
const { eventbus, listener } = t.context;
const runtimeListener = Sinon.stub();
const off = eventbus.on('__test__.event', runtimeListener);
const payload = { count: 0 };
eventbus.emit('__test__.event', payload);
t.true(listener.calledOnceWithExactly(payload));
t.true(runtimeListener.calledOnceWithExactly(payload));
off();
});
test('should dispatch async event listener', async t => {
const { eventbus, listener } = t.context;
const runtimeListener = Sinon.stub().returns({ count: 2 });
const off = eventbus.on('__test__.event', runtimeListener);
const payload = { count: 0 };
const returns = await eventbus.emitAsync('__test__.event', payload);
t.true(listener.calledOnceWithExactly(payload));
t.true(runtimeListener.calledOnceWithExactly(payload));
t.deepEqual(returns, [{ count: 1 }, { count: 2 }]);
off();
});
test('should record event handler call metrics', async t => {
const { eventbus } = t.context;
const timerStub = Sinon.stub(
metrics.event.histogram('function_timer'),
'record'
);
const counterStub = Sinon.stub(
metrics.event.counter('function_calls'),
'add'
);
await eventbus.emitAsync('__test__.event', { count: 0 });
t.deepEqual(timerStub.getCall(0).args[1], {
name: 'event_handler',
event: '__test__.event',
namespace: '__test__',
error: false,
});
t.deepEqual(counterStub.getCall(0).args[1], {
event: '__test__.event',
namespace: '__test__',
});
Sinon.reset();
await eventbus.emitAsync('__test__.throw', { count: 0 });
t.deepEqual(timerStub.getCall(0).args[1], {
name: 'event_handler',
event: '__test__.throw',
namespace: '__test__',
error: true,
});
});
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '../../base';
declare global {
interface Events {
'__test__.event': { count: number };
'__test__.event2': { count: number };
'__test__.throw': { count: number };
}
}
@Injectable()
export class Listeners {
@OnEvent('__test__.event')
onTestEvent({ count }: Events['__test__.event']) {
return {
count: count + 1,
};
}
@OnEvent('__test__.throw')
onThrow() {
throw new Error('Error in event handler');
}
}