// =================================================================================================
// Copyright 2014 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =================================================================================================
package com.twitter.common.util;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.easymock.Capture;
import org.easymock.IAnswer;
import org.junit.Test;
import com.twitter.common.base.Command;
import com.twitter.common.quantity.Amount;
import com.twitter.common.quantity.Time;
import com.twitter.common.util.testing.FakeClock;
import static org.easymock.EasyMock.anyBoolean;
import static org.easymock.EasyMock.anyLong;
import static org.easymock.EasyMock.capture;
import static org.easymock.EasyMock.captureLong;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class LowResClockTest {
/**
* A FakeClock that overrides the {@link FakeClock#advance(Amount) advance} method to allow a
* co-operating thread to execute a synchronous action via {@link #doOnAdvance(Command)}.
*/
static class WaitingFakeClock extends FakeClock {
private final SynchronousQueue<CountDownLatch> signalQueue =
new SynchronousQueue<CountDownLatch>();
@Override
public void advance(Amount<Long, Time> period) {
super.advance(period);
CountDownLatch signal = new CountDownLatch(1);
try {
signalQueue.put(signal);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
signal.await();
} catch (InterruptedException e) {
// ignore
}
}
void doOnAdvance(Command action) throws InterruptedException {
CountDownLatch signal = signalQueue.take();
action.execute();
signal.countDown();
}
}
static class Tick implements Command {
private final Clock clock;
private final long period;
private final Runnable advancer;
private long time;
Tick(Clock clock, long startTime, long period, Runnable advancer) {
this.clock = clock;
time = startTime;
this.period = period;
this.advancer = advancer;
}
@Override
public void execute() {
if (clock.nowMillis() >= time + period) {
advancer.run();
time = clock.nowMillis();
}
}
}
@Test
public void testLowResClock() {
final WaitingFakeClock clock = new WaitingFakeClock();
final long start = clock.nowMillis();
ScheduledExecutorService mockExecutor = createMock(ScheduledExecutorService.class);
final Capture<Runnable> runnable = new Capture<Runnable>();
final Capture<Long> period = new Capture<Long>();
mockExecutor.scheduleAtFixedRate(capture(runnable), anyLong(), captureLong(period),
eq(TimeUnit.MILLISECONDS));
expectLastCall().andAnswer(new IAnswer<ScheduledFuture<?>>() {
public ScheduledFuture<?> answer() {
final Thread ticker = new Thread() {
@Override
public void run() {
Tick tick = new Tick(clock, start, period.getValue(), runnable.getValue());
try {
while (true) {
clock.doOnAdvance(tick);
}
} catch (InterruptedException e) {
/* terminate */
}
}
};
ticker.setDaemon(true);
ticker.start();
final ScheduledFuture<?> future = createMock(ScheduledFuture.class);
final AtomicBoolean stopped = new AtomicBoolean(false);
expect(future.isCancelled()).andAnswer(new IAnswer<Boolean>() {
@Override
public Boolean answer() throws Throwable {
return stopped.get();
}
}).anyTimes();
expect(future.cancel(anyBoolean())).andAnswer(new IAnswer<Boolean>() {
@Override
public Boolean answer() throws Throwable {
ticker.interrupt();
stopped.set(true);
return true;
}
});
replay(future);
return future;
}
});
replay(mockExecutor);
LowResClock lowRes = new LowResClock(Amount.of(1L, Time.SECONDS), mockExecutor, clock);
long t = lowRes.nowMillis();
clock.advance(Amount.of(100L, Time.MILLISECONDS));
assertEquals(t, lowRes.nowMillis());
clock.advance(Amount.of(900L, Time.MILLISECONDS));
assertEquals(t + 1000, lowRes.nowMillis());
clock.advance(Amount.of(100L, Time.MILLISECONDS));
assertEquals(t + 1000, lowRes.nowMillis());
lowRes.close();
try {
lowRes.nowMillis();
fail("Closed clock should throw exception!");
} catch (IllegalStateException e) {
/* expected */
}
}
}