org.eclipse.jetty.util.thread.strategy.EatWhatYouKill Maven / Gradle / Ivy
Show all versions of jetty-util Show documentation
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util.thread.strategy;
import java.io.Closeable;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.LongAdder;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.ExecutionStrategy;
import org.eclipse.jetty.util.thread.Invocable;
import org.eclipse.jetty.util.thread.TryExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A strategy where the thread that produces will run the resulting task if it
* is possible to do so without thread starvation.
*
* This strategy preemptively dispatches a thread as a pending producer, so that
* when a thread produces a task it can immediately run the task and let the pending
* producer thread take over production. When operating in this way, the sub-strategy
* is called Execute Produce Consume (EPC).
* However, if the task produced uses the {@link Invocable} API to indicate that
* it will not block, then the strategy will run it directly, regardless of the
* presence of a pending producer thread and then resume production after the
* task has completed. When operating in this pattern, the sub-strategy is called
* ProduceConsume (PC).
* If there is no pending producer thread available and if the task has not
* indicated it is non-blocking, then this strategy will dispatch the execution of
* the task and immediately continue production. When operating in this pattern, the
* sub-strategy is called ProduceExecuteConsume (PEC).
*/
@ManagedObject("eat what you kill execution strategy")
public class EatWhatYouKill extends ContainerLifeCycle implements ExecutionStrategy, Runnable
{
private static final Logger LOG = LoggerFactory.getLogger(EatWhatYouKill.class);
private enum State
{
IDLE, PRODUCING, REPRODUCING
}
/* The modes this strategy can work in */
private enum Mode
{
PRODUCE_CONSUME,
PRODUCE_INVOKE_CONSUME, // This is PRODUCE_CONSUME an EITHER task with NON_BLOCKING invocation
PRODUCE_EXECUTE_CONSUME,
EXECUTE_PRODUCE_CONSUME // Eat What You Kill!
}
private final AutoLock _lock = new AutoLock();
private final LongAdder _pcMode = new LongAdder();
private final LongAdder _picMode = new LongAdder();
private final LongAdder _pecMode = new LongAdder();
private final LongAdder _epcMode = new LongAdder();
private final Producer _producer;
private final Executor _executor;
private final TryExecutor _tryExecutor;
private State _state = State.IDLE;
private boolean _pending;
public EatWhatYouKill(Producer producer, Executor executor)
{
_producer = producer;
_executor = executor;
_tryExecutor = TryExecutor.asTryExecutor(executor);
addBean(_producer);
addBean(_tryExecutor);
if (LOG.isDebugEnabled())
LOG.debug("{} created", this);
}
@Override
public void dispatch()
{
boolean execute = false;
try (AutoLock l = _lock.lock())
{
switch (_state)
{
case IDLE:
if (!_pending)
{
_pending = true;
execute = true;
}
break;
case PRODUCING:
_state = State.REPRODUCING;
break;
default:
break;
}
}
if (LOG.isDebugEnabled())
LOG.debug("{} dispatch {}", this, execute);
if (execute)
_executor.execute(this);
}
@Override
public void run()
{
tryProduce(true);
}
@Override
public void produce()
{
tryProduce(false);
}
private void tryProduce(boolean wasPending)
{
if (LOG.isDebugEnabled())
LOG.debug("{} tryProduce {}", this, wasPending);
try (AutoLock l = _lock.lock())
{
if (wasPending)
_pending = false;
switch (_state)
{
case IDLE:
// Enter PRODUCING
_state = State.PRODUCING;
break;
case PRODUCING:
// Keep other Thread producing
_state = State.REPRODUCING;
return;
default:
return;
}
}
boolean nonBlocking = Invocable.isNonBlockingInvocation();
while (isRunning())
{
try
{
if (doProduce(nonBlocking))
continue;
return;
}
catch (Throwable th)
{
LOG.warn("Unable to produce", th);
}
}
}
private boolean doProduce(boolean nonBlocking)
{
Runnable task = produceTask();
if (task == null)
{
try (AutoLock l = _lock.lock())
{
// Could another task just have been queued with a produce call?
switch (_state)
{
case PRODUCING:
_state = State.IDLE;
return false;
case REPRODUCING:
_state = State.PRODUCING;
return true;
default:
throw new IllegalStateException(toStringLocked());
}
}
}
Mode mode;
if (nonBlocking)
{
// The calling thread cannot block, so we only have a choice between PC and PEC modes,
// based on the invocation type of the task
switch (Invocable.getInvocationType(task))
{
case NON_BLOCKING:
mode = Mode.PRODUCE_CONSUME;
break;
case EITHER:
mode = Mode.PRODUCE_INVOKE_CONSUME;
break;
default:
mode = Mode.PRODUCE_EXECUTE_CONSUME;
break;
}
}
else
{
// The calling thread can block, so we can choose between PC, PEC and EPC modes,
// based on the invocation type of the task and if a reserved thread is available
switch (Invocable.getInvocationType(task))
{
case NON_BLOCKING:
mode = Mode.PRODUCE_CONSUME;
break;
case BLOCKING:
// The task is blocking, so PC is not an option. Thus we choose
// between EPC and PEC based on the availability of a reserved thread.
try (AutoLock l = _lock.lock())
{
if (_pending)
{
_state = State.IDLE;
mode = Mode.EXECUTE_PRODUCE_CONSUME;
}
else if (_tryExecutor.tryExecute(this))
{
_pending = true;
_state = State.IDLE;
mode = Mode.EXECUTE_PRODUCE_CONSUME;
}
else
{
mode = Mode.PRODUCE_EXECUTE_CONSUME;
}
}
break;
case EITHER:
// The task may be non blocking, so PC is an option. Thus we choose
// between EPC and PC based on the availability of a reserved thread.
try (AutoLock l = _lock.lock())
{
if (_pending)
{
_state = State.IDLE;
mode = Mode.EXECUTE_PRODUCE_CONSUME;
}
else if (_tryExecutor.tryExecute(this))
{
_pending = true;
_state = State.IDLE;
mode = Mode.EXECUTE_PRODUCE_CONSUME;
}
else
{
// PC mode, but we must consume with non-blocking invocation
// as we may be the last thread and we cannot block
mode = Mode.PRODUCE_INVOKE_CONSUME;
}
}
break;
default:
throw new IllegalStateException(toString());
}
}
if (LOG.isDebugEnabled())
LOG.debug("{} m={} t={}/{}", this, mode, task, Invocable.getInvocationType(task));
// Consume or execute task
switch (mode)
{
case PRODUCE_CONSUME:
_pcMode.increment();
runTask(task);
return true;
case PRODUCE_INVOKE_CONSUME:
_picMode.increment();
invokeTask(task);
return true;
case PRODUCE_EXECUTE_CONSUME:
_pecMode.increment();
execute(task);
return true;
case EXECUTE_PRODUCE_CONSUME:
_epcMode.increment();
runTask(task);
// Try to produce again?
try (AutoLock l = _lock.lock())
{
if (_state == State.IDLE)
{
// We beat the pending producer, so we will become the producer instead
_state = State.PRODUCING;
return true;
}
}
return false;
default:
throw new IllegalStateException(toString());
}
}
private void runTask(Runnable task)
{
try
{
task.run();
}
catch (Throwable x)
{
LOG.warn("Task run failed", x);
}
}
private void invokeTask(Runnable task)
{
try
{
Invocable.invokeNonBlocking(task);
}
catch (Throwable x)
{
LOG.warn("Task invoke failed", x);
}
}
private Runnable produceTask()
{
try
{
return _producer.produce();
}
catch (Throwable e)
{
LOG.warn("Task produce failed", e);
return null;
}
}
private void execute(Runnable task)
{
try
{
_executor.execute(task);
}
catch (RejectedExecutionException e)
{
if (isRunning())
LOG.warn("Execute failed", e);
else
LOG.trace("IGNORED", e);
if (task instanceof Closeable)
{
try
{
((Closeable)task).close();
}
catch (Throwable e2)
{
LOG.trace("IGNORED", e2);
}
}
}
}
@ManagedAttribute(value = "number of tasks consumed with PC mode", readonly = true)
public long getPCTasksConsumed()
{
return _pcMode.longValue();
}
@ManagedAttribute(value = "number of tasks executed with PIC mode", readonly = true)
public long getPICTasksExecuted()
{
return _picMode.longValue();
}
@ManagedAttribute(value = "number of tasks executed with PEC mode", readonly = true)
public long getPECTasksExecuted()
{
return _pecMode.longValue();
}
@ManagedAttribute(value = "number of tasks consumed with EPC mode", readonly = true)
public long getEPCTasksConsumed()
{
return _epcMode.longValue();
}
@ManagedAttribute(value = "whether this execution strategy is idle", readonly = true)
public boolean isIdle()
{
try (AutoLock l = _lock.lock())
{
return _state == State.IDLE;
}
}
@ManagedOperation(value = "resets the task counts", impact = "ACTION")
public void reset()
{
_pcMode.reset();
_epcMode.reset();
_pecMode.reset();
_picMode.reset();
}
@Override
public String toString()
{
try (AutoLock l = _lock.lock())
{
return toStringLocked();
}
}
public String toStringLocked()
{
StringBuilder builder = new StringBuilder();
getString(builder);
getState(builder);
return builder.toString();
}
private void getString(StringBuilder builder)
{
builder.append(getClass().getSimpleName());
builder.append('@');
builder.append(Integer.toHexString(hashCode()));
builder.append('/');
builder.append(_producer);
builder.append('/');
}
private void getState(StringBuilder builder)
{
builder.append(_state);
builder.append("/p=");
builder.append(_pending);
builder.append('/');
builder.append(_tryExecutor);
builder.append("[pc=");
builder.append(getPCTasksConsumed());
builder.append(",pic=");
builder.append(getPICTasksExecuted());
builder.append(",pec=");
builder.append(getPECTasksExecuted());
builder.append(",epc=");
builder.append(getEPCTasksConsumed());
builder.append("]");
builder.append("@");
builder.append(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()));
}
}