Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Idle Serial Scheduler

IdleSerialScheduler is a small Java utility for running short processing ticks strictly one at a time on top of a shared executor pool.

Its main property is that while no work exists, it consumes essentially no execution resources:

  • no dedicated thread,
  • no sleeping loop,
  • no periodic polling task,
  • no watchdog ticking in the background.

It becomes active only when external code explicitly signals new work via kick().

It is designed for background loops that:

  • must never run in parallel,
  • should become active only when new work is explicitly signaled,
  • should continue processing while work remains,
  • should coalesce repeated wake-up signals.

Typical use cases:

  • outbox processing,
  • retry queues,
  • polling external system statuses,
  • draining pending records from a database,
  • lightweight background synchronization loops.

What problem it solves

A common naive approach looks like this:

while (true) {
    if (hasWork()) {
        runOnce();
    }
    Thread.sleep(1000);
}

This has obvious drawbacks:

  • it permanently occupies a thread,
  • it reacts slowly or wastes CPU depending on sleep duration,
  • it is awkward to integrate with shared executor pools,
  • it often leads to accidental parallel execution once “improved”.

In contrast, IdleSerialScheduler does not keep any background activity alive on its own while idle.
If no work exists, nothing is spinning, sleeping, polling, or waking up periodically.

IdleSerialScheduler follows a different model:

  • when work appears, external code calls kick(),
  • one processing tick runs,
  • if work still remains, the next tick is scheduled after a delay,
  • if no work remains, the scheduler becomes idle and does not occupy a thread.

Core properties

  • Strictly sequential executionrunOnce() is never executed concurrently.
  • Truly idle when no work exists — no dedicated thread, no timer loop, no watchdog task, no background polling activity.
  • Coalesced wake-ups — many kick() calls collapse into at most one queued immediate tick.
  • Delayed continuation — if work remains, processing continues automatically after the configured delay.
  • Shared-pool friendly — runs on top of a provided backend Executor.

How it works

Execution model:

new work -> kick() -> runOnce()
                   -> if work remains -> delay -> next runOnce()
                   -> if no work remains -> idle

Important: when new work arrives, external code must call kick().


Usage

ExecutorService backendPool = Executors.newFixedThreadPool(4);

AtomicInteger remaining = new AtomicInteger(10);

IdleSerialScheduler scheduler = new IdleSerialScheduler(
        backendPool,
        1,
        TimeUnit.SECONDS,
        () -> {
            // process one small batch / one tick
            System.out.println("tick");
            remaining.decrementAndGet();
        },
        () -> remaining.get() > 0,
        error -> error.printStackTrace()
);

// signal that work is available
scheduler.kick();

API overview

IdleSerialScheduler

Main constructor:

public IdleSerialScheduler(
        Executor backendPool,
        long delay,
        TimeUnit unit,
        Runnable runOnce,
        BooleanSupplier hasWork,
        Consumer<Throwable> errorHandler
)

Parameters:

  • backendPool — shared executor used for actual execution,
  • delay / unit — delay between continuation ticks,
  • runOnce — one short processing tick,
  • hasWork — cheap check whether more work remains,
  • errorHandler — receives exceptions thrown by processing tasks.

SerialExecutor

A low-level utility that guarantees strict sequential execution of submitted tasks on top of a backend executor.


Error handling

There are two different failure categories.

1. Task execution failure

If user task code throws:

  • the error is passed to errorHandler,
  • queue progress continues,
  • subsequent tasks are still processed.

2. Backend submission failure

If the backend executor rejects task submission:

  • the exception is propagated to the caller,
  • queued head task is not dropped,
  • execution can continue later when submission succeeds again.

Semantics and limitations

This utility provides best-effort coalescing.

That means:

  • repeated kick() calls are aggressively collapsed,
  • no parallel ticks will happen,
  • but edge interleavings around an already-running tick and an already-planned delayed continuation are intentionally handled in a practical, not mathematically minimal, way.

This is a deliberate tradeoff for keeping the implementation small, predictable, and robust.

Also note:

  • hasWork should be cheap,
  • runOnce should perform bounded work and return reasonably quickly,
  • this is not a cron scheduler or general job framework.

Testing

The project includes JUnit 5 tests for:

  • coalescing of immediate kicks,
  • absence of parallel execution,
  • delayed continuation while work remains,
  • no self-rescheduling after work is exhausted,
  • continued progress after task failure,
  • behavior under concurrent kicks,
  • backend rejection recovery,
  • serial executor guarantees.

License

MIT

About

Java concurrency utility for running short processing ticks strictly one at a time, with coalesced wake-ups and no dedicated thread, timer loop, or watchdog while idle.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages