#StackBounty: #java Implement Simulated Thread#sleep()

Bounty: 50

Background

I am designing my software so that I can easily perform unit tests. I have an IClock interface which, among other methods, has
IClock#wait(TimeUnit timeUnit, long duration). This method will halt the current thread for timeUnit duration (i.e. 1 second).

There are two implementations of the IClock interface:

  • SimulatedClock: has methods to manually increase the time stored in the clock
  • RealClock: time increases automatically by referencing System.currentTimeMillis()

This is the default method for IClock#wait(...):

/**
     * Locks current thread for specified time
     *
     * @param timeUnit
     * @param dt
     */
    default void wait(TimeUnit timeUnit, long dt)
    {
        Lock lock = new ReentrantLock();
        scheduleIn(timeUnit, dt, lock::unlock);
        lock.lock();
    }

Problem

The current way I want simulated unit tests to work is

  1. Start threads
  2. Wait until all threads are either done or in a blocked state (I am assuming if they are blocked, they have called IClock#wait(...))
  3. If all threads are finished, finish. Else, increase SimulatedClock time by one millisecond.

However, what is really happening is:

  1. Start threads
  2. Start increasing time even though threads have not called IClock#wait() for the first time.

So, what I need to do is to be able to determine when all threads are done or blocked. Although this can be done with Thread#getState(), I would rather employ a solution which is more elegant and works with ForkJoinPool.

Full Code

GitHub

SimulatedClock

package com.team2502.ezauton.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class SimulatedClock implements IClock
{

    private long time = 0;

    private List<Job> jobs = new ArrayList<>();

    public SimulatedClock() {}

    public void init()
    {
        init(System.currentTimeMillis());
    }

    public void init(long time)
    {
        setTime(time);
    }

    /**
     * Add time in milliseconds
     *
     * @param dt millisecond increase
     * @return The new time
     */
    public long addTime(long dt)
    {
        setTime(getTime() + dt);
        return getTime();
    }

    /**
     * Adds time with units
     *
     * @param timeUnit
     * @param value
     */
    public void addTime(TimeUnit timeUnit, long value)
    {
        addTime(timeUnit.toMillis(value));
    }

    /**
     * Add one millisecond and returns new value
     *
     * @return The new time
     */
    public long incAndGet()
    {
        return addTime(1);
    }

    /**
     * Increment a certain amount of times
     *
     * @param times
     */
    public void incTimes(long times, long dt)
    {
        long init = getTime();
        long totalDt = times * dt;
        for(int i = 0; i < times; i++)
        {
            if(!jobs.isEmpty())
            {
                addTime(dt);
            }
            else
            {
                break;
            }
        }
        setTime(init + totalDt);
    }

    /**
     * Increment a certain amount of times
     *
     * @param times
     * @return
     */
    public void incTimes(long times)
    {
        incTimes(times, 1);
    }

    @Override
    public long getTime()
    {
        return time;
    }

    public void setTime(long time)
    {
        jobs.removeIf(job -> {
            if(job.getMillis() < time)
            {
                job.getRunnable().run();
                return true;
            }
            return false;
        });

        this.time = time;
    }

    @Override
    public void scheduleAt(long millis, Runnable runnable)
    {
        if(millis < getTime())
        {
            throw new IllegalArgumentException("You are scheduling a task for before the current time!");
        }
        jobs.add(new Job(millis, runnable));
    }

    private static class Job
    {
        private final long millis;
        private final Runnable runnable;

        public Job(long millis, Runnable runnable)
        {
            this.millis = millis;
            this.runnable = runnable;
        }

        public long getMillis()
        {
            return millis;
        }

        public Runnable getRunnable()
        {
            return runnable;
        }
    }
}

Simulation

package com.team2502.ezauton.command;

import com.team2502.ezauton.utils.SimulatedClock;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Simulation
{

    private final SimulatedClock simulatedClock;
    private List<IAction> actions = new ArrayList<>();

    public Simulation()
    {
        simulatedClock = new SimulatedClock();
    }

    public SimulatedClock getSimulatedClock()
    {
        return simulatedClock;
    }

    public Simulation add(IAction action)
    {
        actions.add(action);
        return this;
    }

    /**
     * @param timeoutMillis Max millis
     */
    public void run(long timeoutMillis)
    {
        simulatedClock.init();

        actions.forEach(action -> new ThreadBuilder(action, simulatedClock).buildAndRun());

        simulatedClock.incTimes(timeoutMillis);

        // Need to wait until all threads are finished
        if(!ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.SECONDS))
        {
            throw new RuntimeException("Simulator did not finish in a second.");
        }

    }

    public void run(TimeUnit timeUnit, long value)
    {
        run(timeUnit.toMillis(value));
    }
}

Example Unit Test

@Test
public void testSimpleAction()
{
    AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    Simulation simulation = new Simulation();
    simulation.add(new DealyedAction((TimeUnit.SECONDS, 5) -> atomicBoolean.set(true)));
    simulation.run(TimeUnit.SECONDS, 100);
    Assert.assertTrue(atomicBoolean.get());
}


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.