Skip to content

Commit

Permalink
perf: use Domain API instead of async_hooks
Browse files Browse the repository at this point in the history
After observing huge performance impact when using async_hooks when concurrent requests are involved, Domain API seems like much better fit for this task. Even though they are deprecated, they will most likely stay till another (stable) solution will be available.

So far async_hooks caused memory leaks even without ORM being involved. Simple express server with async_hooks tracing enabled created dozens of references in CONTEXT map without properly destroying them (the destroy hook was simply not called for those resource).
  • Loading branch information
Martin Adamek committed May 27, 2019
1 parent bcc005e commit a77bdfd
Show file tree
Hide file tree
Showing 2 changed files with 19 additions and 26 deletions.
36 changes: 16 additions & 20 deletions lib/utils/RequestContext.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
import { createHook, executionAsyncId } from 'async_hooks';
import * as domain from 'domain';
import { v4 as uuid } from 'uuid';
import { EntityManager } from '../EntityManager';

declare module 'domain' {
export type ORMDomain = Domain & { __mikro_orm_context?: RequestContext };
const active: ORMDomain;
function create(): ORMDomain;
}

export class RequestContext {

static readonly CONTEXT: Record<number, RequestContext> = {};
readonly id = uuid();

constructor(readonly em: EntityManager) { }

static create(em: EntityManager, next: Function) {
RequestContext.CONTEXT[executionAsyncId()] = new RequestContext(em.fork());

const init = (asyncId: number, type: string, triggerId: number) => {
if (RequestContext.CONTEXT[triggerId]) {
RequestContext.CONTEXT[asyncId] = RequestContext.CONTEXT[triggerId];
}
};
const destroy = (asyncId: number) => {
delete RequestContext.CONTEXT[asyncId];
};

createHook({ init, destroy }).enable();
next();
static create(em: EntityManager, next: (...args: any[]) => void) {
const context = new RequestContext(em.fork());
const d = domain.create();
d.__mikro_orm_context = context;
d.run(next);
}

static currentRequestContext(): RequestContext | null {
return RequestContext.CONTEXT[executionAsyncId()] || null;
static currentRequestContext(): RequestContext | undefined {
return domain.active ? domain.active.__mikro_orm_context : undefined;
}

static getEntityManager(): EntityManager | null {
static getEntityManager(): EntityManager | undefined {
const context = RequestContext.currentRequestContext();
return context ? context.em : null;
return context ? context.em : undefined;
}

}
9 changes: 3 additions & 6 deletions tests/RequestContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@ describe('RequestContext', () => {
beforeEach(async () => wipeDatabase(orm.em));

test('create new context', async () => {
expect(RequestContext.getEntityManager()).toBeNull();
expect(RequestContext.getEntityManager()).toBeUndefined();
RequestContext.create(orm.em, () => {
const em = RequestContext.getEntityManager()!;
expect(em).not.toBe(orm.em);
// access UoW via property so we do not get the one from request context automatically
expect(em['unitOfWork'].getIdentityMap()).not.toBe(orm.em['unitOfWork'].getIdentityMap());
expect(RequestContext.currentRequestContext()).not.toBeUndefined();
});
expect(RequestContext.currentRequestContext()).not.toBeNull();

// on node 12, destroy hook is called after the test is done
// await new Promise(resolve => setTimeout(resolve, 100)); // wait for GC
// expect(RequestContext.currentRequestContext()).toBeNull();
expect(RequestContext.currentRequestContext()).toBeUndefined();
});

test('request context does not break population', async () => {
Expand Down

0 comments on commit a77bdfd

Please sign in to comment.