MemberJunction / MemberJunction/MJ

[scheduling-actions] ExecuteJobNowAction creates orphan 'Running' ScheduledJobRun without dispatching job

Open
#2,414 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
TSQL
Stars
29
Forks
6
Avg merge
2d 1h
Merged PRs (30d)
323

Description

## Summary

`ExecuteScheduledJobNowAction` in `@memberjunction/scheduling-actions` creates a `MJ: Scheduled Job Runs` record with `Status='Running'` and returns `Success=true`, but never dispatches the job. The row stays stuck at `Running` forever and no plugin code runs. "Run Now" from MJ Explorer is effectively a no-op that also leaks an orphan run record every click.

## Reproduction

1. Create an Active Scheduled Job in MJ Explorer (any job type — Action, Agent, IntegrationSync).
2. Invoke the `Execute Scheduled Job Now` action against it (via MJ Explorer "Run Now" UI or direct `RunAction` call with `JobID` param).
3. Query `MJ: Scheduled Job Runs` for that `ScheduledJobID`.

## Expected

- Action delegates to `SchedulingEngine.Instance.ExecuteScheduledJob(jobId, contextUser)`.
- Run lifecycle transitions `Running` -> `Completed` or `Failed` with `CompletedAt` set.
- Plugin driver actually executes (`plugin.Execute(context)` in `ScheduledJobEngine.executeJob`).

## Actual

- A row is inserted with `Status='Running'`, `StartedAt=now`, `QueuedAt=now`, and **never updated**.
- No plugin executes. No error logged.
- Action returns `Success=true` with a `RunID`, misleading the caller.
- The polling loop (`ScheduledJobEngine.ExecuteScheduledJobs`) will not pick this row up either — it evaluates `NextRunAt` on the job, not pending runs.

## Root cause

File: `packages/Scheduling/actions/src/ExecuteJobNowAction.ts` (lines 65-87 on `next`, commit `ceeebeb905`).

The action hand-rolls a `ScheduledJobRun` insert and stops there:

```ts
jobRun.NewRecord();
jobRun.ScheduledJobID = job.ID;
jobRun.Status = 'Running';
jobRun.StartedAt = new Date();
jobRun.QueuedAt = new Date();
const saveResult = await jobRun.Save();
// ...returns success. Never calls SchedulingEngine.
```

The only dispatch path in the codebase is `ScheduledJobEngine.executeJob` (`packages/Scheduling/engine/src/ScheduledJobEngine.ts:347`), called from:
- `ExecuteScheduledJobs` (polling loop, line 255) — filters by `isJobDue` based on `NextRunAt`, so it will not re-process an orphan run row.
- `ExecuteScheduledJob(jobId, contextUser)` (line 299) — the public "run this job by ID now" entry point. The action should be calling this.

`executeJob` itself creates the run record (`createJobRun`), acquires a lock, instantiates the plugin via `ClassFactory`, invokes `plugin.Execute(context)`, and updates the run to `Completed`/`Failed`. So the action's manual row insert is both redundant and incomplete.

## Suggested fix

Replace the manual `MJScheduledJobRunEntity` creation in `ExecuteJobNowAction.ts` with a delegation to the engine:

```ts
const run = await SchedulingEngine.Instance.ExecuteScheduledJob(jobId, params.ContextUser);
this.addOutputParam(params, 'RunID', run.ID);
return { Success: run.Success, ResultCode: run.Success ? 'SUCCESS' : 'FAILED', ... };
```

Decide whether the action awaits completion (simpler, blocks the action runner) or fires-and-forgets (returns `RunID` immediately, run completes asynchronously). Downstream consumers have implemented fire-and-forget as a workaround — worth considering as the default given UI responsiveness.

Remove the JobID existence check and `Status === 'Active'` check too — `ExecuteScheduledJob` already validates via `ScheduledJobs.find` (only active jobs are loaded into the engine's in-memory list).

## Impact

- Every "Run Now" click in MJ Explorer silently does nothing and leaks an orphan `Running` row.
- Any downstream consumer depending on this action has a broken "execute immediately" workflow.
- Orphan rows accumulate indefinitely — no cleanup path, and they pollute job run history / metrics.

## Missing test coverage

- `packages/Scheduling/actions/src/__tests__/` has `BaseJobAction.test.ts` but no `ExecuteJobNowAction.test.ts`.
- No integration test asserts the full `Pending -> Running -> Completed/Failed` lifecycle when this action fires.
- Recommended: integration test that invokes `ExecuteScheduledJobNowAction`, awaits the resulting run, and asserts the run reaches a terminal state (`Completed` or `Failed`) with a non-null `CompletedAt` and the plugin's side effect occurred.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.