Skip to content

Tasks and Daemons — Background Execution

Overview and Motivation

Long-running applications need to do work beside the request that triggered it: run a job later, repeat a job on an interval, serialize jobs that must not overlap, and run supervised background threads that can be terminated cleanly on shutdown. tentackle-core exports two small, related packages for this:

  • org.tentackle.task — a task dispatcher that runs Tasks one at a time, with optional scheduling, repetition and mutual exclusion.
  • org.tentackle.daemon — a daemon supervisor that owns the life cycle of background threads and shuts them down gracefully.

The two connect naturally: the default dispatcher is itself a supervised daemon thread.

Tasks (org.tentackle.task)

Task and AbstractTask

A Task is a Runnable that is also Serializable and Comparable<Task> (so the dispatcher can order it). Most tasks extend AbstractTask and implement run(). Two properties control when and how often a task runs:

task.setScheduledEpochalTime(when);   // absolute time (epoch millis) at which to run; 0 = as soon as possible
task.setRepeatInterval(millis);       // > 0 to re-queue the task this many ms after each run

TaskDispatcher and DefaultTaskDispatcher

A TaskDispatcher executes its tasks serially. DefaultTaskDispatcher is a Thread that implements it (and Supervisable, see below):

TaskDispatcher dispatcher = new DefaultTaskDispatcher("background");
dispatcher.start();

dispatcher.addTask(task);                 // queue a task
boolean done = dispatcher.addTaskAndWait(task);   // queue and block until it finished
dispatcher.waitForTask(task);             // block until a queued task finished
dispatcher.removeTask(task);              // de-queue if still pending

Useful operations on the dispatcher:

  • Queue inspectiongetQueueSize(), isQueueEmpty(), getTask(id), getAllTasks(), isTaskPending(task), isInstanceOfTaskPending(MyTask.class).
  • Mutual exclusionlock(key) / unlock(key) hand out a TaskDispatcherLock; enable it with setUsingMutexLocking(true) so tasks sharing a key never run concurrently.
  • TimingsetSleepInterval, setWaitInterval, setDeadInterval and setShutdownIdleTimeout tune the polling loop and the idle auto-shutdown.
  • DiagnosticstoDiagnosticString(), isAlive().

TaskListener

Register a TaskListener on a dispatcher or on an individual task to observe task life-cycle events. TaskException reports task failures.

Daemons (org.tentackle.daemon)

Daemons are ordinary background threads whose life cycle is owned by a DaemonSupervisor. The supervisor periodically checks its daemons and can terminate or kill them — for example on application shutdown.

The package is a small set of capability interfaces:

Interface Meaning
Terminatable something (typically a Thread) that can be asked to terminate gracefully
Killable a thread that may be forcibly killed
Supervisable a Killable thread that opts into supervision, allowing the supervisor extra checks and control
Scavenger a cleanup service (e.g. a cleanup thread) that may bypass certain checks to avoid spurious exceptions

Because DefaultTaskDispatcher implements Supervisable (and TaskDispatcher extends Terminatable), a task dispatcher can be placed under a DaemonSupervisor and torn down cleanly together with the rest of the application's background threads.

Which Dispatcher Do I Want?

org.tentackle.task deliberately knows nothing about database sessions or the UI. Two higher layers build on it, and picking the right entry point is mostly a question of who provides the session:

DefaultTaskDispatcher DefaultSessionTaskDispatcher Rdc.bg(...)
Module tentackle-core tentackle-session tentackle-fx-rdc
Database session none one exclusive session owned by the dispatcher provided behind the scenes (tracker session or session pool)
Task type AbstractTask AbstractSessionTask any lambda — wrapped into a session task for you
Thread you create and start() it you create and start() it reuses the modification tracker (or a pool)
Typical use timers, repeated jobs, serializing work without persistence server-side database jobs: housekeeping, imports, schedules moving a persistence call off the JavaFX thread

Plain dispatcher — work without a session

The DefaultTaskDispatcher described above is the right choice whenever the background work does not touch the database at all, or manages its own resources: recurring timers, protocol housekeeping, serialized access to some non-database resource.

SessionTaskDispatcher — the dispatcher owns a session

For background work on the database, tentackle-session provides SessionTaskDispatcher, implemented by DefaultSessionTaskDispatcher on top of DefaultTaskDispatcher. It adds exactly one thing: a Session that belongs to the dispatcher thread — exclusively. The session must not be pooled and must not be used by any other thread; since all tasks run serially on that one thread, they can share it safely.

Tasks extend AbstractSessionTask: addTask(...) injects the dispatcher's session, and the task's run() works through getSession(). Two knobs matter in long-running setups: setSessionKeptAlive(true) pings the session while the dispatcher is idle, so a remote server does not consider it dead, and setSessionClosedOnTermination(true) closes the session when the dispatcher terminates.

A complete example — an hourly cleanup job on its own session:

public class CleanupTask extends AbstractSessionTask {

  @Override
  public void run() {
    DomainContext context = Pdo.createDomainContext(getSession());
    for (Invoice invoice : Pdo.create(Invoice.class, context).selectExpired()) {
      invoice.delete();
    }
  }
}

SessionInfo info = SessionInfoFactory.getInstance().create("jobrunner", password, "myapp");
DefaultSessionTaskDispatcher dispatcher = new DefaultSessionTaskDispatcher(
        "jobs", SessionFactory.getInstance().create(info), false, 30000, 0);
dispatcher.setSessionKeptAlive(true);
dispatcher.setSessionClosedOnTermination(true);
dispatcher.start();

CleanupTask task = new CleanupTask();
task.setRepeatInterval(3_600_000);    // re-queue one hour after each run
dispatcher.addTask(task);

The best-known SessionTaskDispatcher is one you never create yourself: the ModificationTracker. Every client (and every caching server) already runs one — a live thread with an open session that accepts tasks like any other dispatcher. That is precisely what the desktop client exploits:

Rdc.bg(...) — the JavaFX client facade

In a rich desktop client, the question is rarely "which dispatcher" but "how do I get this database call off the FX thread". The answer is Rdc.bg(...): it wraps a lambda into an AbstractSessionTask, queues it on the modification tracker (or, opt-in, on a session-pool-backed thread pool), and posts the result back to the FX thread. You never create a thread, a dispatcher, or a session. The details — execution models, serialization consequences, error handling — are in Background Execution.

Rule of thumb: no database → plain DefaultTaskDispatcher; database jobs in a server or standalone process → DefaultSessionTaskDispatcher with AbstractSessionTasks; JavaFX client → Rdc.bg(...) and stop thinking about dispatchers.

See also