Table of Contents

Background Jobs

A server often has work to do outside of any request: clean up expired sessions every 15 minutes, send pending notifications, refresh a cache, consume a queue. Sparkle provides two classes for running such work on a background thread:

Both run inside a Sparkle host, next to the servers and with the same lifetime, and both also work in any application without a host.

Job Workers

TJobWorker runs a job when it starts, then again TJobWorker.Period seconds after each run finishes, and whenever TJobWorker.Trigger is called.

Creating a job

Derive from TJobWorker, override TJobWorker.​ProcessJob, and set the properties in the constructor:

uses
  Bcl.Cancellation,
  Sparkle.Host.Jobs;

type
  TSessionCleanupJob = class(TJobWorker)
  protected
    procedure ProcessJob(const AToken: ICancellationToken); override;
  public
    constructor Create; override;
  end;

constructor TSessionCleanupJob.Create;
begin
  inherited;
  Period := 15 * 60; // seconds
end;

procedure TSessionCleanupJob.ProcessJob(const AToken: ICancellationToken);
begin
  DeleteExpiredSessions(AToken);
end;

Alternatively, set up a job on an instance through TJobWorker.OnProcess, without deriving a class:

Cleanup := TJobWorker.Create;
Cleanup.Period := 15 * 60;
Cleanup.OnProcess :=
  procedure(const AToken: ICancellationToken)
  begin
    DeleteExpiredSessions(AToken);
  end;

Running a job

In a hosted server, add the job to the host builder. The host creates it from the class, starts it after the items added before it, and stops it on shutdown:

uses
  Sparkle.Host;

begin
  CreateHost
    .Add(TApiServerModule)
    .Add(TSessionCleanupJob)
    .Run;
end.

An instance is added through its IHostedService interface. The host then owns it: do not free it yourself.

CreateHost
  .Add(TApiServerModule)
  .Add(Cleanup as IHostedService)
  .Run;

Without a host, call Start and Stop directly and free the worker like any other object. Freeing a worker that is still running stops it first:

Cleanup.Start;
// ...
Cleanup.Free;

How runs are scheduled

Runs never overlap: they happen one at a time, on the worker's own thread. The period is counted from the end of a run, so a slow run delays the next one instead of piling up. A new value assigned to TJobWorker.Period applies to the wait in progress.

TJobWorker.Trigger asks for a run as soon as possible, without waiting for it - for example from an endpoint of your API after new data arrived. If a run is in progress, another one follows it; several requests made meanwhile are served by that single run.

With a period of zero, the job runs when the worker starts and then only when triggered.

Errors

An exception escaping a run is logged through Sparkle.Host.Log, passed to TJobWorker.OnError, and the worker goes on with the next run. A failing job never brings the server down.

Job state

TJobWorker.State returns a TJobRunState snapshot - run and failure counts, when the last run started and finished, how long it took, the last error, when the next run is due - that can be read from any thread, for example by a health check endpoint. TJobWorker.​OnRun​Finished is called after each run with the updated state.

Custom Workers

For work that is not a periodic job - a loop that consumes a queue, or one with a schedule of its own - derive from THostedWorker and override its Execute method:

uses
  System.SysUtils,
  Sparkle.Host.Contracts,
  Sparkle.Host.Log;

type
  TNightlyJobsWorker = class(THostedWorker)
  protected
    procedure Execute(const AStop: IStopToken); override;
  end;

procedure TNightlyJobsWorker.Execute(const AStop: IStopToken);
begin
  repeat
    try
      RunPendingJobs(AStop);
    except
      on E: Exception do
        if not AStop.IsStopRequested then
          LogError('Pending jobs failed: ' + E.Message);
    end;
  until AStop.WaitFor(60000);
end;

The worker gets a thread of its own on Start. Stop signals the IStopToken and waits for Execute to return. Waiting on the token with WaitFor instead of sleeping is what makes the stop immediate: the request releases the wait at once, wherever the loop happens to be.

An exception escaping Execute is logged and, in a host, brings the whole process down with exit code 1, rather than leaving a server that is up with a worker that died. That is why the loop above catches the exceptions of each iteration: one failed batch should not stop the server.

Like job workers, custom workers are added to the host with CreateHost.Add(TNightlyJobsWorker), or used without a host through Start, Stop and Free.

The Stop Token

The token received by a job (ICancellationToken) and by a custom worker (IStopToken, which descends from it) is a cancellation token, canceled when the worker stops. Everything described in the Cancellation chapter of the TMS BIZ Core Library applies to it. Pass it down into long units of work, so that they give up in the middle instead of holding up the stop:

When the work blocks on something other than the token - a queue, a socket, an event of its own - register a callback with ICancellation​Token.​Register that wakes that wait up:

procedure TMessageWorker.Execute(const AStop: IStopToken);
var
  Registration: IInterface;
  Msg: string;
begin
  // PopItem knows nothing about the token: shutting the queue down wakes it up
  Registration := AStop.Register(
    procedure
    begin
      FQueue.DoShutDown;
    end);

  while FQueue.PopItem(Msg) = wrSignaled do
    ProcessMessage(Msg);
end;

Migrating from TJobRunner

TJobRunner (unit Sparkle.Sys.JobRunner) is deprecated in favor of TJobWorker. It keeps compiling and working exactly as before, but receives no fixes or improvements. Moving to TJobWorker is mostly a matter of renaming:

// Before
FRunner := TJobRunner.Create;
FRunner.Period := 15 * 60;
FRunner.OnProcess :=
  procedure
  begin
    DeleteExpiredSessions;
  end;
FRunner.Start;

// After
FCleanup := TJobWorker.Create;
FCleanup.Period := 15 * 60;
FCleanup.OnProcess :=
  procedure(const AToken: ICancellationToken)
  begin
    DeleteExpiredSessions;
  end;
FCleanup.Start;
TJobRunner TJobWorker
Create, Start, Stop, Free Same. TJobWorker can also be added to a host.
Period (seconds) TJobWorker.Period (seconds). Zero means "run on Trigger only".
OnProcess: TProc TJobWorker.OnProcess, which receives the stop token.
Override ProcessJob Override TJobWorker.​ProcessJob, which receives the stop token.
StopRequested The token received by the job: AToken.IsCancellationRequested.
Trigger TJobWorker.Trigger
WaitJob No equivalent. Use TJobWorker.​OnRun​Finished or TJobWorker.State to learn when a run finished.
LoopCount State.RunCount
ErrorMessageFormat Errors are written to the Sparkle.Host.Log log; handle TJobWorker.OnError to report them elsewhere.
StopTimeout No equivalent: Stop waits for the running job.

Differences in behavior worth knowing:

  • Stop waits for the running job to finish instead of giving up after a timeout. Check the token in long jobs so that they stop promptly.
  • Job errors are written through Sparkle.Host.Log instead of Bcl.Logging.
  • A period of zero does not make the job run continuously.