package com.cloudhopper.commons.util.windowing; /* * #%L * ch-commons-util * %% * Copyright (C) 2012 Cloudhopper by Twitter * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License 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. * #L% */ // third party imports import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import org.junit.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // my imports //import net.cloudhopper.commons.util.ByteBuffer; /** * Tests Windowing class and packages. * @author joelauer (twitter: @jjlauer or <a href="http://twitter.com/jjlauer" target=window>http://twitter.com/jjlauer</a>) */ public class WindowTest { private static final Logger logger = LoggerFactory.getLogger(WindowTest.class); @Test public void usage() throws Exception { Window<Integer,String,String> window = new Window<Integer,String,String>(1); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(0, window.getSize()); WindowFuture<Integer,String,String> future0 = window.offer(0, "Request0", 100); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(1, window.getSize()); Assert.assertEquals(0, window.getFreeSize()); Assert.assertEquals(1, future0.getWindowSize()); window.cancel(0); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(0, window.getSize()); Assert.assertEquals(1, window.getFreeSize()); WindowFuture<Integer,String,String> future1 = window.offer(1, "Request1", 100); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(1, window.getSize()); Assert.assertEquals(0, window.getFreeSize()); Assert.assertEquals(1, future1.getWindowSize()); window.complete(1, "Response1"); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(0, window.getSize()); Assert.assertEquals(1, window.getFreeSize()); WindowFuture<Integer,String,String> future2 = window.offer(2, "Request2", 100); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(1, window.getSize()); Assert.assertEquals(0, window.getFreeSize()); Assert.assertFalse(future2.isDone()); Assert.assertFalse(future2.isCancelled()); Assert.assertFalse(future2.isSuccess()); Assert.assertNull(future2.getCause()); Assert.assertEquals(1, future2.getWindowSize()); // trigger this from the future now future2.fail(new Exception("Test Cause")); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(0, window.getSize()); Assert.assertEquals(1, window.getFreeSize()); Assert.assertTrue(future2.isDone()); Assert.assertFalse(future2.isCancelled()); Assert.assertFalse(future2.isSuccess()); Assert.assertNotNull(future2.getCause()); WindowFuture<Integer,String,String> future3 = window.offer(3, "Request3", 100); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(1, window.getSize()); Assert.assertEquals(0, window.getFreeSize()); Assert.assertFalse(future3.isDone()); Assert.assertFalse(future3.isCancelled()); Assert.assertFalse(future3.isSuccess()); Assert.assertNull(future3.getCause()); Assert.assertEquals(1, future3.getWindowSize()); // trigger this from the future now long now = System.currentTimeMillis(); future3.complete("Response3", now); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(0, window.getSize()); Assert.assertEquals(1, window.getFreeSize()); Assert.assertTrue(future3.isDone()); Assert.assertFalse(future3.isCancelled()); Assert.assertTrue(future3.isSuccess()); Assert.assertNull(future3.getCause()); Assert.assertEquals(now, future3.getDoneTimestamp()); WindowFuture<Integer,String,String> future4 = window.offer(4, "Request4", 100); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(1, window.getSize()); Assert.assertEquals(0, window.getFreeSize()); Assert.assertFalse(future4.isDone()); Assert.assertFalse(future4.isCancelled()); Assert.assertFalse(future4.isSuccess()); Assert.assertNull(future4.getCause()); Assert.assertEquals(1, future4.getWindowSize()); // trigger this from the future now future4.cancel(now); Assert.assertEquals(1, window.getMaxSize()); Assert.assertEquals(0, window.getSize()); Assert.assertEquals(1, window.getFreeSize()); Assert.assertTrue(future4.isDone()); Assert.assertTrue(future4.isCancelled()); Assert.assertFalse(future4.isSuccess()); Assert.assertNull(future4.getCause()); Assert.assertEquals(now, future4.getDoneTimestamp()); } @Test public void requestFutureAndWindowFuture() throws Exception { Window<Integer,String,String> window = new Window<Integer,String,String>(1); Integer i = new Integer(1); String request = "Request"+i; WindowFuture<Integer,String,String> requestFuture = window.offer(i, request, 100); Assert.assertEquals(new Integer(i), requestFuture.getKey()); Assert.assertEquals(request, requestFuture.getRequest()); Assert.assertEquals(true, requestFuture.getAcceptTimestamp() > 0); Assert.assertEquals(null, requestFuture.getResponse()); Assert.assertFalse(requestFuture.hasDoneTimestamp()); Assert.assertEquals(0, requestFuture.getDoneTimestamp()); Assert.assertEquals(false, requestFuture.isCancelled()); Assert.assertEquals(false, requestFuture.isDone()); Assert.assertEquals(false, requestFuture.isSuccess()); Assert.assertEquals(-1, requestFuture.getAcceptToDoneTime()); // this should timeout waiting for a response Assert.assertFalse(requestFuture.await(100)); // mimic a response is received String response = "Response"+i; WindowFuture<Integer,String,String> responseFuture = window.complete(i, response); Assert.assertEquals(new Integer(i), responseFuture.getKey()); Assert.assertEquals(request, responseFuture.getRequest()); Assert.assertEquals(true, responseFuture.getAcceptTimestamp() > 0); Assert.assertEquals(response, responseFuture.getResponse()); Assert.assertEquals(true, responseFuture.getDoneTimestamp() > 0); Assert.assertEquals(false, requestFuture.isCancelled()); Assert.assertEquals(true, requestFuture.isDone()); Assert.assertEquals(true, requestFuture.isSuccess()); Assert.assertEquals(true, requestFuture.getAcceptToDoneTime() > 0); // this should succeed now since a response was received Assert.assertTrue(requestFuture.await(100)); String response0 = requestFuture.getResponse(); Assert.assertEquals(response, response0); Assert.assertEquals(request, requestFuture.getRequest()); Assert.assertEquals(true, requestFuture.getAcceptTimestamp() > 0); Assert.assertEquals(response, requestFuture.getResponse()); Assert.assertEquals(true, requestFuture.getDoneTimestamp() > 0); Assert.assertEquals(false, requestFuture.isCancelled()); Assert.assertEquals(true, requestFuture.isDone()); Assert.assertEquals(true, requestFuture.isSuccess()); Assert.assertEquals(true, requestFuture.getAcceptToDoneTime() > 0); } @Test public void filledWindowThrowsOfferTimeoutException() throws Exception { Window<Integer,String,String> window = new Window<Integer,String,String>(1); WindowFuture<Integer,String,String> requestFuture0 = window.offer(0, "Request0", 100); try { // this should timeout waiting for a slot WindowFuture<Integer,String,String> requestFuture1 = window.offer(1, "Request1", 100); Assert.fail(); } catch (OfferTimeoutException e) { // correct behavior } } @Test public void awaitTimesout() throws Exception { Window<Integer,String,String> window = new Window<Integer,String,String>(1); WindowFuture<Integer,String,String> requestFuture0 = window.offer(0, "Request0", 100); // this should timeout waiting for a response Assert.assertFalse(requestFuture0.await(100)); } @Test public void duplicateKeyThrowsDuplicateKeyException() throws Exception { Window<Integer,String,String> window = new Window<Integer,String,String>(1); WindowFuture<Integer,String,String> requestFuture0 = window.offer(0, "Request0", 100); try { WindowFuture<Integer,String,String> requestFuture1 = window.offer(0, "Request0", 100);; Assert.fail(); } catch (DuplicateKeyException e) { // correct behavior } } @Test public void waitingFlagNotOriginallySetButAddedOnAwait() throws Exception { Window<Integer,String,String> window = new Window<Integer,String,String>(1); WindowFuture<Integer,String,String> requestFuture0 = window.offer(0, "Request0", 100); Assert.assertEquals(WindowFuture.CALLER_NOT_WAITING, requestFuture0.getCallerStateHint()); Assert.assertFalse(requestFuture0.isCallerWaiting()); // now the caller was mistaken that they weren't waiting, when they actually // start to wait, we'll set the waiting flag to true Assert.assertFalse(requestFuture0.await(30)); Assert.assertEquals(WindowFuture.CALLER_WAITING_TIMEOUT, requestFuture0.getCallerStateHint()); } @Test public void cancel() throws Exception { Window<Integer,String,String> window = new Window<Integer,String,String>(1); Integer i = new Integer(1); String request = "Request"+i; WindowFuture<Integer,String,String> requestFuture = window.offer(i, request, 100); // cancel it WindowFuture<Integer,String,String> responseFuture = window.cancel(i); Assert.assertEquals(new Integer(i), responseFuture.getKey()); Assert.assertEquals(request, responseFuture.getRequest()); Assert.assertEquals(true, responseFuture.getAcceptTimestamp() > 0); Assert.assertEquals(null, responseFuture.getResponse()); Assert.assertEquals(true, responseFuture.getDoneTimestamp() > 0); Assert.assertEquals(true, requestFuture.isCancelled()); Assert.assertEquals(true, requestFuture.isDone()); Assert.assertEquals(false, requestFuture.isSuccess()); Assert.assertTrue(requestFuture.getAcceptToDoneTime() >= 0); // this should not timeout waiting for a response Assert.assertTrue(requestFuture.await(50)); } @Test public void cancelAll() throws Exception { int count = 5; Window<Integer,String,String> window = new Window<Integer,String,String>(count); String[] requests = new String[count]; for (int i = 0; i < count; i++) { requests[i] = "Request" + i; } for (int i = 0; i < 3; i++) { window.offer(i, requests[i], 100); } Assert.assertEquals(3, window.getSize()); // are there requests pending? List<WindowFuture<Integer,String,String>> cancelled = window.cancelAll(); Assert.assertEquals(0, window.getSize()); Assert.assertEquals(3, cancelled.size()); for (int i = 0; i < 3; i++) { WindowFuture<Integer,String,String> value = cancelled.get(i); Assert.assertEquals("Request"+value.getKey(), value.getRequest()); Assert.assertEquals(true, value.getAcceptTimestamp() > 0); Assert.assertEquals(null, value.getResponse()); Assert.assertEquals(true, value.getDoneTimestamp() > 0); Assert.assertEquals(true, value.isCancelled()); Assert.assertEquals(true, value.isDone()); Assert.assertEquals(false, value.isSuccess()); } } public static class RequestThread extends Thread { private Window<Integer,String,String> window; private BlockingQueue<Integer> requestQueue; public int id; public int requestsPerThread; public Throwable throwable; public RequestThread(Window<Integer,String,String> window, BlockingQueue<Integer> requestQueue, int id, int requestsPerThread) { this.window = window; this.requestQueue = requestQueue; this.id = id; this.requestsPerThread = requestsPerThread; } @Override public void run() { try { for (int x = 0; x < requestsPerThread; x++) { Integer i = Integer.valueOf(""+id+""+x); String request = "Request"+i; // logger.debug("adding request " + i); WindowFuture<Integer,String,String> requestFuture = window.offer(i, request, 1000); Assert.assertEquals(i, requestFuture.getKey()); Assert.assertEquals(request, requestFuture.getRequest()); Assert.assertEquals(true, requestFuture.getAcceptTimestamp() > 0); Assert.assertEquals(null, requestFuture.getResponse()); Assert.assertEquals(0, requestFuture.getDoneTimestamp()); Assert.assertEquals(false, requestFuture.isCancelled()); Assert.assertEquals(false, requestFuture.isDone()); Assert.assertEquals(false, requestFuture.isSuccess()); Assert.assertEquals(-1, requestFuture.getAcceptToDoneTime()); requestQueue.add(i); requestFuture.await(100); Assert.assertEquals(request, requestFuture.getRequest()); Assert.assertEquals(true, requestFuture.getAcceptTimestamp() > 0); Assert.assertEquals("Response"+i, requestFuture.getResponse()); //Assert.assertEquals(null, requestFuture.getResponse()); Assert.assertEquals(true, requestFuture.getDoneTimestamp() > 0); Assert.assertEquals(false, requestFuture.isCancelled()); Assert.assertEquals(true, requestFuture.isDone()); } } catch (Throwable t) { logger.error("", t); this.throwable = t; return; } } } public static class ResponseThread extends Thread { private Window<Integer,String,String> window; private BlockingQueue<Integer> requestQueue; public int total; public Throwable throwable; public ResponseThread(Window<Integer,String,String> window, BlockingQueue<Integer> requestQueue, int total) { this.window = window; this.requestQueue = requestQueue; this.total = total; } @Override public void run() { try { for (int x = 0; x < total; x++) { //Integer i = new Integer(x); Integer i = requestQueue.poll(100, TimeUnit.MILLISECONDS); if (i == null) { throw new Exception("Integer was null in ResponseThread"); } String response = "Response"+i; // logger.debug("adding response " + i); WindowFuture<Integer,String,String> responseFuture = window.complete(i, response); Assert.assertEquals(new Integer(i), responseFuture.getKey()); Assert.assertEquals("Request"+i, responseFuture.getRequest()); Assert.assertEquals(true, responseFuture.getAcceptTimestamp() > 0); Assert.assertEquals(response, responseFuture.getResponse()); Assert.assertEquals(true, responseFuture.getDoneTimestamp() > 0); Assert.assertEquals(false, responseFuture.isCancelled()); Assert.assertEquals(true, responseFuture.isDone()); Assert.assertEquals(true, responseFuture.isSuccess()); } } catch (Throwable t) { logger.error("", t); this.throwable = t; return; } } } @Test public void simulatedMultithreadedProcessing() throws Exception { final Window<Integer,String,String> window = new Window<Integer,String,String>(5); final int requestThreadCount = 8; final int requestsPerThread = 10000; final BlockingQueue<Integer> requestQueue = new LinkedBlockingQueue<Integer>(); RequestThread[] requestThreads = new RequestThread[requestThreadCount]; for (int i = 0; i < requestThreadCount; i++) { requestThreads[i] = new RequestThread(window, requestQueue, i, requestsPerThread); } ResponseThread responseThread = new ResponseThread(window, requestQueue, requestThreadCount*requestsPerThread); // start 'em for (RequestThread requestThread : requestThreads) { requestThread.start(); } responseThread.start(); // wait for them to finish for (RequestThread requestThread : requestThreads) { requestThread.join(); } responseThread.join(); // make sure everything was successful for (int i = 0; i < requestThreadCount; i++) { if (requestThreads[i].throwable != null) { logger.error("", requestThreads[i].throwable); } Assert.assertNull("RequestThread " + i + " throwable wasn't null: " + requestThreads[i].throwable, requestThreads[i].throwable); } if (responseThread.throwable != null) { logger.error("", responseThread.throwable); } Assert.assertNull("ResponseThread throwable wasn't null", responseThread.throwable); Assert.assertEquals(0, window.getSize()); } @Test public void abortOffering() throws Exception { // test that terminating slot waiters early works as expected final Window<Integer,String,String> window = new Window<Integer,String,String>(2); // first two items should work Assert.assertFalse(window.abortPendingOffers()); window.offer(1, "Request1", 100); Assert.assertFalse(window.abortPendingOffers()); window.offer(2, "Request2", 100); Assert.assertFalse(window.abortPendingOffers()); // third item should fail try { window.offer(3, "Request3", 20); Assert.fail(); } catch (OfferTimeoutException e) { // correct behavior } // start up 3 other threads that will all be waiting too Thread[] waiters = new Thread[3]; for (int i = 0; i < waiters.length; i++) { final int x = i; waiters[i] = new Thread() { @Override public void run() { try { window.offer(4+x, "Request" + (4+x), 5000); Assert.fail(); } catch (PendingOfferAbortedException e) { // correct behavior } catch (Exception e) { logger.error("", e); Assert.fail(); } } }; waiters[i].start(); } // start up a thread that will call "terminateSlotWaiters" in 300 ms Thread terminator = new Thread() { @Override public void run() { try { Thread.sleep(300); } catch (Exception e) { } try { Assert.assertEquals(4, window.getPendingOfferCount()); boolean hadWaiters = window.abortPendingOffers(); logger.debug("hadWaiters: " + hadWaiters); Assert.assertTrue(hadWaiters); } catch (Exception e) { logger.error("", e); Assert.fail(); } } }; terminator.start(); // now wait for a slot up to 5 seconds (the thread we spawned earlier // should definitely cause it to terminate early) try { window.offer(3, "Request3", 5000); Assert.fail(); } catch (PendingOfferAbortedException e) { // correct behavior } // make sure everything is finished terminator.join(); for (Thread t : waiters) { t.join(); } // next call to terminate slot waiters shouldn't do anything Assert.assertEquals(0, window.getPendingOfferCount()); Assert.assertFalse(window.abortPendingOffers()); window.complete(1, "Response1"); Assert.assertFalse(window.abortPendingOffers()); window.offer(4, "Request4", 100); Assert.assertFalse(window.abortPendingOffers()); } @Test public void abortOfferingCalledWithNoWaitingOfferers() throws Exception { final Window<Integer,String,String> window = new Window<Integer,String,String>(2); window.offer(0, "Request0", 100); window.abortPendingOffers(); window.offer(1, "Request1", 100); } @Test public void invalidArguments() throws Exception { final Window<Integer,String,String> window = new Window<Integer,String,String>(1); try { window.offer(1, "test1", -1); Assert.fail(); } catch (IllegalArgumentException e) { // correct behavior } } }