/* $Id$ */ /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ package org.apache.manifoldcf.connectorcommon.throttler; import org.apache.manifoldcf.core.interfaces.*; import org.apache.manifoldcf.connectorcommon.interfaces.*; import java.io.*; import java.util.*; import java.util.concurrent.atomic.*; import org.junit.*; import static org.junit.Assert.*; public class TestThrottler extends org.apache.manifoldcf.core.tests.BaseHSQLDB { @Test public void multiThreadConnectionPoolTest() throws Exception { // First, create the throttle group. IThreadContext threadContext = ThreadContextFactory.make(); IThrottleGroups tg = ThrottleGroupsFactory.make(threadContext); tg.createOrUpdateThrottleGroup("test","test",new ThrottleSpec()); // We create a pretend connection pool IConnectionThrottler connectionThrottler = tg.obtainConnectionThrottler("test","test",new String[]{"A","B","C"}); System.out.println("Connection throttler obtained"); // How best to test this? // Well, what I'm going to do is to have multiple threads active. Each one will do perfectly sensible things // while generating a log that includes timestamps for everything that happens. At the end, the log will be // analyzed for violations of throttling policy. PollingThread pt = new PollingThread(); pt.start(); EventLog eventLog = new EventLog(); int numThreads = 10; TesterThread[] threads = new TesterThread[numThreads]; for (int i = 0; i < numThreads; i++) { threads[i] = new TesterThread(connectionThrottler, eventLog); threads[i].start(); } // Now, join all the threads at the end for (int i = 0; i < numThreads; i++) { threads[i].finishUp(); } pt.interrupt(); pt.finishUp(); // Shut down the throttle group tg.removeThrottleGroup("test","test"); // Finally, do the log analysis eventLog.analyze(); System.out.println("Done test"); } protected static class PollingThread extends Thread { protected Throwable exception = null; public PollingThread() { } public void run() { try { IThreadContext threadContext = ThreadContextFactory.make(); IThrottleGroups throttleGroups = ThrottleGroupsFactory.make(threadContext); while (true) { throttleGroups.poll("test"); Thread.sleep(1000L); } } catch (InterruptedException e) { } catch (Exception e) { exception = e; } } public void finishUp() throws Exception { join(); if (exception != null) { if (exception instanceof RuntimeException) throw (RuntimeException)exception; else if (exception instanceof Error) throw (Error)exception; else if (exception instanceof Exception) throw (Exception)exception; else throw new RuntimeException("Unknown exception: "+exception.getClass().getName()+": "+exception.getMessage(),exception); } } } protected static class TesterThread extends Thread { protected final EventLog eventLog; protected final IConnectionThrottler connectionThrottler; protected Throwable exception = null; public TesterThread(IConnectionThrottler connectionThrottler, EventLog eventLog) { this.connectionThrottler = connectionThrottler; this.eventLog = eventLog; } public void run() { try { int numberConnectionCycles = 3; int numberFetchesPerCycle = 3; for (int k = 0; k < numberConnectionCycles; k++) { // First grab a connection. int rval = connectionThrottler.waitConnectionAvailable(); if (rval == IConnectionThrottler.CONNECTION_FROM_NOWHERE) throw new Exception("Unexpected return value from waitConnectionAvailable()"); IFetchThrottler fetchThrottler; if (rval == IConnectionThrottler.CONNECTION_FROM_CREATION) { // Pretend to create the connection eventLog.addLogEntry(new ConnectionCreatedEvent()); } else { // Pretend to get it from the pool eventLog.addLogEntry(new ConnectionFromPoolEvent()); } fetchThrottler = connectionThrottler.getNewConnectionFetchThrottler(); for (int l = 0; l < numberFetchesPerCycle; l++) { // Perform a fake fetch if (fetchThrottler.obtainFetchDocumentPermission() == false) throw new Exception("Unexpected return value for obtainFetchDocumentPermission()"); eventLog.addLogEntry(new FetchStartEvent()); IStreamThrottler streamThrottler = fetchThrottler.createFetchStream(); try { // Do one read if (streamThrottler.obtainReadPermission(1000) == false) throw new Exception("False from obtainReadPermission!"); eventLog.addLogEntry(new ReadStartEvent(1000)); streamThrottler.releaseReadPermission(1000, 1000); eventLog.addLogEntry(new ReadDoneEvent(1000)); // Do another read if (streamThrottler.obtainReadPermission(1000) == false) throw new Exception("False from obtainReadPermission!"); eventLog.addLogEntry(new ReadStartEvent(1000)); streamThrottler.releaseReadPermission(1000, 1000); eventLog.addLogEntry(new ReadDoneEvent(1000)); // Do a third read if (streamThrottler.obtainReadPermission(1000) == false) throw new Exception("False from obtainReadPermission!"); eventLog.addLogEntry(new ReadStartEvent(1000)); streamThrottler.releaseReadPermission(1000, 100); eventLog.addLogEntry(new ReadDoneEvent(100)); } finally { // Close the stream streamThrottler.closeStream(); } eventLog.addLogEntry(new FetchDoneEvent()); } // Pretend to release the connection boolean destroyIt = connectionThrottler.noteReturnedConnection(); if (destroyIt) { eventLog.addLogEntry(new ConnectionDestroyedEvent()); connectionThrottler.noteConnectionDestroyed(); } else { eventLog.addLogEntry(new ConnectionReturnedToPoolEvent()); connectionThrottler.noteConnectionReturnedToPool(); } } } catch (Exception e) { e.printStackTrace(); exception = e; } } public void finishUp() throws Exception { join(); if (exception != null) { if (exception instanceof RuntimeException) throw (RuntimeException)exception; else if (exception instanceof Error) throw (Error)exception; else if (exception instanceof Exception) throw (Exception)exception; else throw new RuntimeException("Unknown exception: "+exception.getClass().getName()+": "+exception.getMessage(),exception); } } } protected static class ThrottleSpec implements IThrottleSpec { public ThrottleSpec() { } /** Given a bin name, find the max open connections to use for that bin. *@return -1 if no limit found. */ @Override public int getMaxOpenConnections(String binName) { if (binName.equals("A")) return 3; if (binName.equals("B")) return 4; return Integer.MAX_VALUE; } /** Look up minimum milliseconds per byte for a bin. *@return 0.0 if no limit found. */ @Override public double getMinimumMillisecondsPerByte(String binName) { if (binName.equals("B")) return 0.5; if (binName.equals("C")) return 0.75; return 0.0; } /** Look up minimum milliseconds for a fetch for a bin. *@return 0 if no limit found. */ @Override public long getMinimumMillisecondsPerFetch(String binName) { if (binName.equals("A")) return 5; if (binName.equals("C")) return 20; return 0; } } protected static class EventLog { protected final List<LogEntry> logList = new ArrayList<LogEntry>(); public EventLog() { } public synchronized void addLogEntry(LogEntry x) { System.out.println(x.toString()); logList.add(x); } public synchronized void analyze() throws Exception { State s = new State(); for (LogEntry l : logList) { l.apply(s); } // Success! } } protected static abstract class LogEntry { protected final long timestamp; public LogEntry(long timestamp) { this.timestamp = timestamp; } public abstract void apply(State state) throws Exception; public String toString() { return "Time: "+timestamp; } } protected static class ConnectionCreatedEvent extends LogEntry { public ConnectionCreatedEvent() { super(System.currentTimeMillis()); } public void apply(State state) throws Exception { if (state.outstandingConnections + 1 > 3) throw new Exception("Too many outstanding connections at once!"); state.outstandingConnections++; } public String toString() { return super.toString() + "; Connection created"; } } protected static class ConnectionDestroyedEvent extends LogEntry { public ConnectionDestroyedEvent() { super(System.currentTimeMillis()); } public void apply(State state) throws Exception { state.outstandingConnections--; } public String toString() { return super.toString() + "; Connection destroyed"; } } protected static class ConnectionFromPoolEvent extends LogEntry { public ConnectionFromPoolEvent() { super(System.currentTimeMillis()); } public void apply(State state) throws Exception { } public String toString() { return super.toString() + "; Connection from pool"; } } protected static class ConnectionReturnedToPoolEvent extends LogEntry { public ConnectionReturnedToPoolEvent() { super(System.currentTimeMillis()); } public void apply(State state) throws Exception { } public String toString() { return super.toString() + "; Connection back to pool"; } } protected static class FetchStartEvent extends LogEntry { public FetchStartEvent() { super(System.currentTimeMillis()); } public void apply(State state) throws Exception { if (timestamp < state.lastFetch + 20L - 1L) throw new Exception("Fetch too fast: took place in "+ (timestamp - state.lastFetch) + " milliseconds"); state.lastFetch = timestamp; } public String toString() { return super.toString() + "; Fetch start"; } } protected static class FetchDoneEvent extends LogEntry { public FetchDoneEvent() { super(System.currentTimeMillis()); } public void apply(State state) throws Exception { } public String toString() { return super.toString() + "; Fetch done"; } } protected static class ReadStartEvent extends LogEntry { final int proposed; public ReadStartEvent(int proposed) { super(System.currentTimeMillis()); this.proposed = proposed; } public void apply(State state) throws Exception { if (state.firstByteReadTime == -1L) state.firstByteReadTime = timestamp; else { // Calculate running minimum amount of time it should have taken for the bytes given long minTime = (long)(((double)state.byteTotal) * 0.75 + 0.5); if (timestamp - state.firstByteReadTime < minTime) throw new Exception("Took too short a time to read "+state.byteTotal+" bytes: "+(timestamp - state.firstByteReadTime)); } } public String toString() { return super.toString() + "; Read start("+proposed+")"; } } protected static class ReadDoneEvent extends LogEntry { final int actual; public ReadDoneEvent(int actual) { super(System.currentTimeMillis()); this.actual = actual; } public void apply(State state) throws Exception { state.byteTotal += actual; } public String toString() { return super.toString() + "; Read done("+actual+")"; } } protected static class State { public int outstandingConnections = 0; public long lastFetch = 0L; public long firstByteReadTime = -1L; public long byteTotal = 0L; } }