beans)
+ {
+ beans.addAll(getBeans(clazz));
+ for (Container c : getBeans(Container.class))
+ {
+ Bean bean = getBean(c);
+ if (bean!=null && bean.isManageable())
+ {
+ if (c instanceof ContainerLifeCycle)
+ ((ContainerLifeCycle)c).getContainedBeans(clazz, beans);
+ else
+ beans.addAll(c.getContainedBeans(clazz));
+ }
+ }
+ }
}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java
index efadbcf694f..92b6204bc14 100755
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/QueuedThreadPool.java
@@ -16,7 +16,6 @@
// ========================================================================
//
-
package org.eclipse.jetty.util.thread;
import java.io.IOException;
@@ -66,6 +65,7 @@ public class QueuedThreadPool extends AbstractLifeCycle implements SizedThreadPo
private boolean _daemon = false;
private boolean _detailedDump = false;
private int _lowThreadsThreshold = 1;
+ private ThreadPoolBudget _budget;
public QueuedThreadPool()
{
@@ -106,6 +106,20 @@ public class QueuedThreadPool extends AbstractLifeCycle implements SizedThreadPo
}
_jobs=queue;
_threadGroup=threadGroup;
+ _budget=new ThreadPoolBudget(this);
+ }
+
+ @Override
+ public ThreadPoolBudget getThreadPoolBudget()
+ {
+ return _budget;
+ }
+
+ public void setThreadPoolBudget(ThreadPoolBudget budget)
+ {
+ if (budget!=null && budget.getSizedThreadPool()!=this)
+ throw new IllegalArgumentException();
+ _budget = budget;
}
@Override
@@ -184,6 +198,9 @@ public class QueuedThreadPool extends AbstractLifeCycle implements SizedThreadPo
}
}
+ if (_budget!=null)
+ _budget.reset();
+
synchronized (_joinLock)
{
_joinLock.notifyAll();
@@ -563,7 +580,7 @@ public class QueuedThreadPool extends AbstractLifeCycle implements SizedThreadPo
@Override
public String toString()
{
- return String.format("org.eclipse.jetty.util.thread.QueuedThreadPool@%s{%s,%d<=%d<=%d,i=%d,q=%d}", _name, getState(), getMinThreads(), getThreads(), getMaxThreads(), getIdleThreads(), (_jobs == null ? -1 : _jobs.size()));
+ return String.format("QueuedThreadPool@%s{%s,%d<=%d<=%d,i=%d,q=%d}", _name, getState(), getMinThreads(), getThreads(), getMaxThreads(), getIdleThreads(), (_jobs == null ? -1 : _jobs.size()));
}
private Runnable idleJobPoll() throws InterruptedException
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java
index 6dd85adcd25..0c8a2d9b6ee 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ReservedThreadExecutor.java
@@ -20,8 +20,11 @@ package org.eclipse.jetty.util.thread;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
+import org.eclipse.jetty.util.ConcurrentStack;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
@@ -32,19 +35,39 @@ import org.eclipse.jetty.util.log.Logger;
* An Executor using preallocated/reserved Threads from a wrapped Executor.
* Calls to {@link #execute(Runnable)} on a {@link ReservedThreadExecutor} will either succeed
* with a Thread immediately being assigned the Runnable task, or fail if no Thread is
- * available. Threads are preallocated up to the capacity from a wrapped {@link Executor}.
+ * available.
+ *
Threads are reserved lazily, with a new reserved thread being allocated from a
+ * wrapped {@link Executor} when an execution fails. If the {@link #setIdleTimeout(long, TimeUnit)}
+ * is set to non zero (default 1 minute), then the reserved thread pool will shrink by 1 thread
+ * whenever it has been idle for that period.
*/
@ManagedObject("A pool for reserved threads")
public class ReservedThreadExecutor extends AbstractLifeCycle implements Executor
{
private static final Logger LOG = Log.getLogger(ReservedThreadExecutor.class);
+ private static final Runnable STOP = new Runnable()
+ {
+ @Override
+ public void run()
+ {}
+
+ @Override
+ public String toString()
+ {
+ return "STOP!";
+ }
+ };
private final Executor _executor;
- private final Locker _locker = new Locker();
- private final ReservedThread[] _queue;
- private int _head;
- private int _size;
- private int _pending;
+ private final int _capacity;
+ private final ConcurrentStack.NodeStack _stack;
+ private final AtomicInteger _size = new AtomicInteger();
+ private final AtomicInteger _pending = new AtomicInteger();
+
+ private ThreadPoolBudget.Lease _lease;
+ private Object _owner;
+ private long _idleTime = 1L;
+ private TimeUnit _idleTimeUnit = TimeUnit.MINUTES;
public ReservedThreadExecutor(Executor executor)
{
@@ -57,25 +80,45 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements Executo
* is calculated based on a heuristic from the number of available processors and
* thread pool size.
*/
- public ReservedThreadExecutor(Executor executor,int capacity)
+ public ReservedThreadExecutor(Executor executor, int capacity)
+ {
+ this(executor,capacity,null);
+ }
+
+ /**
+ * @param executor The executor to use to obtain threads
+ * @param capacity The number of threads to preallocate. If less than 0 then capacity
+ * is calculated based on a heuristic from the number of available processors and
+ * thread pool size.
+ */
+ public ReservedThreadExecutor(Executor executor,int capacity, Object owner)
{
_executor = executor;
+ _capacity = reservedThreads(executor,capacity);
+ _stack = new ConcurrentStack.NodeStack<>();
+ _owner = owner;
- if (capacity < 0)
+ LOG.debug("{}",this);
+ }
+ /**
+ * @param executor The executor to use to obtain threads
+ * @param capacity The number of threads to preallocate, If less than 0 then capacity
+ * is calculated based on a heuristic from the number of available processors and
+ * thread pool size.
+ * @return the number of reserved threads that would be used by a ReservedThreadExecutor
+ * constructed with these arguments.
+ */
+ public static int reservedThreads(Executor executor,int capacity)
+ {
+ if (capacity>=0)
+ return capacity;
+ int cpus = Runtime.getRuntime().availableProcessors();
+ if (executor instanceof ThreadPool.SizedThreadPool)
{
- int cpus = Runtime.getRuntime().availableProcessors();
- if (executor instanceof ThreadPool.SizedThreadPool)
- {
- int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads();
- capacity = Math.max(1, Math.min(cpus, threads / 8));
- }
- else
- {
- capacity = cpus;
- }
+ int threads = ((ThreadPool.SizedThreadPool)executor).getMaxThreads();
+ return Math.max(1, Math.min(cpus, threads / 8));
}
-
- _queue = new ReservedThread[capacity];
+ return cpus;
}
public Executor getExecutor()
@@ -86,40 +129,66 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements Executo
@ManagedAttribute(value = "max number of reserved threads", readonly = true)
public int getCapacity()
{
- return _queue.length;
+ return _capacity;
}
@ManagedAttribute(value = "available reserved threads", readonly = true)
public int getAvailable()
{
- try (Locker.Lock lock = _locker.lock())
- {
- return _size;
- }
+ return _size.get();
}
@ManagedAttribute(value = "pending reserved threads", readonly = true)
public int getPending()
{
- try (Locker.Lock lock = _locker.lock())
- {
- return _pending;
- }
+ return _pending.get();
+ }
+
+ @ManagedAttribute(value = "idletimeout in MS", readonly = true)
+ public long getIdleTimeoutMs()
+ {
+ if(_idleTimeUnit==null)
+ return 0;
+ return _idleTimeUnit.toMillis(_idleTime);
+ }
+
+ /**
+ * Set the idle timeout for shrinking the reserved thread pool
+ * @param idleTime Time to wait before shrinking, or 0 for no timeout.
+ * @param idleTimeUnit Time units for idle timeout
+ */
+ public void setIdleTimeout(long idleTime, TimeUnit idleTimeUnit)
+ {
+ if (isRunning())
+ throw new IllegalStateException();
+ _idleTime = idleTime;
+ _idleTimeUnit = idleTimeUnit;
+ }
+
+ @Override
+ public void doStart() throws Exception
+ {
+ _lease = ThreadPoolBudget.leaseFrom(getExecutor(),this,_capacity);
+ super.doStart();
}
@Override
public void doStop() throws Exception
{
- try (Locker.Lock lock = _locker.lock())
+ if (_lease!=null)
+ _lease.close();
+ while(true)
{
- while (_size>0)
+ ReservedThread thread = _stack.pop();
+ if (thread==null)
{
- ReservedThread thread = _queue[_head];
- _queue[_head] = null;
- _head = (_head+1)%_queue.length;
- _size--;
- thread._wakeup.signal();
+ super.doStop();
+ return;
}
+
+ _size.decrementAndGet();
+
+ thread.stop();
}
}
@@ -136,118 +205,196 @@ public class ReservedThreadExecutor extends AbstractLifeCycle implements Executo
*/
public boolean tryExecute(Runnable task)
{
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} tryExecute {}",this ,task);
+
if (task==null)
return false;
- try (Locker.Lock lock = _locker.lock())
+ ReservedThread thread = _stack.pop();
+ if (thread==null && task!=STOP)
{
- if (_size==0)
+ startReservedThread();
+ return false;
+ }
+
+ int size = _size.decrementAndGet();
+ thread.offer(task);
+
+ if (size==0 && task!=STOP)
+ startReservedThread();
+
+ return true;
+ }
+
+ private void startReservedThread()
+ {
+ try
+ {
+ while (true)
{
- if (_pending<_queue.length)
+ int pending = _pending.get();
+ if (pending >= _capacity)
+ return;
+ if (_pending.compareAndSet(pending, pending + 1))
{
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} startReservedThread p={}", this, pending + 1);
+
_executor.execute(new ReservedThread());
- _pending++;
+ return;
}
- return false;
}
-
- ReservedThread thread = _queue[_head];
- _queue[_head] = null;
- _head = (_head+1)%_queue.length;
- _size--;
-
- if (_size==0 && _pending<_queue.length)
- {
- _executor.execute(new ReservedThread());
- _pending++;
- }
-
- thread._task = task;
- thread._wakeup.signal();
-
- return true;
}
catch(RejectedExecutionException e)
{
LOG.ignore(e);
- return false;
}
}
@Override
public String toString()
{
- try (Locker.Lock lock = _locker.lock())
- {
- return String.format("%s{s=%d,p=%d}",super.toString(),_size,_pending);
- }
+ if (_owner==null)
+ return String.format("%s@%x{s=%d,p=%d}",this.getClass().getSimpleName(),hashCode(),_size.get(),_pending.get());
+ return String.format("%s@%s{s=%d,p=%d}",this.getClass().getSimpleName(),_owner,_size.get(),_pending.get());
}
- private class ReservedThread implements Runnable
+ private class ReservedThread extends ConcurrentStack.Node implements Runnable
{
- private Condition _wakeup = null;
+ private final Locker _locker = new Locker();
+ private final Condition _wakeup = _locker.newCondition();
+ private boolean _starting = true;
private Runnable _task = null;
- private void reservedWait() throws InterruptedException
+ public void offer(Runnable task)
{
- _wakeup.await();
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} offer {}", this, task);
+
+ try (Locker.Lock lock = _locker.lock())
+ {
+ _task = task;
+ _wakeup.signal();
+ }
}
- @Override
- public void run()
+ public void stop()
{
- while (true)
+ offer(STOP);
+ }
+
+ private Runnable reservedWait()
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} waiting", this);
+
+ Runnable task = null;
+ while (isRunning() && task==null)
{
- Runnable task = null;
+ boolean idle = false;
+
try (Locker.Lock lock = _locker.lock())
{
- // if this is our first loop, decrement pending count
- if (_wakeup==null)
- {
- _pending--;
- _wakeup = _locker.newCondition();
- }
-
- // Exit if no longer running or there now too many preallocated threads
- if (!isRunning() || _size>=_queue.length)
- break;
-
- // Insert ourselves in the queue
- _queue[(_head+_size++)%_queue.length] = this;
-
- // Wait for a task, ignoring spurious wakeups
- while (isRunning() && task==null)
+ if (_task == null)
{
try
{
- if (LOG.isDebugEnabled())
- LOG.debug("{} waiting", this);
- reservedWait();
- if (LOG.isDebugEnabled())
- LOG.debug("{} woken up", this);
- task = _task;
- _task = null;
+ if (_idleTime == 0)
+ _wakeup.await();
+ else
+ idle = !_wakeup.await(_idleTime, _idleTimeUnit);
}
catch (InterruptedException e)
{
LOG.ignore(e);
}
}
+ task = _task;
+ _task = null;
}
- // Run any task
- if (task!=null)
+ if (idle)
{
- try
- {
- task.run();
- }
- catch (Throwable e)
- {
- LOG.warn(e);
- }
+ // Because threads are held in a stack, excess threads will be
+ // idle. However, we cannot remove threads from the bottom of
+ // the stack, so we submit a poison pill job to stop the thread
+ // on top of the stack (which unfortunately will be the most
+ // recently used)
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} IDLE", this);
+ tryExecute(STOP);
}
}
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} task={}", this, task);
+
+ return task==null?STOP:task;
+ }
+
+ @Override
+ public void run()
+ {
+ while (isRunning())
+ {
+ Runnable task = null;
+
+ // test and increment size BEFORE decrementing pending,
+ // so that we don't have a race starting new pending.
+ while(true)
+ {
+ int size = _size.get();
+ if (size>=_capacity)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} size {} > capacity", this, size, _capacity);
+ if (_starting)
+ _pending.decrementAndGet();
+ return;
+ }
+ if (_size.compareAndSet(size,size+1))
+ break;
+ }
+
+ if (_starting)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} started", this);
+ _pending.decrementAndGet();
+ _starting = false;
+ }
+
+ // Insert ourselves in the stack. Size is already incremented, but
+ // that only effects the decision to keep other threads reserved.
+ _stack.push(this);
+
+ // Wait for a task
+ task = reservedWait();
+
+ if (task==STOP)
+ // return on STOP poison pill
+ break;
+
+ // Run the task
+ try
+ {
+ task.run();
+ }
+ catch (Throwable e)
+ {
+ LOG.warn(e);
+ }
+ }
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} Exited", this);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x",ReservedThreadExecutor.this,hashCode());
}
}
}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPool.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPool.java
index 56055793106..ecc61bd87c3 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPool.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPool.java
@@ -67,9 +67,13 @@ public interface ThreadPool extends Executor
/* ------------------------------------------------------------ */
public interface SizedThreadPool extends ThreadPool
{
- public int getMinThreads();
- public int getMaxThreads();
- public void setMinThreads(int threads);
- public void setMaxThreads(int threads);
+ int getMinThreads();
+ int getMaxThreads();
+ void setMinThreads(int threads);
+ void setMaxThreads(int threads);
+ default ThreadPoolBudget getThreadPoolBudget()
+ {
+ return null;
+ }
}
}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPoolBudget.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPoolBudget.java
new file mode 100644
index 00000000000..e2ed8069a59
--- /dev/null
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPoolBudget.java
@@ -0,0 +1,178 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util.thread;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * A budget of required thread usage, used to warn or error for insufficient configured threads.
+ *
+ * @see ThreadPool.SizedThreadPool#getThreadPoolBudget()
+ */
+public class ThreadPoolBudget
+{
+ static final Logger LOG = Log.getLogger(ThreadPoolBudget.class);
+
+ public interface Lease extends Closeable
+ {
+ int getThreads();
+ }
+
+ /**
+ * An allocation of threads
+ */
+ public class Leased implements Lease
+ {
+ final Object leasee;
+ final int threads;
+
+ private Leased(Object leasee,int threads)
+ {
+ this.leasee = leasee;
+ this.threads = threads;
+ }
+
+ @Override
+ public int getThreads()
+ {
+ return threads;
+ }
+
+ @Override
+ public void close()
+ {
+ info.remove(this);
+ allocations.remove(this);
+ warned.set(false);
+ }
+ }
+
+ private static final Lease NOOP_LEASE = new Lease()
+ {
+ @Override
+ public void close() throws IOException
+ {
+ }
+
+ @Override
+ public int getThreads()
+ {
+ return 0;
+ }
+ };
+
+ final ThreadPool.SizedThreadPool pool;
+ final Set allocations = new CopyOnWriteArraySet<>();
+ final Set info = new CopyOnWriteArraySet<>();
+ final AtomicBoolean warned = new AtomicBoolean();
+ final int warnAt;
+
+ /**
+ * Construct a budget for a SizedThreadPool, with the warning level set by heuristic.
+ * @param pool The pool to budget thread allocation for.
+ */
+ public ThreadPoolBudget(ThreadPool.SizedThreadPool pool)
+ {
+ this(pool,Runtime.getRuntime().availableProcessors());
+ }
+
+ /**
+ * @param pool The pool to budget thread allocation for.
+ * @param warnAt The level of free threads at which a warning is generated.
+ */
+ public ThreadPoolBudget(ThreadPool.SizedThreadPool pool, int warnAt)
+ {
+ this.pool = pool;
+ this.warnAt = warnAt;
+ }
+
+ public ThreadPool.SizedThreadPool getSizedThreadPool()
+ {
+ return pool;
+ }
+
+ public void reset()
+ {
+ allocations.clear();
+ info.clear();
+ warned.set(false);
+ }
+
+ public Lease leaseTo(Object leasee, int threads)
+ {
+ Leased lease = new Leased(leasee,threads);
+ allocations.add(lease);
+ check();
+ return lease;
+ }
+
+ /**
+ * Check registered allocations against the budget.
+ * @throws IllegalStateException if insufficient threads are configured.
+ */
+ public void check() throws IllegalStateException
+ {
+ int required = allocations.stream()
+ .mapToInt(Lease::getThreads)
+ .sum();
+ int maximum = pool.getMaxThreads();
+ int actual = maximum - required;
+
+ if (actual <= 0)
+ {
+ infoOnLeases();
+ throw new IllegalStateException(String.format("Insufficient configured threads: required=%d < max=%d for %s", required, maximum, pool));
+ }
+
+ if (actual < warnAt)
+ {
+ infoOnLeases();
+ if (warned.compareAndSet(false,true))
+ LOG.warn("Low configured threads: (max={} - required={})={} < warnAt={} for {}", maximum, required, actual, warnAt, pool);
+ }
+ }
+
+ private void infoOnLeases()
+ {
+ allocations.stream().filter(lease->!info.contains(lease))
+ .forEach(lease->{
+ info.add(lease);
+ LOG.info("{} requires {} threads from {}",lease.leasee,lease.getThreads(),pool);
+ });
+ }
+
+ public static Lease leaseFrom(Executor executor, Object leasee, int threads)
+ {
+ if (executor instanceof ThreadPool.SizedThreadPool)
+ {
+ ThreadPoolBudget budget = ((ThreadPool.SizedThreadPool)executor).getThreadPoolBudget();
+ if (budget!=null)
+ return budget.leaseTo(leasee,threads);
+ }
+ return NOOP_LEASE;
+ }
+}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleTest.java
index cdc610a7e2e..d16df62e7db 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/component/ContainerLifeCycleTest.java
@@ -26,9 +26,13 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.util.TypeUtil;
+import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertThat;
+
public class ContainerLifeCycleTest
{
@Test
@@ -608,4 +612,32 @@ public class ContainerLifeCycleTest
super.destroy();
}
}
+
+
+ @Test
+ public void testGetBeans() throws Exception
+ {
+ TestContainerLifeCycle root = new TestContainerLifeCycle();
+ TestContainerLifeCycle left = new TestContainerLifeCycle();
+ root.addBean(left);
+ TestContainerLifeCycle right = new TestContainerLifeCycle();
+ root.addBean(right);
+ TestContainerLifeCycle leaf = new TestContainerLifeCycle();
+ right.addBean(leaf);
+
+ root.addBean(Integer.valueOf(0));
+ root.addBean(Integer.valueOf(1));
+ left.addBean(Integer.valueOf(2));
+ right.addBean(Integer.valueOf(3));
+ leaf.addBean(Integer.valueOf(4));
+ leaf.addBean("leaf");
+
+ assertThat(root.getBeans(Container.class), containsInAnyOrder(left,right));
+ assertThat(root.getBeans(Integer.class), containsInAnyOrder(Integer.valueOf(0),Integer.valueOf(1)));
+ assertThat(root.getBeans(String.class), containsInAnyOrder());
+
+ assertThat(root.getContainedBeans(Container.class), containsInAnyOrder(left,right,leaf));
+ assertThat(root.getContainedBeans(Integer.class), containsInAnyOrder(Integer.valueOf(0),Integer.valueOf(1),Integer.valueOf(2),Integer.valueOf(3),Integer.valueOf(4)));
+ assertThat(root.getContainedBeans(String.class), containsInAnyOrder("leaf"));
+ }
}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java
index 3a3f73325dd..4b1964cdcfd 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/thread/ReservedThreadExecutorTest.java
@@ -18,24 +18,44 @@
package org.eclipse.jetty.util.thread;
+import java.security.SecureRandom;
import java.util.ArrayDeque;
import java.util.Deque;
+import java.util.Random;
+import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.StreamSupport;
+import org.eclipse.jetty.toolchain.test.annotation.Stress;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
public class ReservedThreadExecutorTest
{
private static final int SIZE = 2;
- private static final Runnable NOOP = () -> {};
+ private static final Runnable NOOP = new Runnable()
+ {
+ @Override
+ public void run()
+ {}
+
+ @Override
+ public String toString()
+ {
+ return "NOOP!";
+ }
+ };
private TestExecutor _executor;
private ReservedThreadExecutor _reservedExecutor;
@@ -127,17 +147,87 @@ public class ReservedThreadExecutorTest
waitForAllAvailable();
}
- protected void waitForAllAvailable() throws InterruptedException
+
+ @Test
+ public void testShrink() throws Exception
+ {
+ final long IDLE = 1000;
+
+ _reservedExecutor.stop();
+ _reservedExecutor.setIdleTimeout(IDLE,TimeUnit.MILLISECONDS);
+ _reservedExecutor.start();
+
+ // Reserved threads are lazily started.
+ assertThat(_executor._queue.size(), is(0));
+
+ assertThat(_reservedExecutor.tryExecute(NOOP),is(false));
+ _executor.execute();
+ waitForNoPending();
+
+ CountDownLatch latch = new CountDownLatch(1);
+ Runnable waitForLatch = ()->{try {latch.await();} catch(Exception e){}};
+ assertThat(_reservedExecutor.tryExecute(waitForLatch),is(true));
+ _executor.execute();
+
+ assertThat(_reservedExecutor.tryExecute(NOOP),is(false));
+ _executor.execute();
+ waitForNoPending();
+
+ latch.countDown();
+ waitForAvailable(2);
+
+ // Check that regular moderate activity keeps the pool a moderate size
+ TimeUnit.MILLISECONDS.sleep(IDLE/2);
+ assertThat(_reservedExecutor.tryExecute(NOOP),is(true));
+ waitForAvailable(2);
+ TimeUnit.MILLISECONDS.sleep(IDLE/2);
+ assertThat(_reservedExecutor.tryExecute(NOOP),is(true));
+ waitForAvailable(1);
+ TimeUnit.MILLISECONDS.sleep(IDLE/2);
+ assertThat(_reservedExecutor.tryExecute(NOOP),is(true));
+ waitForAvailable(1);
+ TimeUnit.MILLISECONDS.sleep(IDLE/2);
+ assertThat(_reservedExecutor.tryExecute(NOOP),is(true));
+ waitForAvailable(1);
+ TimeUnit.MILLISECONDS.sleep(IDLE/2);
+ assertThat(_reservedExecutor.tryExecute(NOOP),is(true));
+ waitForAvailable(1);
+
+ // check fully idle goes to zero
+ TimeUnit.MILLISECONDS.sleep(IDLE);
+ assertThat(_reservedExecutor.getAvailable(),is(0));
+
+ }
+
+ protected void waitForNoPending() throws InterruptedException
{
long started = System.nanoTime();
- while (_reservedExecutor.getAvailable() < SIZE)
+ while (_reservedExecutor.getPending() > 0)
+ {
+ long elapsed = System.nanoTime() - started;
+ if (elapsed > TimeUnit.SECONDS.toNanos(10))
+ Assert.fail("pending="+_reservedExecutor.getPending());
+ Thread.sleep(10);
+ }
+ assertThat(_reservedExecutor.getPending(), is(0));
+ }
+
+ protected void waitForAvailable(int size) throws InterruptedException
+ {
+ long started = System.nanoTime();
+ while (_reservedExecutor.getAvailable() < size)
{
long elapsed = System.nanoTime() - started;
if (elapsed > TimeUnit.SECONDS.toNanos(10))
Assert.fail();
Thread.sleep(10);
}
- assertThat(_reservedExecutor.getAvailable(), is(SIZE));
+ assertThat(_reservedExecutor.getAvailable(), is(size));
+ }
+
+ protected void waitForAllAvailable() throws InterruptedException
+ {
+ waitForAvailable(SIZE);
}
private static class TestExecutor implements Executor
@@ -177,4 +267,73 @@ public class ReservedThreadExecutorTest
}
}
}
+
+ @Ignore
+ @Test
+ public void stressTest() throws Exception
+ {
+ QueuedThreadPool pool = new QueuedThreadPool(20);
+ pool.setStopTimeout(10000);
+ pool.start();
+ ReservedThreadExecutor reserved = new ReservedThreadExecutor(pool,10);
+ reserved.setIdleTimeout(0,null);
+ reserved.start();
+
+ final int LOOPS = 1000000;
+ final Random random = new Random();
+ final AtomicInteger executions = new AtomicInteger(LOOPS);
+ final CountDownLatch executed = new CountDownLatch(executions.get());
+ final AtomicInteger usedReserved = new AtomicInteger(0);
+ final AtomicInteger usedPool = new AtomicInteger(0);
+
+ Runnable task = new Runnable()
+ {
+ public void run()
+ {
+ try
+ {
+ while (true)
+ {
+ int loops = executions.get();
+ if (loops <= 0)
+ return;
+
+ if (executions.compareAndSet(loops, loops - 1))
+ {
+ if (reserved.tryExecute(this))
+ {
+ usedReserved.incrementAndGet();
+ } else
+ {
+ usedPool.incrementAndGet();
+ pool.execute(this);
+ }
+ return;
+ }
+ }
+ }
+ finally
+ {
+ executed.countDown();
+ }
+ }
+ };
+
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+ task.run();
+
+ assertTrue(executed.await(60,TimeUnit.SECONDS));
+
+ reserved.stop();
+ pool.stop();
+
+ assertThat(usedReserved.get()+usedPool.get(),is(LOOPS));
+ System.err.printf("reserved=%d pool=%d total=%d%n",usedReserved.get(),usedPool.get(),LOOPS);
+ }
}
diff --git a/jetty-util/src/test/resources/jetty-logging.properties b/jetty-util/src/test/resources/jetty-logging.properties
index adc470de2b3..96eb0ce650a 100644
--- a/jetty-util/src/test/resources/jetty-logging.properties
+++ b/jetty-util/src/test/resources/jetty-logging.properties
@@ -1,4 +1,5 @@
# Setup default logging implementation for during testing
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.util.LEVEL=DEBUG
-#org.eclipse.jetty.util.PathWatcher.LEVEL=DEBUG
\ No newline at end of file
+#org.eclipse.jetty.util.PathWatcher.LEVEL=DEBUG
+#org.eclipse.jetty.util.thread.ReservedThreadExecutor.LEVEL=DEBUG
diff --git a/pom.xml b/pom.xml
index 3691716c44b..5694935b61f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -416,7 +416,7 @@
org.apache.maven.plugins
maven-failsafe-plugin
- 2.20
+ 2.20.1
org.apache.maven.plugins
diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java
index 9859ec878fc..b7a020d59e2 100644
--- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java
+++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java
@@ -37,6 +37,7 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.util.BytesContentProvider;
diff --git a/tests/test-webapps/pom.xml b/tests/test-webapps/pom.xml
index 738e7695aa1..72a533eeae6 100644
--- a/tests/test-webapps/pom.xml
+++ b/tests/test-webapps/pom.xml
@@ -38,5 +38,6 @@
test-servlet-spec
test-jaas-webapp
test-jndi-webapp
+ test-http2-webapp
diff --git a/tests/test-webapps/test-http2-webapp/pom.xml b/tests/test-webapps/test-http2-webapp/pom.xml
new file mode 100644
index 00000000000..3b3ba85365b
--- /dev/null
+++ b/tests/test-webapps/test-http2-webapp/pom.xml
@@ -0,0 +1,175 @@
+
+
+
+ org.eclipse.jetty.tests
+ test-webapps-parent
+ 9.4.8-SNAPSHOT
+
+
+ 4.0.0
+ test-http2-webapp
+ Test :: Jetty HTTP2 Webapp
+ war
+
+
+ ${project.groupId}.http2
+
+
+
+
+
+ maven-dependency-plugin
+
+
+ unpack-webapp
+ pre-integration-test
+
+ unpack
+
+
+
+
+ ${project.groupId}
+ ${project.artifactId}
+ ${project.version}
+ war
+ false
+ ${project.build.directory}/webapp
+
+
+
+
+
+
+
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+
+
+
+
+ jdk8
+
+ [1.8,1.9)
+
+
+
+
+ maven-dependency-plugin
+
+
+ copy-alpn-boot
+ pre-integration-test
+
+ copy
+
+
+
+
+ org.mortbay.jetty.alpn
+ alpn-boot
+ ${alpn.version}
+ jar
+ false
+ ${project.build.directory}/alpn
+
+
+
+
+
+
+
+ maven-failsafe-plugin
+
+ -Xbootclasspath/p:${project.build.directory}/alpn/alpn-boot-${alpn.version}.jar
+
+
+
+
+
+
+ org.eclipse.jetty
+ jetty-alpn-openjdk8-client
+ ${project.version}
+
+
+ org.eclipse.jetty.alpn
+ alpn-api
+
+
+
+
+ org.eclipse.jetty
+ jetty-alpn-openjdk8-server
+ ${project.version}
+ test
+
+
+
+
+ jdk9
+
+ [1.9,)
+
+
+
+ org.eclipse.jetty
+ jetty-alpn-java-client
+ ${project.version}
+
+
+ org.eclipse.jetty
+ jetty-alpn-java-server
+ ${project.version}
+ test
+
+
+
+
+
+
+
+ org.eclipse.jetty.http2
+ http2-client
+ ${project.version}
+
+
+ javax.servlet
+ javax.servlet-api
+ provided
+
+
+
+ org.eclipse.jetty
+ jetty-client
+ ${project.version}
+ test
+
+
+ org.eclipse.jetty
+ jetty-webapp
+ ${project.version}
+ test
+
+
+ org.eclipse.jetty.http2
+ http2-server
+ ${project.version}
+ test
+
+
+ junit
+ junit
+ test
+
+
+
diff --git a/tests/test-webapps/test-http2-webapp/src/main/java/org/eclipse/jetty/test/webapp/HTTP1Servlet.java b/tests/test-webapps/test-http2-webapp/src/main/java/org/eclipse/jetty/test/webapp/HTTP1Servlet.java
new file mode 100644
index 00000000000..dd7aae745bc
--- /dev/null
+++ b/tests/test-webapps/test-http2-webapp/src/main/java/org/eclipse/jetty/test/webapp/HTTP1Servlet.java
@@ -0,0 +1,138 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.test.webapp;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.http.MetaData;
+import org.eclipse.jetty.http2.api.Session;
+import org.eclipse.jetty.http2.api.Stream;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.frames.DataFrame;
+import org.eclipse.jetty.http2.frames.HeadersFrame;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.Promise;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+public class HTTP1Servlet extends HttpServlet
+{
+ private SslContextFactory sslContextFactory;
+ private HTTP2Client http2Client;
+
+ @Override
+ public void init() throws ServletException
+ {
+ try
+ {
+ sslContextFactory = new SslContextFactory(true);
+ http2Client = new HTTP2Client();
+ http2Client.addBean(sslContextFactory);
+ http2Client.start();
+ }
+ catch (Exception x)
+ {
+ throw new ServletException(x);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ try
+ {
+ http2Client.stop();
+ }
+ catch (Exception x)
+ {
+ x.printStackTrace();
+ }
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ String host = "localhost";
+ int port = request.getServerPort();
+ String contextPath = request.getContextPath();
+ ServletOutputStream output = response.getOutputStream();
+ AsyncContext asyncContext = request.startAsync();
+ http2Client.connect(sslContextFactory, new InetSocketAddress(host, port), new Session.Listener.Adapter(), new Promise()
+ {
+ @Override
+ public void succeeded(Session session)
+ {
+ HttpURI uri = new HttpURI(request.getScheme(), host, port, contextPath + "/h2");
+ MetaData.Request metaData = new MetaData.Request(HttpMethod.GET.asString(), uri, HttpVersion.HTTP_2, new HttpFields());
+ HeadersFrame frame = new HeadersFrame(metaData, null, true);
+ session.newStream(frame, new Promise.Adapter()
+ {
+ @Override
+ public void failed(Throwable x)
+ {
+ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
+ response.setHeader("X-Failure", "stream");
+ asyncContext.complete();
+ }
+ }, new Stream.Listener.Adapter()
+ {
+ @Override
+ public void onData(Stream stream, DataFrame frame, Callback callback)
+ {
+ try
+ {
+ ByteBuffer buffer = frame.getData();
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ output.write(bytes);
+ callback.succeeded();
+ if (frame.isEndStream())
+ asyncContext.complete();
+ }
+ catch (IOException x)
+ {
+ asyncContext.complete();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
+ response.setHeader("X-Failure", "session");
+ asyncContext.complete();
+ }
+ });
+ }
+}
diff --git a/tests/test-webapps/test-http2-webapp/src/main/java/org/eclipse/jetty/test/webapp/HTTP2Servlet.java b/tests/test-webapps/test-http2-webapp/src/main/java/org/eclipse/jetty/test/webapp/HTTP2Servlet.java
new file mode 100644
index 00000000000..ab92da37e01
--- /dev/null
+++ b/tests/test-webapps/test-http2-webapp/src/main/java/org/eclipse/jetty/test/webapp/HTTP2Servlet.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.test.webapp;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class HTTP2Servlet extends HttpServlet
+{
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ response.getOutputStream().print("ok");
+ }
+}
diff --git a/tests/test-webapps/test-http2-webapp/src/main/webapp/WEB-INF/web.xml b/tests/test-webapps/test-http2-webapp/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..9944a3b0ff2
--- /dev/null
+++ b/tests/test-webapps/test-http2-webapp/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ h1
+ org.eclipse.jetty.test.webapp.HTTP1Servlet
+ true
+
+
+ h1
+ /h1
+
+
+
+ h2
+ org.eclipse.jetty.test.webapp.HTTP2Servlet
+
+
+ h2
+ /h2
+
+
+
+
+
diff --git a/tests/test-webapps/test-http2-webapp/src/test/java/org/eclipse/jetty/test/webapp/HTTP2FromWebAppIT.java b/tests/test-webapps/test-http2-webapp/src/test/java/org/eclipse/jetty/test/webapp/HTTP2FromWebAppIT.java
new file mode 100644
index 00000000000..b4f3a8a9391
--- /dev/null
+++ b/tests/test-webapps/test-http2-webapp/src/test/java/org/eclipse/jetty/test/webapp/HTTP2FromWebAppIT.java
@@ -0,0 +1,96 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.test.webapp;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.http2.HTTP2Cipher;
+import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class HTTP2FromWebAppIT
+{
+ @Test
+ public void testHTTP2FromWebApp() throws Exception
+ {
+ Server server = new Server();
+
+ SslContextFactory serverTLS = new SslContextFactory();
+ serverTLS.setKeyStorePath("src/test/resources/keystore.jks");
+ serverTLS.setKeyStorePassword("storepwd");
+ serverTLS.setCipherComparator(new HTTP2Cipher.CipherComparator());
+
+ HttpConfiguration httpsConfig = new HttpConfiguration();
+ httpsConfig.addCustomizer(new SecureRequestCustomizer());
+
+ HttpConnectionFactory h1 = new HttpConnectionFactory(httpsConfig);
+ ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
+ alpn.setDefaultProtocol(h1.getProtocol());
+ SslConnectionFactory ssl = new SslConnectionFactory(serverTLS, alpn.getProtocol());
+ HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpsConfig);
+
+ ServerConnector connector = new ServerConnector(server, ssl, alpn, h2, h1);
+ server.addConnector(connector);
+
+ String contextPath = "/http2_from_webapp";
+ WebAppContext context = new WebAppContext("target/webapp", contextPath);
+ server.setHandler(context);
+
+ server.start();
+
+ try
+ {
+ SslContextFactory clientTLS = new SslContextFactory(true);
+ HttpClient client = new HttpClient(clientTLS);
+ client.start();
+
+ try
+ {
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .scheme(HttpScheme.HTTPS.asString())
+ .path(contextPath + "/h1")
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertEquals("ok", response.getContentAsString());
+ }
+ finally
+ {
+ client.stop();
+ }
+ }
+ finally
+ {
+ server.stop();
+ }
+ }
+}
diff --git a/tests/test-webapps/test-http2-webapp/src/test/resources/jetty-logging.properties b/tests/test-webapps/test-http2-webapp/src/test/resources/jetty-logging.properties
new file mode 100644
index 00000000000..055e90b60ef
--- /dev/null
+++ b/tests/test-webapps/test-http2-webapp/src/test/resources/jetty-logging.properties
@@ -0,0 +1,3 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+org.eclipse.jetty.LEVEL=INFO
+#org.eclipse.jetty.alpn.LEVEL=DEBUG
diff --git a/tests/test-webapps/test-http2-webapp/src/test/resources/keystore.jks b/tests/test-webapps/test-http2-webapp/src/test/resources/keystore.jks
new file mode 100644
index 00000000000..428ba54776e
Binary files /dev/null and b/tests/test-webapps/test-http2-webapp/src/test/resources/keystore.jks differ