MemberJunction / MemberJunction/MJ
[scheduling-actions] ExecuteJobNowAction creates orphan 'Running' ScheduledJobRun without dispatching job
- 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
Assessment
This issue has not been assessed yet.