Skip to content

plugin-reports: the timers started by ReportsPlugin are released from stop(), which the kernel never calls #10371

Description

@os-zhuang

Found while fixing #9371 in @objectstack/service-messaging. Same defect, different package — filed rather than fixed, to keep that PR to one defect with one proof.

The shape

Plugin (packages/core/src/types.ts:170) declares exactly one teardown hook:

destroy?(): Promise<void>|void;

ObjectKernel.performShutdown() and LiteKernel.destroy() walk the plugins in reverse and call plugin.destroy(). stop() is not on that interface, so a plugin that spells its teardown stop() is never torn down: await kernel.shutdown() resolves with the plugin's timers still armed.

That is exactly what #9371 was — measured there as 48 further delivery reads/writes in the 80 ms after a resolved shutdown().

This instance

packages/plugins/plugin-reports/src/reports-plugin.ts has an async stop() and 5setInterval/setTimeout sites, and no destroy(). Nothing in the tree calls its stop().

The rest of the class (no timers, so lower stakes — listed for whoever sweeps)

These also expose an async stop() with no destroy(), i.e. a teardown the kernel does not reach. None of them own timers today, so the consequence is limited to whatever else stop() releases:

  • packages/connectors/connector-openapi/src/connector-openapi-plugin.ts
  • packages/connectors/connector-rest/src/connector-rest-plugin.ts
  • packages/connectors/connector-slack/src/connector-slack-plugin.ts
  • packages/plugins/plugin-approvals/src/approvals-plugin.ts
  • packages/services/service-knowledge/src/knowledge-service-plugin.ts

Why it stays invisible

plugin-reportsunref()s at least some of its timers (as messaging did), so a long-lived host process still exits and nothing complains. The bill lands in test harnesses, where the process is alive throughout teardown — see #9371 for the full mechanism by which that turns into a green suite exiting 1.

Shape of the repair (as applied in #9371)

Move the body to destroy() and keep stop() as a delegating alias — it is public API of an exported class, and removing it would break an embedder who learned to call it directly precisely because the kernel never did.

A guard that fails when a Plugin implementation declares stop() and no destroy() would close the whole class; that is a judgement call for triage, not something this finding asserts.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions