Java Reference

Timers and Callbacks

Introduction

Timers and callbacks are essential tools for programming, especially for control systems that require precise and reliable timing. Both core Java and third-party libraries offer various ways to create and use timers, but they do not cover all the scenarios that control systems face. Therefore, KOS provides a set of specialized callback classes that handle common and complex timer-based problems.

The term callback is used to describe these classes because they offer more than just simple timer functionality. They also provide guarantees about the execution of the callback method, such as ensuring that no callback is ever invoked concurrently, and that they are performed in order. Even if a breakpoint occurs during a callback, and more events trigger the same timer, the pending callback events are queued and executed in a sequential order.

Timers and callbacks can introduce subtle bugs and race conditions in multi-threaded applications, which can compromise the performance and safety of control systems. The KOS callback classes are designed to prevent these issues by providing concurrency and race condition guarantees. This document explains the features and benefits of KOS callbacks and shows how they can be used to solve typical and challenging control system problems.

Common Functionality

All KOS callback classes provide a baseline of common functionality:

All events are queued

A timer does not invoke a callback directly when it fires, but rather adds an event to the queue. This ensures that the order of callbacks is preserved and that none of them are skipped due to timing issues.

Callbacks executed from shared thread pool

The shared KOS worker thread pool handles the execution of callbacks. This simplifies the code and avoids the overhead of creating or managing threads.

"Inline" methods provide synchronous callbacks

You can use flushInline() and forceInline() methods in any callback to make the current thread wait until the timer event is done. Before the timer event, all the events in the queue will run. These methods are useful for making callbacks synchronous in some situations.

Timers are dynamically adjustable

You can change the timers of any callback function at any time, without causing any problems with concurrency. This means you can control the delays and intervals of your callbacks more flexibly and efficiently.

Delay variance is supported

Callbacks support the concept of variance. Consider the phenomenom where multiple clients sending periodic requests to a server will, over time, tend to align and result in server load spikes. The typical mitigation for this is to add a random component to each timer iteration on the clients. By setting the min and max variance values for a callback, a percentage window is defined for the timer, and each iteration of the timer selects a new random delay within those min and max boundaries.

TriggeredCallback

A high frequency trigger event can cause a costly operation to run repeatedly, which may affect the performance of the system. To avoid this, the TriggeredCallback class introduces a brief pause before executing the operation, which reduces the dependency between the trigger and the operation.

Decouple events from operations

For example, consider an RIFD tag that tracks the remaining volume of the ingredient in a container. As the ingredient is poured from the container, volume data is generated by the pump. This data is used to write the updated volume back to the tag. However, writing to an RFID tag is slow, typically on the order of 100ms. If volume data from the pumps arrives every 20ms, there is a disconnect between the incoming events and the operation they trigger.

A TriggeredCallback is used to decouple the pump events from the write operation, which provides control over the write policy at the same time. For example, even though writes take 100ms, the desired policy may be to only write to the tag every 1000ms to avoid monopolizing the RFID hardware. This is accomplished by creating a TriggeredCallback with a delay of 1000ms. The first call to trigger() (where a timer is not active) causes the timer to start. All future calls to trigger() (while the timer is active) are ignored. Once the timer fires, the callback is performed, writing the RFID tag. The next call to trigger() starts the process all over again. The following code sample demonstrates this.

Using a TriggeredCallback to slow down RFID updates
public class MyClass implements PumpListener {

    private TriggeredCallback triggeredCallback;

    public MyClass() {
        // Instantiate a triggered callback:
        triggeredCallback = new TriggeredCallback(1000, this::updateRfidTag);
    }

    // Called by client code when the pump is on:
    @Override
    public void onPumpIsPumping() {
        triggeredCallback.trigger();
    }

    // Called when the triggered callback timer fires:
    private void updateRfidTag() {
        // Insert code to update the RFID tag's data...
    }
}

Use variances

Extending this example, consider multiple pumps, each with a distinct RFID tag that needs to be updated. If all the pumps start at the same time, they will all trigger a write attempt after the first 1000ms and they will collide at the write operation as only a single tag can be written at a time. If the delays were staggered or randomized, overlapping write requests would be minimized and thread contention reduced. As TriggeredCallback supports variances, this problem is easily solved by setting a variance window without changing any other logic. For example, by setting a max variance of 2.0, each timer iteration would select a random value in the range of 100 - 200% of the configured timer delay. In the example above, each delay is between 1000 and 2000ms. The following code snippet adds a max variance.

Adding max variance to a callback class
public class MyClass implements PumpListener {

    public MyClass() {
        // Instantiate a triggered callback with max variance of 200%:
        triggeredCallback = new TriggeredCallback(1000, this::updateRfidTag).setMaxVariance(2.0);
    }

    // . . .
}

AdjustableCallback

The AdjustableCallback class is a versatile timer that can execute a function once or repeatedly, with the option to change the delay between calls even after the timer has started. It also supports variance, which adds a random factor to the delay to make the timer less predictable.

Use config value to adjust delay

Consider a recurring timer that can be adjusted by a config value. A great example of this is FutureService which every second broadcasts the state of every running future to any listeners so they can get progress information. The frequency of updates is driven by a config value in the service. Someone may want to shorten the delay to see if the UI displays smoother data, or may want to make the delay longer to see if it reduces the UI load, and they can do that by simply changing the config value, which in turn calls setDelay(). Since the timer rebuilds on the fly, it’s all seamless and one line of code.

Use a configuration value to adjust the delay
public class MyClass implements ConfigAware<OurConfigBean> {

    private OurConfigBean ourConfigBean = new OurConfigBean();

    private final Handle handle;
    private final AdjustableCallback adjustableCallback;

    public MyClass() {
        // Give this object a handle:
        handle = new Handle(this, "myclass1");

        // Instantiate and start the timer:
        adjustableCallback = new AdjustableCallback(true, 100, this::doSomething);
    }

    private void doSomething() {
    }

    //--- ConfigAware interface ---

    @Override
    public void onConfigChanged(BeanChanges changes) {
        // Modify the delay whenever the configuration changes:
        adjustableCallback.setDelay(ourConfigBean.getDelayInMsec()); (1)
    }

    @Override
    public OurConfigBean getConfig() {
        return ourConfigBean;
    }

    @Override
    public void setConfig(OurConfigBean ourConfigBean) {
        this.ourConfigBean = ourConfigBean;
    }

    @Override
    public Handle getHandle() {
        return handle;
    }
}
1 Update the delay when any configuration value changes

Enable/disable recurring timer

In this use case, we want to enable or disable a recurring timer based on some event.

For example, consider that you have code that periodically scans RFID tags, but you only want to scan when the dispenser door is open, not when it’s closed.

To code this, set up an AdjustableCallback timer with a desired delay and variance. When it receives a "door open" event, the timer is started; when it receives a "door closed" event, the timer is cancelled.

Here’s some code that achieves this:

Enable and disable a callback timer when dispenser door opens and closes
public class MyClass implements DoorListener {

    private final AdjustableCallback adjustableCallback;

    public MyClass() {
        // Instantiate the timer, but don't start it:
        adjustableCallback = new AdjustableCallback(true, this::readRfidTags);
        adjustableCallback.setDelay(100);
    }

    private void readRfidTags() {
    }

    @Override
    public void doorOpened() {
        adjustableCallback.start();
    }

    @Override
    public void doorClosed() {
        adjustableCallback.cancel();
    }
}

In addition, you could:

  • Add variance to the timer to support a bit of randomness to the time period.

  • Hook the delay value up to a config property, making it easy to adjust the scan frequency.

Change delay based on state

Another use case is a timer that runs at different frequencies based on state. Consider an RFID reader on a dispenser door. When the door is closed and you want to look for a technician tag, you scan quickly to have low latency, but when the door is open and you’re using RFID to scan ingredients, you want to scan the door at a lower frequency to minimize contention. In this example you have two config values, one for door open, one for door closed and simply set the delay based on the state of the door.

Change the timer’s delay based on some state
public class MyClass implements ConfigAware<OurConfigBean>, DoorListener {

    private OurConfigBean ourConfigBean = new OurConfigBean();

    private final Handle handle;
    private final AdjustableCallback adjustableCallback;

    public MyClass() {
        // Give this object a handle:
        handle = new Handle(this, "myclass3");

        // Instantiate and start the timer:
        adjustableCallback = new AdjustableCallback(true, 100, this::scanRfidTags);
    }

    private void scanRfidTags() {
    }

    @Override
    public void doorOpened() { (1)
        adjustableCallback.setDelay(ourConfigBean.getDelayWhenDoorIsOpen());
    }

