mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat(server): cluster level event system (#9884)
This commit is contained in:
62
packages/backend/server/src/__tests__/event/cluster.spec.ts
Normal file
62
packages/backend/server/src/__tests__/event/cluster.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
111
packages/backend/server/src/__tests__/event/eventbus.spec.ts
Normal file
111
packages/backend/server/src/__tests__/event/eventbus.spec.ts
Normal file
@@ -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,
|
||||
});
|
||||
});
|
||||
26
packages/backend/server/src/__tests__/event/provider.ts
Normal file
26
packages/backend/server/src/__tests__/event/provider.ts
Normal file
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { TestingModule } from '@nestjs/testing';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { EmailAlreadyUsed } from '../../base';
|
||||
import { EmailAlreadyUsed, EventBus } from '../../base';
|
||||
import { Permission } from '../../models/common';
|
||||
import { UserModel } from '../../models/user';
|
||||
import { WorkspaceMemberStatus } from '../../models/workspace';
|
||||
@@ -46,7 +45,7 @@ test('should create a new user', async t => {
|
||||
});
|
||||
|
||||
test('should trigger user.created event', async t => {
|
||||
const event = t.context.module.get(EventEmitter2);
|
||||
const event = t.context.module.get(EventBus);
|
||||
const spy = Sinon.spy();
|
||||
event.on('user.created', spy);
|
||||
|
||||
@@ -117,7 +116,7 @@ test('should not update email to an existing one', async t => {
|
||||
});
|
||||
|
||||
test('should trigger user.updated event', async t => {
|
||||
const event = t.context.module.get(EventEmitter2);
|
||||
const event = t.context.module.get(EventBus);
|
||||
const spy = Sinon.spy();
|
||||
event.on('user.updated', spy);
|
||||
|
||||
@@ -217,7 +216,7 @@ test('should fulfill user', async t => {
|
||||
});
|
||||
|
||||
test('should trigger user.updated event when fulfilling user', async t => {
|
||||
const event = t.context.module.get(EventEmitter2);
|
||||
const event = t.context.module.get(EventBus);
|
||||
const createSpy = Sinon.spy();
|
||||
const updateSpy = Sinon.spy();
|
||||
event.on('user.created', createSpy);
|
||||
@@ -250,7 +249,7 @@ test('should delete user', async t => {
|
||||
});
|
||||
|
||||
test('should trigger user.deleted event', async t => {
|
||||
const event = t.context.module.get(EventEmitter2);
|
||||
const event = t.context.module.get(EventBus);
|
||||
const spy = Sinon.spy();
|
||||
event.on('user.deleted', spy);
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { TestingModule } from '@nestjs/testing';
|
||||
import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { Config } from '../../base/config';
|
||||
import { Config, EventBus } from '../../base';
|
||||
import { Permission } from '../../models/common';
|
||||
import { UserModel } from '../../models/user';
|
||||
import { WorkspaceModel } from '../../models/workspace';
|
||||
@@ -305,7 +304,7 @@ test('should grant member with read permission and Pending status by default', a
|
||||
email: 'test2@affine.pro',
|
||||
});
|
||||
|
||||
const event = t.context.module.get(EventEmitter2);
|
||||
const event = t.context.module.get(EventBus);
|
||||
const updatedSpy = Sinon.spy();
|
||||
event.on('workspace.members.updated', updatedSpy);
|
||||
const member1 = await t.context.workspace.grantMember(
|
||||
@@ -829,7 +828,7 @@ test('should delete workspace member in Pending, Accepted status', async t => {
|
||||
);
|
||||
t.is(member.status, WorkspaceMemberStatus.Pending);
|
||||
|
||||
const event = t.context.module.get(EventEmitter2);
|
||||
const event = t.context.module.get(EventBus);
|
||||
const updatedSpy = Sinon.spy();
|
||||
event.on('workspace.members.updated', updatedSpy);
|
||||
let success = await t.context.workspace.deleteMember(
|
||||
@@ -880,7 +879,7 @@ test('should trigger workspace.members.requestDeclined event when delete workspa
|
||||
);
|
||||
t.is(member.status, WorkspaceMemberStatus.UnderReview);
|
||||
|
||||
const event = t.context.module.get(EventEmitter2);
|
||||
const event = t.context.module.get(EventBus);
|
||||
const updatedSpy = Sinon.spy();
|
||||
const requestDeclinedSpy = Sinon.spy();
|
||||
event.on('workspace.members.updated', updatedSpy);
|
||||
@@ -925,7 +924,7 @@ test('should trigger workspace.members.requestDeclined event when delete workspa
|
||||
);
|
||||
t.is(member.status, WorkspaceMemberStatus.NeedMoreSeatAndReview);
|
||||
|
||||
const event = t.context.module.get(EventEmitter2);
|
||||
const event = t.context.module.get(EventBus);
|
||||
const updatedSpy = Sinon.spy();
|
||||
const requestDeclinedSpy = Sinon.spy();
|
||||
event.on('workspace.members.updated', updatedSpy);
|
||||
|
||||
@@ -105,18 +105,21 @@ function gql(app: INestApplication, query: string) {
|
||||
.expect(200);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
test.before(async ({ context }) => {
|
||||
const { app } = await createTestingApp({
|
||||
providers: [TestResolver, TestGateway],
|
||||
controllers: [TestController],
|
||||
});
|
||||
|
||||
context.logger = Sinon.stub(new Logger().localInstance);
|
||||
|
||||
context.app = app;
|
||||
});
|
||||
|
||||
test.afterEach.always(async ctx => {
|
||||
test.beforeEach(() => {
|
||||
Sinon.reset();
|
||||
});
|
||||
|
||||
test.after.always(async ctx => {
|
||||
await ctx.context.app.close();
|
||||
});
|
||||
|
||||
@@ -131,6 +134,7 @@ test('should be able to handle known user error in graphql query', async t => {
|
||||
t.is(err.message, 'You do not have permission to access this resource.');
|
||||
t.is(err.extensions.status, HttpStatus.FORBIDDEN);
|
||||
t.is(err.extensions.name, 'ACCESS_DENIED');
|
||||
// console.log(t.context.logger.error.getCalls());
|
||||
t.true(t.context.logger.error.notCalled);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import Sinon from 'sinon';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { AppModule } from '../../app.module';
|
||||
import { EventEmitter, Runtime } from '../../base';
|
||||
import { EventBus, Runtime } from '../../base';
|
||||
import { ConfigModule } from '../../base/config';
|
||||
import { CurrentUser } from '../../core/auth';
|
||||
import { AuthService } from '../../core/auth/service';
|
||||
@@ -158,7 +158,7 @@ const test = ava as TestFn<{
|
||||
db: PrismaClient;
|
||||
app: INestApplication;
|
||||
service: SubscriptionService;
|
||||
event: Sinon.SinonStubbedInstance<EventEmitter>;
|
||||
event: Sinon.SinonStubbedInstance<EventBus>;
|
||||
feature: Sinon.SinonStubbedInstance<FeatureManagementService>;
|
||||
runtime: Sinon.SinonStubbedInstance<Runtime>;
|
||||
stripe: {
|
||||
@@ -203,14 +203,12 @@ test.before(async t => {
|
||||
m.overrideProvider(FeatureManagementService).useValue(
|
||||
Sinon.createStubInstance(FeatureManagementService)
|
||||
);
|
||||
m.overrideProvider(EventEmitter).useValue(
|
||||
Sinon.createStubInstance(EventEmitter)
|
||||
);
|
||||
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
|
||||
m.overrideProvider(Runtime).useValue(Sinon.createStubInstance(Runtime));
|
||||
},
|
||||
});
|
||||
|
||||
t.context.event = app.get(EventEmitter);
|
||||
t.context.event = app.get(EventBus);
|
||||
t.context.service = app.get(SubscriptionService);
|
||||
t.context.feature = app.get(FeatureManagementService);
|
||||
t.context.runtime = app.get(Runtime);
|
||||
|
||||
@@ -10,7 +10,7 @@ import ava from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { AppModule } from '../app.module';
|
||||
import { EventEmitter } from '../base';
|
||||
import { EventBus } from '../base';
|
||||
import { AuthService } from '../core/auth';
|
||||
import { DocContentService } from '../core/doc-renderer';
|
||||
import { Permission, PermissionService } from '../core/permission';
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
const test = ava as TestFn<{
|
||||
app: INestApplication;
|
||||
auth: AuthService;
|
||||
event: Sinon.SinonStubbedInstance<EventEmitter>;
|
||||
event: Sinon.SinonStubbedInstance<EventBus>;
|
||||
quota: QuotaService;
|
||||
quotaManager: QuotaManagementService;
|
||||
permissions: PermissionService;
|
||||
@@ -52,8 +52,8 @@ test.beforeEach(async t => {
|
||||
imports: [AppModule],
|
||||
tapModule: module => {
|
||||
module
|
||||
.overrideProvider(EventEmitter)
|
||||
.useValue(Sinon.createStubInstance(EventEmitter));
|
||||
.overrideProvider(EventBus)
|
||||
.useValue(Sinon.createStubInstance(EventBus));
|
||||
module.overrideProvider(DocContentService).useValue({
|
||||
getWorkspaceContent() {
|
||||
return {
|
||||
@@ -67,7 +67,7 @@ test.beforeEach(async t => {
|
||||
|
||||
t.context.app = app;
|
||||
t.context.auth = app.get(AuthService);
|
||||
t.context.event = app.get(EventEmitter);
|
||||
t.context.event = app.get(EventBus);
|
||||
t.context.quota = app.get(QuotaService);
|
||||
t.context.quotaManager = app.get(QuotaManagementService);
|
||||
t.context.permissions = app.get(PermissionService);
|
||||
|
||||
@@ -112,7 +112,12 @@ export async function createTestingModule(
|
||||
|
||||
if (init) {
|
||||
await m.init();
|
||||
|
||||
// we got a lot smoking tests try to break nestjs
|
||||
// can't tolerate the noisy logs
|
||||
// @ts-expect-error private
|
||||
m.applyLogger({
|
||||
logger: ['fatal'],
|
||||
});
|
||||
const runtime = m.get(Runtime);
|
||||
// by pass password min length validation
|
||||
await runtime.set('auth/password.min', 1);
|
||||
@@ -128,7 +133,7 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
|
||||
cors: true,
|
||||
bodyParser: true,
|
||||
rawBody: true,
|
||||
logger: ['warn'],
|
||||
logger: ['fatal'],
|
||||
});
|
||||
|
||||
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
|
||||
|
||||
Reference in New Issue
Block a user