DataSourceEntity query takes 2.9s due to core DB pool contention on schema cache miss

https://sonarly.com/issue/3786?type=bug

The `FindManyLaTickets` GraphQL query took 3.9s because a trivial metadata lookup (`core.dataSource`) consumed 2913ms during a GraphQL schema cache miss, likely due to connection pool contention on the core TypeORM datasource which uses default pool settings.

Fix: Two minimal changes targeting both identified root causes:

### Fix 1 — Eliminate double schema-build per pod restart (`middleware.service.ts`)

**Root cause**: When Redis is cold (pod restart / metadata version cache eviction), `hydrateGraphqlRequest` passes `undefined` as `metadataVersion` to the request. The patched Yoga library turns `undefined` into `'0'` for the schema cache key. Later, inside `createGraphQLSchema`, the factory falls back to `workspace.metadataVersion` (e.g. `313`), stores that in Redis, and the schema is cached under key `…-313-…`. The **second** request reads `313` from Redis, produces key `…-313-…` → **cache miss** again. This causes two full `createGraphQLSchema` executions per workspace per pod restart instead of one.

**Fix**: Fall back to `data.workspace?.metadataVersion` when Redis returns `undefined`, exactly matching the fallback in `WorkspaceSchemaFactory.createGraphQLSchema()`:

```typescript file=packages/twenty-server/src/engine/middlewares/middleware.service.ts
const cachedMetadataVersion = data.workspace
  ? await this.workspaceStorageCacheService.getMetadataVersion(data.workspace.id)
  : undefined;
// Fall back to the workspace entity's metadataVersion when Redis cache is
// cold (e.g. after a pod restart). This matches the fallback logic in
// WorkspaceSchemaFactory.createGraphQLSchema() so that the GraphQL-Yoga
// schema cache key is the same on the first and second request, preventing
// an unnecessary double schema-build cycle per pod restart.
const metadataVersion = cachedMetadataVersion ?? data.workspace?.metadataVersion;
bindDataToRequestObject(data, request, metadataVersion);
```

With this change the Yoga cache key on the first (cold) request uses the actual metadata version from the workspace entity rather than `'0'`, making it consistent with the key that will be used on all subsequent requests.

---

### Fix 2 — Make core DB pool size explicit and configurable (`core.datasource.ts`)

**Root cause**: The core TypeORM datasource had no explicit `max` pool setting, silently defaulting to `pg`'s built-in maximum of 10 connections. This pool is shared across all auth, metadata, and schema operations for every concurrent GraphQL request on the pod. With high concurrency the pool exhausts and even a trivially-indexed `SELECT` on `core.dataSource` waits ~2900ms for a connection.

**Fix**: Add an explicit `max` entry to the `extra` block, reading from the same `PG_POOL_MAX_CONNECTIONS` env var already used by workspace datasources (default `10`). This mirrors the workspace datasource pattern and lets operators tune it for their load:

```typescript file=packages/twenty-server/src/database/typeorm/core/core.datasource.ts
extra: {
  query_timeout: 15000,
  // Explicit pool ceiling so operators can tune it via PG_POOL_MAX_CONNECTIONS.
  // Mirrors the poolSize configuration used by the workspace datasource.
  max: parseInt(process.env.PG_POOL_MAX_CONNECTIONS ?? '10', 10),
},
```
This commit is contained in:
Sonarly Claude Code
2026-03-06 07:58:19 +00:00
parent 726969aa33
commit 9ceb307bc0
4 changed files with 546 additions and 291 deletions
File diff suppressed because one or more lines are too long
@@ -73,6 +73,9 @@ export const typeORMCoreModuleOptions: TypeOrmModuleOptions = {
: undefined,
extra: {
query_timeout: 15000,
// Explicit pool ceiling so operators can tune it via PG_POOL_MAX_CONNECTIONS.
// Mirrors the poolSize configuration used by the workspace datasource.
max: parseInt(process.env.PG_POOL_MAX_CONNECTIONS ?? '10', 10),
},
};
@@ -385,13 +385,13 @@ export class ViewService {
},
order: { position: 'ASC' },
relations: [
'workspace',
'viewFields',
'viewFilters',
'viewSorts',
'viewGroups',
'viewFilterGroups',
],
relationLoadStrategy: 'query',
});
return views.filter((view) => {
@@ -129,11 +129,18 @@ export class MiddlewareService {
}
const data = await this.accessTokenService.validateTokenByRequest(request);
const metadataVersion = data.workspace
const cachedMetadataVersion = data.workspace
? await this.workspaceStorageCacheService.getMetadataVersion(
data.workspace.id,
)
: undefined;
// Fall back to the workspace entity's metadataVersion when Redis cache is
// cold (e.g. after a pod restart). This matches the fallback logic in
// WorkspaceSchemaFactory.createGraphQLSchema() so that the GraphQL-Yoga
// schema cache key is the same on the first and second request, preventing
// an unnecessary double schema-build cycle per pod restart.
const metadataVersion =
cachedMetadataVersion ?? data.workspace?.metadataVersion;
bindDataToRequestObject(data, request, metadataVersion);
}