Skip to content

plugin-reports: the documented setInterval dispatcher fallback is unreachable on ObjectKernel, and the job fallback it takes instead never fires #10746

Description

@os-warren

Found while fixing #10371 (teardown hook naming) in plugin-reports. Different defect, filed rather than fixed — that PR is one defect with one proof, and this one changes whether scheduled reports run at all.

The claim in the code

packages/plugins/plugin-reports/src/reports-plugin.ts:33-36:

The dispatcher uses IJobService.schedule when one is registered; otherwise it falls back to a plain setInterval so single-kernel deployments work without service-job.

start() implements exactly that: it tries ctx.getService<IJobService>('job') and only reaches setInterval when the lookup yields nothing with a schedule method.

Why the fallback can never be reached on ObjectKernel

ObjectKernel.preInjectCoreFallbacks() (packages/core/src/kernel.ts:261-275) walks ServiceRequirementDef and registers a CORE_FALLBACK_FACTORIES entry for every unprovided core service before Phase 2, i.e. before any kernel:ready hook runs. job is one of the five (packages/core/src/fallbacks/index.ts). So ctx.getService('job') ALWAYS resolves on an ObjectKernel, and the setInterval branch is dead code there.

And the thing it resolves to does not schedule. createMemoryJob() (packages/core/src/fallbacks/memory-job.ts) is honest about it in its own docblock:

trigger() really runs the registered handler, but nothing here owns a timer, so a schedule()d job NEVER fires on its own.

Its schedule() is jobs.set(name, { schedule, handler }) and nothing else.

Net effect

On an ObjectKernel stack without @objectstack/service-job, ReportsServicePlugin logs

ReportsServicePlugin: dispatcher registered with job service {"intervalMs":60000}

…and then dispatches nothing, ever. sys_report_schedule is never polled, no scheduled report is ever emailed, and the log line reads like success. The setInterval path the docblock offers as the single-kernel answer is unreachable.

Measured

Booting ObjectKernel + ObjectQLPlugin + ReportsServicePlugin({ dispatchIntervalMs: 5000 }) with no job plugin and a real SqlDriver, then counting engine.find('sys_report_schedule', …) on the engine the plugin captured at kernel:ready: 0 reads in 5600 ms, with the log line above present. The same stack on LiteKernel — which injects no fallbacks — takes the setInterval branch and reads on schedule. That contrast is what identifies the cause as the pre-injected fallback rather than anything in the reports plugin's own logic.

(The LiteKernel half of that measurement is now pinned in packages/plugins/plugin-reports/src/plugin-shutdown-releases-dispatcher.test.ts as part of #10371, with the mechanism written down at bootReportsLiteKernel. #10371 pins that the dispatcher is released at shutdown; nothing pins that it runs on the kernel real deployments use.)

Not asserted here

Three questions this finding does not decide:

  1. Where the repair belongs. Either plugin-reports stops treating "a job service exists" as "a working scheduler" (e.g. by reading __serviceInfo.status === 'degraded', which the fallback already declares), or the job fallback stops advertising a schedule() it cannot honour, or ServiceRequirementDef stops classifying job as pre-injectable. These are materially different contracts.
  2. How wide the class is. Any plugin written as "prefer the platform job service, else own a timer" has the same shape against the same pre-injection. This finding measured only plugin-reports.
  3. Whether the degraded-fallback surface is meant to be consulted.createMemoryJob() publishes __serviceInfo: { status: 'degraded', message } — a declared, machine-readable "I am not really scheduling". If consumers are expected to read it, no consumer in this tree does; if they are not, it is inert.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions