0

I am trying to avoid sleeping the current thread until a ScheduledFuture executes with a 0 delay. Unfortunately, I can't find a hook against the future that informs when the runnable executes. The future in question wraps a guava cache.put(key,value) operation. The runnable should be called in advance of the cache expiring the key...essentially, I want one key to never expire.

    final Runnable refresh = new Runnable() {
        @Override
        public void run()
        {
            cache.put( key, value );
        }
    };

    // replace the token when 95% of the ttl has passed
    long refreshInterval = (long)( keyExpires * 1000 *
                                   0.5 );

    // execute the future task and then delay the current thread long enough for the
    // executor to process the runnable. 50 ms should be long enough.
    ScheduledFuture<?> future = scheduler.scheduleAtFixedRate( refresh,
                                                               0,
                                                               refreshInterval,
                                                               TimeUnit.MILLISECONDS );

   /// this is the code I'd like to avoid
   try {
        Thread.sleep( 50 );
    } catch( InterruptedException e1 ) {} catch( ExecutionException e ) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

The executor service does run the code immediately but there's lag time to spin up a thread. That lag will be system specific, so I want to avoid an arbitrary sleep.

I am using ScheduledThreadPoolExecutor to create the ScheduledFuture and I can get the behaviour I want using an accessor of that type, like isDone(). However, that seems hacky too. Is there a cleaner implementation that offers the behaviour of sleeping the current thread without using a side effect of the Executor service?

Thanks, Robin

Edit: to show the test that fails without a Thread.sleep()

    cache.putNonExpiring( "key", "value" );
    Assert.assertNotNull( "Immediate get should have value", cache.get( "key" ) );

To work correctly, a put(key,value) should be performed synchronously to allow an immediate get(key) operation.

Robin Coe
  • 750
  • 7
  • 28

1 Answers1

1

Perhaps you could use a semaphore or other synchronization type that the current thread blocks on until the refresh runnable releases the semaphore

// A semaphore initialized with no permits
final Semaphore runnableExecuting = new Sempahore(0);

final Runnable refresh = new Runnable()
{
    @Override
    public void run()
    {
        // Release one permit.  This should unblock the thread
        // scheduled this task.  After the initial releasing
        // the semaphore is essentially unneeded
        runnableExecuting.release();

        // Your code
    }
}

// After executor scheduling

// Attempt to acquire a permit, which the semphore initially has none.
// This will block until a permit becomes available
runnableExecuting.acquire();
V3V
  • 107
  • 5
  • I did try a future.wait(), although not sure the future sends a notify() when it first runs. Problem there was powermock throwing an invalidmonitorException (or similar)...seems powermock uses a wait() as well. I tried other synchronization around the cache itself but that never seemed to effect the running threads. – Robin Coe Nov 02 '17 at 16:49
  • Yes, on it's own nothing would call future.notify. Did you try using Java's Semaphore? So create a new semphore with zero permits. Then in your refresh runnable call semaphore.release and after your executor call semaphore.acquire – V3V Nov 02 '17 at 17:23
  • I'm not sure I understand that approach. If I create a semaphore on the main thread, how to I block on it while the second thread spins up and then releases the lock? Could be my knowledge of permits? At the core of this question is a code smell and I'm thinking of changing the implementation to use a map for storing permanent keys is a better approach. Sine I am wrapping the guava cache, I can use a facade to perform a get(key) that looks at both maps. It does suffer a side effect of the same key being added to both maps, though. Perhaps sample code to explain the permits approach? thx – Robin Coe Nov 02 '17 at 17:35
  • Interesting solution and it works after one small change. I had to set the permit count to 1. Without that, the code hangs on the acquire(). I was seeing two new threads getting created. Not sure why but, I was seeing two invocations of the scheduler call and so I saw a main Thread and two pooled threads (pool1 - Thread 1 and pool1 - Thread 2), all pointing at the acquire() line in the breakpoint stack. – Robin Coe Nov 02 '17 at 20:05