Skip to content

Test suite hangs: repeated DataSource initialize()/destroy() cycles within one file never resolve #924

Description

@arturasmckwcz

What's happening

yarn test / yarn test:run fails deterministically on ~33 of 93 test files, every run, with Error: Hook timed out in Nms (whatever hookTimeout is set to — reproduced at both 10s and 30s) on the beforeAll that does:

beforeAll(async () => {
  fastify = await createServer();
  await fastify.ready();
  ...
});

Every subsequent test/hook in the same describe then fails with TypeError: Cannot read properties of undefined (reading 'db') (or 'id'/'close') since fastify was never assigned.

What it's not

  • Not Postgres connection-pool exhaustion. pg_stat_activity shows ~6 connections throughout a run (max_connections=100). Capping the pool (extra: { max: 5, idleTimeoutMillis: 1000 } in data-source.ts, test-only) made no difference — reverted.
  • Not a stuck pg_advisory_lock. pg_locks/pg_stat_activity are completely empty during the hang — the process never even opens a connection to Postgres before it hangs. So the block happens before dataSource.initialize()'s connection attempt, not inside it.
  • Not CPU contention from running other work in parallel. Reproduces running the single affected file completely alone (yarn vitest run src/test/server/routes/opportunity.routes.test.ts), with nothing else competing for CPU/DB.
  • Not flaky — identical failure count (33 files, 589 passed / 205 skipped tests) across multiple independent full-suite runs.

What I found (via temporary instrumentation, reverted — not committed)

Instrumenting createServer() in src/server/index.ts line-by-line showed execution reaches await fastifyInstance.register(typeormPlugin) and never proceeds — i.e. the hang is inside typeormPlugininitDatabase() (src/data/index.ts), before dataSource.initialize()'s actual Postgres connection is ever visible on the server side.

The suspicious part: src/server/plugins/typeorm.ts's onClose hook calls dataSource.destroy() whenever a test's fastify.close() runs:

fastify.addHook("onClose", async () => {
  if (dataSource.isInitialized) {
    await dataSource.destroy();
  }
});

dataSource is a module-level singleton guarded by if (!dataSource.isInitialized) in the plugin's registration path — but because onClose tears it fully down again, any test file with multiple describe blocks, each with its own beforeAll(createServer)/afterAll(fastify.close()) (e.g. src/test/server/routes/opportunity.routes.test.ts, which has 8), forces a full initialize()destroy()initialize() → ... cycle on the same DataSource/entity-metadata object, repeated once per describe in that one file. All 8 cycles in that file hang identically, every time.

This smells like a TypeORM decorator-metadata or connection-lifecycle issue that shows up specifically on repeated initialize()/destroy() of the same DataSource instance within one process — not something to guess-fix without a deeper look at what state survives a destroy() and what initialize() re-does with it.

Suggested next steps

  • Try reproducing minimally: a tiny script that calls dataSource.initialize()dataSource.destroy()dataSource.initialize() in a loop against the real entity list, timing each call, to confirm/deny that repeated re-initialization on the same instance (rather than the test harness) is the actual trigger.
  • Consider whether each test file should get its own fresh DataSource instance (avoiding destroy()+re-initialize() on a shared singleton) rather than sharing the module-level one across describe blocks.
  • Worth checking whether this correlates with running the local docker compose dev be service (nodemon) concurrently against the same Postgres container while running tests on the host — that wasn't ruled out as a contributing factor, only as the sole cause (the hang reproduced with no visible Postgres-side activity either way).

Impact

Blocks yarn test/yarn test:run and the pre-push husky hook for any commit, not just ones touching affected code — currently the only way to commit at all is --no-verify.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions