Skip to content

Commit

Permalink
fix(client): Ensure getDMMF gets called only once (#14712)
Browse files Browse the repository at this point in the history
Problem: when we have a freshly instantiated client (haven't done
any queries yet) and we execute multiple queries at the same event loop
tick, each one of them will start `getDMMF` request, which could be
quite slow.

Fixed by caching the results promise first time `getDMMF` is called and
returning the same promise for subsequent calls.

Fix #14695
  • Loading branch information
SevInf committed Aug 10, 2022
1 parent cf9aba8 commit 96a85ed
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 6 deletions.
12 changes: 6 additions & 6 deletions packages/client/src/runtime/getPrismaClient.ts
Expand Up @@ -16,7 +16,7 @@ import {
TracingConfig,
} from '@prisma/engine-core'
import type { DataSource, GeneratorConfig } from '@prisma/generator-helper'
import { ClientEngineType, getClientEngineType, logger, tryLoadEnvs, warnOnce } from '@prisma/internals'
import { callOnce, ClientEngineType, getClientEngineType, logger, tryLoadEnvs, warnOnce } from '@prisma/internals'
import type { LoadedEnv } from '@prisma/internals/dist/utils/tryLoadEnvs'
import { AsyncResource } from 'async_hooks'
import fs from 'fs'
Expand Down Expand Up @@ -1118,8 +1118,7 @@ new PrismaClient({
otelParentCtx,
}: InternalRequestParams) {
if (this._dmmf === undefined) {
const dmmf = await this._getDmmf({ clientMethod, callsite })
this._dmmf = new DMMFHelper(getPrismaClientDMMF(dmmf))
this._dmmf = await this._getDmmf({ clientMethod, callsite })
}

let rootField: string | undefined
Expand Down Expand Up @@ -1215,13 +1214,14 @@ new PrismaClient({
})
}

private async _getDmmf(params: Pick<InternalRequestParams, 'clientMethod' | 'callsite'>) {
private _getDmmf = callOnce(async (params: Pick<InternalRequestParams, 'clientMethod' | 'callsite'>) => {
try {
return await this._engine.getDmmf()
const dmmf = await this._engine.getDmmf()
return new DMMFHelper(getPrismaClientDMMF(dmmf))
} catch (error) {
this._fetcher.handleRequestError({ ...params, error })
}
}
})

get $metrics(): MetricsClient {
if (!this._hasPreviewFlag('metrics')) {
Expand Down
1 change: 1 addition & 0 deletions packages/internals/src/index.ts
Expand Up @@ -48,6 +48,7 @@ export { engineEnvVarMap, resolveBinary } from './resolveBinary'
export { sendPanic } from './sendPanic'
export type { DatabaseCredentials } from './types'
export { assertNever } from './utils/assertNever'
export { callOnce } from './utils/callOnce'
export { drawBox } from './utils/drawBox'
export { extractPreviewFeatures } from './utils/extractPreviewFeatures'
export { formatms } from './utils/formatms'
Expand Down
32 changes: 32 additions & 0 deletions packages/internals/src/utils/callOnce.test.ts
@@ -0,0 +1,32 @@
import { callOnce } from './callOnce'

test('returns the result correctly', async () => {
const wrapper = callOnce(jest.fn().mockResolvedValue('hello'))
await expect(wrapper()).resolves.toBe('hello')
})

test('forwards the arguments correctly', async () => {
const wrapper = callOnce((x: number) => Promise.resolve(x + 1))
await expect(wrapper(2)).resolves.toBe(3)
})

test('сalls wrapped function only once before promise resolves', async () => {
const wrapped = jest.fn().mockResolvedValue('hello')
const wrapper = callOnce(wrapped)
void wrapper()
void wrapper()
await wrapper()

expect(wrapped).toBeCalledTimes(1)
})

test('caches the result', async () => {
const wrapped = jest.fn().mockResolvedValue('hello')
const wrapper = callOnce(wrapped)
await wrapper()
await wrapper()
const result = await wrapper()

expect(wrapped).toBeCalledTimes(1)
expect(result).toBe('hello')
})
15 changes: 15 additions & 0 deletions packages/internals/src/utils/callOnce.ts
@@ -0,0 +1,15 @@
type AsyncFn<Args extends unknown[], R> = (...args: Args) => Promise<R>

/**
* Takes an async function `fn` as a parameters and returns a wrapper function, which ensures
* that `fn` will be called only once:
*
* - If the first call is not finished yet, returns the promise to the same result
* - If the first call is finished, returns the result of this call
* @param fn
* @returns
*/
export function callOnce<Args extends unknown[], R>(fn: AsyncFn<Args, R>): AsyncFn<Args, R> {
let result: Promise<R> | undefined
return (...args) => (result ??= fn(...args))
}

0 comments on commit 96a85ed

Please sign in to comment.