Aggressor Aggressor - 4 months ago 23
Java Question

ScheduledExecutorService Is Only Firing Once

I looked at
ScheduledExecutorService only loops once

But it doesn't seem to address my issue.

I have a custom timer and when I hit

start
it is supposed to fire a callback every second:

/**
* Starts the timer. If the timer was already running, this call is ignored.
*/
public void start()
{
if (_isRunning)
{
return;
}

_isRunning = true;

// Schedules repeated task that fires each time at the interval given
Log.d("Timer", "Starting execution");
_future = _execService.scheduleWithFixedDelay(new Runnable()
{
@Override
public void run()
{
Log.d("Timer", "Fire");

_elapsedTime += PausableTimer.this._interval;

// If time has exceeded duration, stop timer
if (_duration > 0 && _elapsedTime >= _duration)
{
Log.d("Timer", "Finish");
onFinish();
_future.cancel(false);
}
}
}, 0, _interval, TimeUnit.MILLISECONDS);
}


Here is how I invoke the timer:

_timer = new PausableTimer(1000, PausableTimer.DURATION_INFINITY);
_timer.start();


This is creating a timer that fires in 1000 millisecond intervals for inifity.

However my logs only show it firing once. The "Finish" log is not appearing so I know it hasn't ended.

Do you know why this is only firing once?

Update

/**
* Timer that can play and pause.
*/
public class PausableTimer extends Timer
{
public static final int DURATION_INFINITY = -1;

private Callbacks.VoidCallback _onTick;
private Callbacks.VoidCallback _onFinish;

private volatile boolean _isRunning = false;
private long _interval;
private long _elapsedTime;
private long _duration;
private ScheduledExecutorService _execService = Executors.newSingleThreadScheduledExecutor();
private Future<?> _future = null;

/**
* Creates a pausable timer.
* @param interval The time gap between each tick in millis.
* @param duration The period in millis for which the timer should run.
* Set it to {@code Timer#DURATION_INFINITY} if the timer has to run indefinitely.
*/
public PausableTimer(long interval, long duration)
{
_interval = interval;
_duration = duration;
_elapsedTime = 0;
_isRunning = false;
}




/// LIFE CYCLE



/**
* Starts the timer. If the timer was already running, this call is ignored.
*/
public void start()
{
if (_isRunning)
{
Log.d("Timer", "already started running");
return;
}

_isRunning = true;

// Schedules repeated task that fires each time at the interval given
Log.d("Timer", "Starting execution");
_future = _execService.scheduleWithFixedDelay(new Runnable()
{
@Override
public void run()
{
Log.d("Timer", "Fire");
onTick();

_elapsedTime += PausableTimer.this._interval;

// If time has exceeded duration, stop timer
if (_duration > 0 && _elapsedTime >= _duration)
{
Log.d("Timer", "Finish");
onFinish();
_future.cancel(false);
}
}
}, 0, PausableTimer.this._interval, TimeUnit.MILLISECONDS);
}

/**
* Pauses the timer.
*/
public void pause()
{
if(!_isRunning)
{
return;
}

_future.cancel(false);
_isRunning = false;
}

/**
* Resumes the timer if it was paused, else starts the timer.
*/
public void resume()
{
start();
}

/**
* Called periodically with the _interval set as the delay between subsequent calls.
* Fires tick callback if set.
*/
private void onTick()
{
if (_onTick != null)
{
_onTick.callback();
}
}

/**
* Called once the timer has run for the specified _duration.
* If the _duration was set as infinity, then this method is never called.
* Fires finished callback if set.
*/
protected void onFinish()
{
if (_onFinish != null)
{
_onFinish.callback();
}

_isRunning = false;
}

/**
* Stops the timer. If the timer is not running, then this call does nothing.
*/
public void cancel()
{
pause();
_elapsedTime = 0;
}




/// GETTERS



/**
* @return the elapsed time (in millis) since the start of the timer.
*/
public long getElapsedTime()
{
return _elapsedTime;
}

/**
* @return the time remaining (in millis) for the timer to stop.
* If the _duration was set to {@code Timer#DURATION_INFINITY}, then -1 is returned.
*/
public long getRemainingTime()
{
if (_duration <= PausableTimer.DURATION_INFINITY)
{
return PausableTimer.DURATION_INFINITY;
}

return _duration - _elapsedTime;
}






/// BINDERS



/**
* @return true if the timer is currently running, and false otherwise.
*/
public boolean isRunning()
{
return _isRunning;
}

/**
* Binds onTick callback.
*/
public void bindOnTick(Callbacks.VoidCallback callback)
{
_onTick = callback;
}

/**
* Binds onFinish callback.
* @param callback
*/
public void bindOnFinish(Callbacks.VoidCallback callback)
{
_onFinish = callback;
}
}


Here is a typical log. Basically what is happening is I start it up, wait 10-15 seconds, then start it again. It is supposed to fire every second. However it fires once or twice, then doesn't fire until its restarted.


D/Timer: Reset timer

D/Timer: Starting execution

D/Timer: Fire

D/Timer: Reset timer

D/Timer: Starting execution

D/Timer: Fire

D/Timer: Fire

D/Timer: Reset timer

D/Timer: Starting execution


To be extra clear, here is some invoking code I use:

private void restartTimer()
{
if (_timer != null)
{
_timer.cancel();
}
Log.d("Timer", "Reset timer");

_timer = new PausableTimer(1000, PausableTimer.DURATION_INFINITY);
_timer.bindOnTick(new Callbacks.VoidCallback()
{
@Override
public void callback()
{
decrementTimeRemaining();
}
});
_timer.start();
}


SOLUTION

After figuring out that the
onTick()
call in my
run()
was causing the thread to get stopped, I solved this by dispatching the
onTick()
call to the main thread:

_future = _execService.scheduleWithFixedDelay(new Runnable()
{
@Override
public void run()
{
Log.d("Timer", "Run begin");

Runnable task = new Runnable()
{
@Override
public void run()
{
Log.d("Timer", "Main thread tick");
PausableTimer.this.onTick();
}
};
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(task);

_elapsedTime += PausableTimer.this._interval;
Log.d("Timer", "Run middle");


// If time has exceeded duration, stop timer
if (_duration > 0 && _elapsedTime >= _duration)
{
Log.d("Timer", "Finish");
onFinish();
_future.cancel(false);
}
}
}, 0, PausableTimer.this._interval, TimeUnit.MILLISECONDS);

Answer

Very interesting cause of the problem (to me at least).

This issue was my onTick() callback that I was firing:

I noticed something very odd with my logs. The run() logs before the onTick() were firing, and the ones below it where not:

    Log.d("Timer", "Starting execution");
    _future = _execService.scheduleWithFixedDelay(new Runnable()
    {
        @Override
        public void run()
        {
            Log.d("Timer", "Run begin"); // fires 

            onTick(); // when I remove this, all below logs fire!

            _elapsedTime += PausableTimer.this._interval;
            Log.d("Timer", "Run middle"); // didn't fire
            Log.d("Timer", "Elapsed time " + _elapsedTime); // didn't fire
            Log.d("Timer", "Duration " + _duration); // didn't fire

            // If time has exceeded duration, stop timer
            if (_duration > 0 && _elapsedTime >= _duration)
            {
                Log.d("Timer", "Finish"); // didn't fire
                onFinish();
                _future.cancel(false);
            }

            Log.d("Timer", "Run End"); // didn't fire
        }
    }, 0, PausableTimer.this._interval, TimeUnit.MILLISECONDS);

When I removed the onTick() all the logs fired.

I suspect something gets mucked up when I try to go on the main thread from here with the onTick().

I'm not exactly sure yet, but this is the reason that the timer was only firing once, the onTick() call mucks it up.

I will continue to investigate further and am open to input you might have on this.

Solution Dispatched the callback on the main thread:

    _future = _execService.scheduleWithFixedDelay(new Runnable()
    {
        @Override
        public void run()
        {
            Log.d("Timer", "Run begin");

            Runnable task = new Runnable()
            {
                @Override
                public void run()
                {
                    Log.d("Timer", "Main thread tick");
                    PausableTimer.this.onTick();
                }
            };
            Handler mainHandler = new Handler(Looper.getMainLooper());
            mainHandler.post(task);

            _elapsedTime += PausableTimer.this._interval;
            Log.d("Timer", "Run middle");


            // If time has exceeded duration, stop timer
            if (_duration > 0 && _elapsedTime >= _duration)
            {
                Log.d("Timer", "Finish");
                onFinish();
                _future.cancel(false);
            }
        }
    }, 0, PausableTimer.this._interval, TimeUnit.MILLISECONDS);
Comments