de.mklinger.qetcher.client.jetty.io.WriteFlusher Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of qetcher-client-bundle Show documentation
Show all versions of qetcher-client-bundle Show documentation
Qetcher Java client, OSGi bundle, minimal dependencies
//
// ========================================================================
// Copyright (c) 1995-2018 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.io;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.WritePendingException;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.Invocable;
import org.eclipse.jetty.util.thread.Invocable.InvocationType;
/**
* A Utility class to help implement {@link EndPoint#write(Callback, ByteBuffer...)} by calling
* {@link EndPoint#flush(ByteBuffer...)} until all content is written.
* The abstract method {@link #onIncompleteFlush()} is called when not all content has been written after a call to
* flush and should organize for the {@link #completeWrite()} method to be called when a subsequent call to flush
* should be able to make more progress.
*/
abstract public class WriteFlusher
{
private static final Logger LOG = Log.getLogger(WriteFlusher.class);
private static final boolean DEBUG = LOG.isDebugEnabled(); // Easy for the compiler to remove the code if DEBUG==false
private static final ByteBuffer[] EMPTY_BUFFERS = new ByteBuffer[]{BufferUtil.EMPTY_BUFFER};
private static final EnumMap> __stateTransitions = new EnumMap<>(StateType.class);
private static final State __IDLE = new IdleState();
private static final State __WRITING = new WritingState();
private static final State __COMPLETING = new CompletingState();
private final EndPoint _endPoint;
private final AtomicReference _state = new AtomicReference<>();
static
{
// fill the state machine
__stateTransitions.put(StateType.IDLE, EnumSet.of(StateType.WRITING));
__stateTransitions.put(StateType.WRITING, EnumSet.of(StateType.IDLE, StateType.PENDING, StateType.FAILED));
__stateTransitions.put(StateType.PENDING, EnumSet.of(StateType.COMPLETING, StateType.IDLE));
__stateTransitions.put(StateType.COMPLETING, EnumSet.of(StateType.IDLE, StateType.PENDING, StateType.FAILED));
__stateTransitions.put(StateType.FAILED, EnumSet.of(StateType.IDLE));
}
// A write operation may either complete immediately:
// IDLE-->WRITING-->IDLE
// Or it may not completely flush and go via the PENDING state
// IDLE-->WRITING-->PENDING-->COMPLETING-->IDLE
// Or it may take several cycles to complete
// IDLE-->WRITING-->PENDING-->COMPLETING-->PENDING-->COMPLETING-->IDLE
//
// If a failure happens while in IDLE, it is a noop since there is no operation to tell of the failure.
// If a failure happens while in WRITING, but the the write has finished successfully or with an IOExceptions,
// the callback's complete or respectively failed methods will be called.
// If a failure happens in PENDING state, then the fail method calls the pending callback and moves to IDLE state
//
// IDLE--(fail)-->IDLE
// IDLE-->WRITING--(fail)-->FAILED-->IDLE
// IDLE-->WRITING-->PENDING--(fail)-->IDLE
// IDLE-->WRITING-->PENDING-->COMPLETING--(fail)-->FAILED-->IDLE
//
// So a call to fail in the PENDING state will be directly handled and the state changed to IDLE
// A call to fail in the WRITING or COMPLETING states will just set the state to FAILED and the failure will be
// handled with the write or completeWrite methods try to move the state from what they thought it was.
//
protected WriteFlusher(EndPoint endPoint)
{
_state.set(__IDLE);
_endPoint = endPoint;
}
private enum StateType
{
IDLE,
WRITING,
PENDING,
COMPLETING,
FAILED
}
/**
* Tries to update the current state to the given new state.
*
* @param previous the expected current state
* @param next the desired new state
* @return the previous state or null if the state transition failed
* @throws WritePendingException if currentState is WRITING and new state is WRITING (api usage error)
*/
private boolean updateState(State previous, State next)
{
if (!isTransitionAllowed(previous, next))
throw new IllegalStateException();
boolean updated = _state.compareAndSet(previous, next);
if (DEBUG)
LOG.debug("update {}:{}{}{}", this, previous, updated ? "-->" : "!->", next);
return updated;
}
private void fail(PendingState pending)
{
State current = _state.get();
if (current.getType() == StateType.FAILED)
{
FailedState failed = (FailedState)current;
if (updateState(failed, __IDLE))
{
pending.fail(failed.getCause());
return;
}
}
throw new IllegalStateException();
}
private void ignoreFail()
{
State current = _state.get();
while (current.getType() == StateType.FAILED)
{
if (updateState(current, __IDLE))
return;
current = _state.get();
}
}
private boolean isTransitionAllowed(State currentState, State newState)
{
Set allowedNewStateTypes = __stateTransitions.get(currentState.getType());
if (!allowedNewStateTypes.contains(newState.getType()))
{
LOG.warn("{}: {} -> {} not allowed", this, currentState, newState);
return false;
}
return true;
}
/**
* State represents a State of WriteFlusher.
*/
private static class State
{
private final StateType _type;
private State(StateType stateType)
{
_type = stateType;
}
public StateType getType()
{
return _type;
}
@Override
public String toString()
{
return String.format("%s", _type);
}
}
/**
* In IdleState WriteFlusher is idle and accepts new writes
*/
private static class IdleState extends State
{
private IdleState()
{
super(StateType.IDLE);
}
}
/**
* In WritingState WriteFlusher is currently writing.
*/
private static class WritingState extends State
{
private WritingState()
{
super(StateType.WRITING);
}
}
/**
* In FailedState no more operations are allowed. The current implementation will never recover from this state.
*/
private static class FailedState extends State
{
private final Throwable _cause;
private FailedState(Throwable cause)
{
super(StateType.FAILED);
_cause = cause;
}
public Throwable getCause()
{
return _cause;
}
}
/**
* In CompletingState WriteFlusher is flushing buffers that have not been fully written in write(). If write()
* didn't flush all buffers in one go, it'll switch the State to PendingState. completeWrite() will then switch to
* this state and try to flush the remaining buffers.
*/
private static class CompletingState extends State
{
private CompletingState()
{
super(StateType.COMPLETING);
}
}
/**
* In PendingState not all buffers could be written in one go. Then write() will switch to PendingState() and
* preserve the state by creating a new PendingState object with the given parameters.
*/
private class PendingState extends State
{
private final Callback _callback;
private final ByteBuffer[] _buffers;
private PendingState(ByteBuffer[] buffers, Callback callback)
{
super(StateType.PENDING);
_buffers = buffers;
_callback = callback;
}
public ByteBuffer[] getBuffers()
{
return _buffers;
}
protected boolean fail(Throwable cause)
{
if (_callback != null)
{
_callback.failed(cause);
return true;
}
return false;
}
protected void complete()
{
if (_callback != null)
_callback.succeeded();
}
InvocationType getCallbackInvocationType()
{
return Invocable.getInvocationType(_callback);
}
public Object getCallback()
{
return _callback;
}
}
public InvocationType getCallbackInvocationType()
{
State s = _state.get();
return (s instanceof PendingState)
? ((PendingState)s).getCallbackInvocationType()
: Invocable.InvocationType.BLOCKING;
}
/**
* Abstract call to be implemented by specific WriteFlushers. It should schedule a call to {@link #completeWrite()}
* or {@link #onFail(Throwable)} when appropriate.
*/
abstract protected void onIncompleteFlush();
/**
* Tries to switch state to WRITING. If successful it writes the given buffers to the EndPoint. If state transition
* fails it'll fail the callback.
*
* If not all buffers can be written in one go it creates a new {@code PendingState} object to preserve the state
* and then calls {@link #onIncompleteFlush()}. The remaining buffers will be written in {@link #completeWrite()}.
*
* If all buffers have been written it calls callback.complete().
*
* @param callback the callback to call on either failed or complete
* @param buffers the buffers to flush to the endpoint
* @throws WritePendingException if unable to write due to prior pending write
*/
public void write(Callback callback, ByteBuffer... buffers) throws WritePendingException
{
if (DEBUG)
LOG.debug("write: {} {}", this, BufferUtil.toDetailString(buffers));
if (!updateState(__IDLE, __WRITING))
throw new WritePendingException();
try
{
buffers = flush(buffers);
// if we are incomplete?
if (buffers != null)
{
if (DEBUG)
LOG.debug("flushed incomplete");
PendingState pending = new PendingState(buffers, callback);
if (updateState(__WRITING, pending))
onIncompleteFlush();
else
fail(pending);
return;
}
// If updateState didn't succeed, we don't care as our buffers have been written
if (!updateState(__WRITING, __IDLE))
ignoreFail();
if (callback != null)
callback.succeeded();
}
catch (IOException e)
{
if (DEBUG)
LOG.debug("write exception", e);
if (updateState(__WRITING, __IDLE))
{
if (callback != null)
callback.failed(e);
}
else
fail(new PendingState(buffers, callback));
}
}
/**
* Complete a write that has not completed and that called {@link #onIncompleteFlush()} to request a call to this
* method when a call to {@link EndPoint#flush(ByteBuffer...)} is likely to be able to progress.
*
* It tries to switch from PENDING to COMPLETING. If state transition fails, then it does nothing as the callback
* should have been already failed. That's because the only way to switch from PENDING outside this method is
* {@link #onFail(Throwable)} or {@link #onClose()}
*/
public void completeWrite()
{
if (DEBUG)
LOG.debug("completeWrite: {}", this);
State previous = _state.get();
if (previous.getType() != StateType.PENDING)
return; // failure already handled.
PendingState pending = (PendingState)previous;
if (!updateState(pending, __COMPLETING))
return; // failure already handled.
try
{
ByteBuffer[] buffers = pending.getBuffers();
buffers = flush(buffers);
// if we are incomplete?
if (buffers != null)
{
if (DEBUG)
LOG.debug("flushed incomplete {}", BufferUtil.toDetailString(buffers));
if (buffers != pending.getBuffers())
pending = new PendingState(buffers, pending._callback);
if (updateState(__COMPLETING, pending))
onIncompleteFlush();
else
fail(pending);
return;
}
// If updateState didn't succeed, we don't care as our buffers have been written
if (!updateState(__COMPLETING, __IDLE))
ignoreFail();
pending.complete();
}
catch (IOException e)
{
if (DEBUG)
LOG.debug("completeWrite exception", e);
if (updateState(__COMPLETING, __IDLE))
pending.fail(e);
else
fail(pending);
}
}
/**
* Flushes the buffers iteratively until no progress is made.
*
* @param buffers The buffers to flush
* @return The unflushed buffers, or null if all flushed
* @throws IOException if unable to flush
*/
protected ByteBuffer[] flush(ByteBuffer[] buffers) throws IOException
{
boolean progress = true;
while (progress && buffers != null)
{
long before = remaining(buffers);
boolean flushed = _endPoint.flush(buffers);
long after = remaining(buffers);
long written = before - after;
if (LOG.isDebugEnabled())
LOG.debug("Flushed={} written={} remaining={} {}", flushed, written, after, this);
if (written > 0)
{
Connection connection = _endPoint.getConnection();
if (connection instanceof Listener)
((Listener)connection).onFlushed(written);
}
if (flushed)
return null;
progress = written > 0;
int index = 0;
while (true)
{
if (index == buffers.length)
{
// All buffers consumed.
buffers = null;
index = 0;
break;
}
else
{
int remaining = buffers[index].remaining();
if (remaining > 0)
break;
++index;
progress = true;
}
}
if (index > 0)
buffers = Arrays.copyOfRange(buffers, index, buffers.length);
}
if (LOG.isDebugEnabled())
LOG.debug("!fully flushed {}", this);
// If buffers is null, then flush has returned false but has consumed all the data!
// This is probably SSL being unable to flush the encrypted buffer, so return EMPTY_BUFFERS
// and that will keep this WriteFlusher pending.
return buffers == null ? EMPTY_BUFFERS : buffers;
}
private long remaining(ByteBuffer[] buffers)
{
if (buffers == null)
return 0;
long result = 0;
for (ByteBuffer buffer : buffers)
result += buffer.remaining();
return result;
}
/**
* Notify the flusher of a failure
*
* @param cause The cause of the failure
* @return true if the flusher passed the failure to a {@link Callback} instance
*/
public boolean onFail(Throwable cause)
{
// Keep trying to handle the failure until we get to IDLE or FAILED state
while (true)
{
State current = _state.get();
switch (current.getType())
{
case IDLE:
case FAILED:
if (DEBUG)
LOG.debug("ignored: " + this, cause);
return false;
case PENDING:
if (DEBUG)
LOG.debug("failed: " + this, cause);
PendingState pending = (PendingState)current;
if (updateState(pending, __IDLE))
return pending.fail(cause);
break;
default:
if (DEBUG)
LOG.debug("failed: " + this, cause);
if (updateState(current, new FailedState(cause)))
return false;
break;
}
}
}
public void onClose()
{
onFail(new ClosedChannelException());
}
boolean isIdle()
{
return _state.get().getType() == StateType.IDLE;
}
public String toStateString()
{
switch (_state.get().getType())
{
case WRITING:
return "W";
case PENDING:
return "P";
case COMPLETING:
return "C";
case IDLE:
return "-";
case FAILED:
return "F";
default:
return "?";
}
}
@Override
public String toString()
{
State s = _state.get();
return String.format("WriteFlusher@%x{%s}->%s", hashCode(), s, s instanceof PendingState ? ((PendingState)s).getCallback() : null);
}
/**
* A listener of {@link WriteFlusher} events.
*/
public interface Listener
{
/**
* Invoked when a {@link WriteFlusher} flushed bytes in a non-blocking way,
* as part of a - possibly larger - write.
* This method may be invoked multiple times, for example when writing a large
* buffer: a first flush of bytes, then the connection became TCP congested, and
* a subsequent flush of bytes when the connection became writable again.
* This method is never invoked concurrently, but may be invoked by different
* threads, so implementations may not rely on thread-local variables.
* Implementations may throw an {@link IOException} to signal that the write
* should fail, for example if the implementation enforces a minimum data rate.
*
* @param bytes the number of bytes flushed
* @throws IOException if the write should fail
*/
void onFlushed(long bytes) throws IOException;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy