CH

September 25, 2016

A Robust, Testable (and tested), Reentrant Timer in Java and Clojure

Filed under: clojure, java, software development — Benjamin Vulpes @ 6:30 p.m.

I do most of my for-pay web work in Clojure. It's a Lisp, that the popularity of JVM-hosted languages has made acceptable in both Enterpriselandia and Startuplandia1, and so wasn't the worst choice in the world for this particular kid (already entirely competent with the kazoos of Python and Java) to build a new base of competence (and at the time I decided to develop more than just base competence, I hoped it'd yield some marketability as well. Marketability it might have had, but not for the product I was marketing) in. Java, being a product of the web generation ("hey, we got them using garbage collection, whaddaya want?!") for the web generation, comes with a...large set of tools to handle the variously surprising and predictable braindamage the practicing hacker encounters in daily webshittery. Clojure provides a baseline-acceptable set of asbestos gloves for working with the JVM2 that I've wielded to great effect building postback webapps, and APIs for single-page JS applications.

Today I will share with you a smattering of non-production code, demonstrating testability of an important subsystem that every webapp develops at some point: the scheduled job. Find attached a tested, and further testable system for scheduling events, written in plain old Java, and called from Clojure (this is excellent study material for novice Clojurians wrapping their heads around interop with the host platform). The code and its tests assume that the timer and its testable implementation are swappable in your system (or to use the self-aggrandizing software industry lingo, that you thought far enough ahead when writing whatever you're building that it provides for Dependency Injection).

A Java interface that both the timer and its testable version will implement:

public interface Timer {

    public interface TimerCallback {
        public void execute();
    }

    UUID schedule(TimerCallback callback, Date date);
    void cancel(UUID uuid);
}

The implementation:

public class ConcreteTimer implements Timer {

    private HashMap timers;

    class ConcreteTimerTask extends TimerTask {

        TimerCallback callback;

        public ConcreteTimerTask(TimerCallback callback) {
            this.callback = callback;
        }

        public void run() {
            callback.execute();
        }
    }

    public ConcreteTimer() {
        this.timers = new HashMap<>();
    }

    public UUID schedule(TimerCallback callback, Date date) {
        ConcreteTimerTask task = new ConcreteTimerTask(callback);
        Timer timer = new Timer();
        UUID uuid = UUID.randomUUID();
        timer.schedule(task, date);
        timers.put(uuid, timer);
        return uuid;
    }

    public Date getDate() {
        return new Date();
    }

    public HashMap getTimers(){
        return timers;
    }

    public void cancel(UUID uuid) {
        Timer timer = timers.get(uuid);
        timers.remove(uuid);
        if (timer != null) {
            timer.cancel();
        }
    }
}

The testable system:

public class MockTimer implements Timer {

    public class MockTimerTask extends TimerTask {

        TimerCallback callback;

        public MockTimerTask(TimerCallback callback) {
            this.callback = callback;
        }

        public void run() {
            callback.execute();
        }

    }

    public class MockTimerObject {
        public UUID uuid;
        public MockTimerTask task;
        public Date date;

        public MockTimerObject(UUID uuid, MockTimerTask task, Date date) {
            this.uuid = uuid;
            this.task = task;
            this.date = date;
        }

        public Date getDate() {
            return this.date;
        }
    }

    private List timers;
    private Date mockDate;

    public MockTimer(long init_millis) {
        this.timers = new ArrayList();
        this.mockDate = new Date(init_millis);
    }

    public Date getDate() {
        return mockDate;
    }

    public List getTimers() {
        return timers;
    }

    public UUID schedule(TimerCallback callback, Date date) {
        MockTimerTask task = new MockTimerTask(callback);
        UUID uuid = UUID.randomUUID();
        timers.add(new MockTimerObject(uuid, task, date));
        return uuid;
    }

    public void cancel(UUID uuid) {
        timers.removeIf((MockTimerObject m) -> m.uuid.equals(uuid));
    }

    public void advance(long millis) {
        timers.sort((o1, o2) -> o1.getDate().compareTo(o2.getDate()));
        if (timers.size() > 0) {
            MockTimerObject callback = timers.remove(0);
            if (callback.getDate().getTime() > (mockDate.getTime() + millis)) {
                mockDate = new Date(mockDate.getTime() + millis);
                timers.add(callback);
                return;
            } else if (callback.getDate().getTime() <= (mockDate.getTime() + millis)) {
                Date oldDate = mockDate;
                long target = mockDate.getTime() + millis;
                mockDate = callback.getDate();
                callback.task.run();
                advance(target - callback.getDate().getTime());
                return;
            }
        } else {
            mockDate = new Date(mockDate.getTime() + millis);
        }
    }
}

Calling it from Clojure:

(defn instantiate-callback [callback]
  (reify Timer$TimerCallback
    (execute [this] (callback))))

(deftest can-advance-mock-timer
  (let [then-millis (long 1e6)
        mock-timer (MockTimer. (long 0))]
    (.advance mock-timer then-millis)
    (is (= (Date. then-millis) (.getDate mock-timer)))))

(deftest mock-timer-runs-tasks
  (let [when (Date. (long 1e6))
        mock-timer (MockTimer. (long 0))
        callback-fn (test-utils/dummy-fn)
        timer-callback (instantiate-callback #(callback-fn 1))]
    (.schedule mock-timer timer-callback when)
    (.advance mock-timer (long (+ 1 1e6) ))
    (is (= '((1)) (callback-fn)))))

(deftest mock-timer-runs-tasks-in-order
  (let [time-1 (long 1e6)
        time-2 (long 1e7)
        mock-timer (MockTimer. (long 0))
        callback-fn (test-utils/dummy-fn)
        callback-1 (instantiate-callback #(callback-fn 1))
        callback-2 (instantiate-callback #(callback-fn 2))]
    (.schedule mock-timer callback-2 (Date. time-2))
    (.schedule mock-timer callback-1 (Date. time-1))
    (.advance mock-timer time-2)
    (is (= ['(1) '(2)] (callback-fn)))))

(deftest mock-timer-works-reentrantly
  (let [time-1 (long 1)
        time-2 (long 2)
        time-3 (long 3)
        mock-timer (MockTimer. (long 0))
        callback-fn-3 (test-utils/dummy-fn)
        callback-3 (instantiate-callback
                    #(callback-fn-3 3))
        callback-2 (instantiate-callback
                    #(.schedule mock-timer callback-3 (Date. time-2)))
        callback-1 (instantiate-callback
                    #(.schedule mock-timer callback-2 (Date. time-1)))]
    (.schedule mock-timer callback-1 (Date. time-1))
    (.advance mock-timer (long 5))
    (is (= '((3)) (callback-fn-3)))))

Writing a timer system in this style blesses your entire system with a new axis of testability. You can construct your mock timer objects in test setup, pass them into (for example) functions responsible for processing HTTP requests, realizing side-effects, and then returning HTTP responses, and in your test suite assert that after a certain amount of time has elapsed, that the timers perform the jobs as designed. You can even test that reentrantly-scheduled jobs ran, and that they performed their side-effects as expected.

On the flip side, this kind of onanistic software development balloons budgets and drives teams to blow clean through their deadlines. You're probably better off just instantiating a java.util.Timer whenever you need one. Testability of complex systems is expensive, and your customers will probably never notice if the jobs don't run correctly anyways. Even if stakeholders do observe some flaws in your program, you can slow-roll them with reproduction requests ("Sorry, that's not really detailed enough. What do I do after making a new Frobulator to prevent the Frobulator Inspector from running next week?" while you labor frantically behind the scenes (manually testing everything, of course, because your team's moving too fast to write tests) to run down and fix those bugs.

  1. Enterpriselandia cares only about the IBM-ness of any given technology choice ("Nobody ever got fired for buying Microsoft!"), and the children of Startuplandia, absent of course any sort of historical perspective on software development care only about the top shiny. []
  2. It still features a few glaring warts: system startup time is so hilariously long that luminaries have bolted libraries together "...for managing the lifecycle and dependencies of software components which have runtime state", a fancy sidestep of the issue that the base REPL (your interpreter/compiler in a Lisp) takes so long to boot that folks prefer to set up and tear down running systems by hand rather than restarting the runtime; and the utter inability of the compiler to perform any sort of type checking, along with the concomittant attempts to paper over the issue: Schema, Spec, and the disingenuous notion that compile time type checking doesn't matter at all:

    While I may actually agree with Halloway on the point above from the managerial perspective, this verges on a claim that "Stakeholders don't care if drivers are working the stick or piloting an automatic". While that may be true for your VP of Marketing, I guarantee you that whichever human is responsible for controlling fleet maintenance costs does in fact give a shit, and does in fact prefer that drivers not operate the transmissions by hand. []

2 Comments »

  1. I guarantee you that whichever human is responsible for controlling fleet maintenance costs does in fact give a shit, and does in fact prefer that drivers not operate the transmissions by hand

    Depends very much on the hiring process/practices. If company of ninnies, yes. If selective, no, but exactly the opposite.

RSS feed for comments on this post. TrackBack URL

Reply

« Best ecommerce landing page of 2017 --- The CIA's flickr account »