    @Override
    public void doorClosed() { (2)
        adjustableCallback.setDelay(ourConfigBean.getDelayWhenDoorIsClosed());
    }

    @Override
    public OurConfigBean getConfig() {
        return ourConfigBean;
    }

    @Override
    public void setConfig(OurConfigBean ourConfigBean) {
        this.ourConfigBean = ourConfigBean;
    }

    @Override
    public Handle getHandle() {
        return handle;
    }
}
1 Set delay when the door is closed
2 Set another delay value when the door is open

Use variance

Another use case is a long timer such as checking a server for data. You might want to check the server once a day. These tend to align over timer as all servers hit the same server so using variance keeps them evenly distributed. In addition, consider the case where you want to change the delay from 24 hours to 12 hours. If you update the fleet, they are all suddenly aligned and will trigger in 12 hours. Since AdjustableCallback setDelay() is relative, if a timer had already been waiting 6 hours, it will wait 6 more to reach a total of 12, whereas a timer that has been waiting for 14 hours will trigger immediately (subject to variance). This tends to be very useful for large fleet operations.

PushPullCallback

The PushPullCallback class is used to schedule a callback after a specified delay, but allows that delay to be adjusted in various ways.

Push callback out

One use case is as a TriggeredCallback with different priorities. For example, consider how Studio processes operations. Some operations are sent from the server as a result of other users making changes and some operations are the result of the actions you perform. Both may result in performing a lot of expensive validations or cache updates, but since you don’t know about cloud based updates (you don’t know someone just made a change) you can use a long timer value, but if you then do a similar event locally you want to use a short timer value. With a PushPullCallback you can use it in pull model. The background changes call pull() with a long timer value while local changes call pull() with a short value. If a timer isn’t running, it will be started. If one is already running, it will use the shortest pull value. This essentially lets you "upgrade" a timer to the highest priority event for one cycle.

public class MyClass implements PumpListener {

    private TriggeredCallback triggeredCallback;

    public MyClass() {
        // Instantiate a triggered callback:
        triggeredCallback = new TriggeredCallback(1000, this::updateRfidTag);
    }

    // Called by client code when the pump is on:
    @Override
    public void onPumpIsPumping() {
        triggeredCallback.trigger();
    }

    // Called when the triggered callback timer fires:
    private void updateRfidTag() {
        // Here's the code that updates the RFID tag's data...
    }
}

Pull callback in

The opposite use case also exists. Consider the case where you read RFID tags to detect ingredients. Each newly detected ingredient causes an attempt to insert the ingredient using insertionService. Each insertion performs some work which then triggers a cache rebuild. Let’s say the cache rebuild has it’s own timer of 100ms. Since it takes about 50ms to read a tag and you have 36 tags to read, you’ll end up rebuilding the cache every other tag which means you’re throwing away 17 of the 18 cache rebuilds. This can contribute to startup latency. By using a push callback instead of a triggered callback, you can call push(100) instead of trigger(). Since you’ll call this about every 50ms, the push will keep pushing the cache rebuild out until the last read finishes and then the cache will be rebuilt a single time. If you happen to have an event that indicates that the last rfid read just occurred, you can also call flush() to force the cache rebuild without waiting for the remainder of the timer. If you don’t want to flush() immediately, but don’t want to wait the full delay, you can also call pull() with a shorter value which will pull the delay into the shorter value.

public class MyClass {

    private TriggeredCallback triggeredCallback;

    public MyClass() {
        triggeredCallback = new TriggeredCallback(1000, this::writeVolumeToRfidDevice)
                .setMinVariance(0.8).setMaxVariance(1.2);
    }

    private void writeVolumeToRfidDevice() {
    }

    public void updateRemainingVolume() {
        triggeredCallback.trigger();
    }
}

Summary

This article explained how to use the three callback classes for flexible operation scheduling:

  • TriggeredCallback

  • AdjustableCallback

  • PushPullCallback

Previous
Next
On this page
Java Development
Seamlessly transition from Legacy+ systems to Freestyle microdosing and advanced distributed dispense systems.
UI Development
Using KOS SDKs, integrating Consumer and Non-consumer facing UIs becomes seamless, giving you less hassle and more time to create.
Video Library
Meet some of our development team, as they lead you through the tools, features, and tips and tricks of various KOS tools.
Resources
Familiarize yourself with KOS terminology, our reference materials, and explore additional resources that complement your KOS journey.
Copyright © 2024 TCCC. All rights reserved.