
io.aeron.archive.client.AeronArchive Maven / Gradle / Ivy
Show all versions of aeron-all Show documentation
/*
* Copyright 2014-2024 Real Logic Limited.
*
* 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
*
* https://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 io.aeron.archive.client;
import io.aeron.*;
import io.aeron.archive.codecs.ControlResponseCode;
import io.aeron.archive.codecs.RecordingSignal;
import io.aeron.archive.codecs.RecordingSignalEventDecoder;
import io.aeron.archive.codecs.SourceLocation;
import io.aeron.config.Config;
import io.aeron.config.DefaultType;
import io.aeron.exceptions.AeronException;
import io.aeron.exceptions.ConcurrentConcludeException;
import io.aeron.exceptions.ConfigurationException;
import io.aeron.exceptions.TimeoutException;
import io.aeron.security.CredentialsSupplier;
import io.aeron.security.NullCredentialsSupplier;
import io.aeron.version.Versioned;
import org.agrona.CloseHelper;
import org.agrona.ErrorHandler;
import org.agrona.LangUtil;
import org.agrona.SemanticVersion;
import org.agrona.concurrent.AgentInvoker;
import org.agrona.concurrent.BackoffIdleStrategy;
import org.agrona.concurrent.IdleStrategy;
import org.agrona.concurrent.NanoClock;
import org.agrona.concurrent.NoOpLock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static io.aeron.archive.client.AeronArchive.Configuration.MESSAGE_TIMEOUT_DEFAULT_NS;
import static io.aeron.archive.client.ArchiveProxy.DEFAULT_RETRY_ATTEMPTS;
import static io.aeron.driver.Configuration.*;
import static java.util.concurrent.atomic.AtomicIntegerFieldUpdater.newUpdater;
import static org.agrona.SystemUtil.getDurationInNanos;
import static org.agrona.SystemUtil.getSizeAsInt;
/**
* Client for interacting with a local or remote Aeron Archive which records and replays message streams from storage.
*
* This client provides a simple interaction model which is mostly synchronous and may not be optimal.
* The underlying components such as the {@link ArchiveProxy} and the {@link ControlResponsePoller} or
* {@link RecordingDescriptorPoller} may be used directly if a more asynchronous interaction is required.
*
* Note: This class is threadsafe but the lock can be elided for single threaded access via {@link Context#lock(Lock)}
* being set to {@link NoOpLock}.
*/
@Versioned
public final class AeronArchive implements AutoCloseable
{
/**
* Represents a timestamp that has not been set. Can be used when the time is not known.
*/
public static final long NULL_TIMESTAMP = Aeron.NULL_VALUE;
/**
* Represents a position that has not been set. Can be used when the position is not known.
*/
public static final long NULL_POSITION = Aeron.NULL_VALUE;
/**
* Represents a length that has not been set. If null length is provided then replay the whole recorded stream.
*/
public static final long NULL_LENGTH = Aeron.NULL_VALUE;
/**
* Indicates the client is no longer connected to an archive.
*/
public static final String NOT_CONNECTED_MSG = "not connected";
private static final int FRAGMENT_LIMIT = 10;
private boolean isClosed = false;
private boolean isInCallback = false;
private long lastCorrelationId = Aeron.NULL_VALUE;
private final long controlSessionId;
private final long archiveId;
private final long messageTimeoutNs;
private final Context context;
private final Aeron aeron;
private final ArchiveProxy archiveProxy;
private final IdleStrategy idleStrategy;
private final ControlResponsePoller controlResponsePoller;
private final Lock lock;
private final NanoClock nanoClock;
private final AgentInvoker aeronClientInvoker;
private final AgentInvoker agentInvoker;
private RecordingDescriptorPoller recordingDescriptorPoller;
private RecordingSubscriptionDescriptorPoller recordingSubscriptionDescriptorPoller;
AeronArchive(
final Context context,
final ControlResponsePoller controlResponsePoller,
final ArchiveProxy archiveProxy,
final long controlSessionId,
final long archiveId)
{
this.context = context;
aeron = context.aeron();
aeronClientInvoker = aeron.conductorAgentInvoker();
agentInvoker = context.agentInvoker();
idleStrategy = context.idleStrategy();
messageTimeoutNs = context.messageTimeoutNs();
lock = context.lock();
nanoClock = aeron.context().nanoClock();
this.controlResponsePoller = controlResponsePoller;
this.archiveProxy = archiveProxy;
this.controlSessionId = controlSessionId;
this.archiveId = archiveId;
}
/**
* Position of the recorded stream at the base of a segment file. If a recording starts within a term
* then the base position can be before the recording started.
*
* @param startPosition of the stream.
* @param position of the stream to calculate the segment base position from.
* @param termBufferLength of the stream.
* @param segmentFileLength which is a multiple of term length.
* @return the position of the recorded stream at the beginning of a segment file.
*/
public static long segmentFileBasePosition(
final long startPosition, final long position, final int termBufferLength, final int segmentFileLength)
{
final long startTermBasePosition = startPosition - (startPosition & (termBufferLength - 1));
final long lengthFromBasePosition = position - startTermBasePosition;
final long segments = (lengthFromBasePosition - (lengthFromBasePosition & (segmentFileLength - 1)));
return startTermBasePosition + segments;
}
/**
* Notify the archive that this control session is closed, so it can promptly release resources then close the
* local resources associated with the client.
*/
public void close()
{
lock.lock();
try
{
if (!isClosed)
{
isClosed = true;
final ErrorHandler errorHandler = context.errorHandler();
Exception resultEx = null;
if (archiveProxy.publication().isConnected())
{
resultEx = quietClose(resultEx, () -> archiveProxy.closeSession(controlSessionId));
}
if (!context.ownsAeronClient())
{
resultEx = quietClose(resultEx, archiveProxy.publication());
resultEx = quietClose(resultEx, controlResponsePoller.subscription());
}
boolean rethrow = false;
try
{
context.close();
}
catch (final Exception ex)
{
rethrow = true;
if (null != resultEx)
{
resultEx.addSuppressed(ex);
}
else
{
resultEx = ex;
}
}
if (null != resultEx)
{
if (null != errorHandler)
{
errorHandler.onError(resultEx);
}
if (rethrow)
{
LangUtil.rethrowUnchecked(resultEx);
}
}
}
}
finally
{
lock.unlock();
}
}
/**
* Connect to an Aeron archive using a default {@link Context}. This will create a control session.
*
* @return the newly created Aeron Archive client.
*/
public static AeronArchive connect()
{
return connect(new Context());
}
/**
* Connect to an Aeron archive by providing a {@link Context}. This will create a control session.
*
* Before connecting {@link Context#conclude()} will be called.
* If an exception occurs then {@link Context#close()} will be called.
*
* @param ctx for connection configuration.
* @return the newly created Aeron Archive client.
*/
public static AeronArchive connect(final Context ctx)
{
Subscription subscription = null;
Publication publication = null;
AsyncConnect asyncConnect = null;
try
{
ctx.conclude();
final Aeron aeron = ctx.aeron();
subscription = aeron.addSubscription(ctx.controlResponseChannel(), ctx.controlResponseStreamId());
checkAndSetupResponseChannel(ctx, subscription);
publication = aeron.addExclusivePublication(ctx.controlRequestChannel(), ctx.controlRequestStreamId());
final ControlResponsePoller controlResponsePoller = new ControlResponsePoller(subscription);
final ArchiveProxy archiveProxy = new ArchiveProxy(
publication,
ctx.idleStrategy(),
aeron.context().nanoClock(),
ctx.messageTimeoutNs(),
DEFAULT_RETRY_ATTEMPTS,
ctx.credentialsSupplier());
asyncConnect = new AsyncConnect(ctx, controlResponsePoller, archiveProxy);
final IdleStrategy idleStrategy = ctx.idleStrategy();
final AgentInvoker aeronClientInvoker = aeron.conductorAgentInvoker();
final AgentInvoker delegatingInvoker = ctx.agentInvoker();
int previousStep = asyncConnect.step();
AeronArchive aeronArchive;
while (null == (aeronArchive = asyncConnect.poll()))
{
if (asyncConnect.step() == previousStep)
{
idleStrategy.idle();
}
else
{
idleStrategy.reset();
previousStep = asyncConnect.step();
}
if (null != aeronClientInvoker)
{
aeronClientInvoker.invoke();
}
if (null != delegatingInvoker)
{
delegatingInvoker.invoke();
}
}
return aeronArchive;
}
catch (final ConcurrentConcludeException ex)
{
throw ex;
}
catch (final Exception ex)
{
if (!ctx.ownsAeronClient())
{
CloseHelper.quietClose(subscription);
CloseHelper.quietClose(publication);
}
CloseHelper.quietCloseAll(asyncConnect, ctx::close);
throw ex;
}
}
/**
* Begin an attempt at creating a connection which can be completed by calling {@link AsyncConnect#poll()} until
* it returns the client, before complete it will return null.
*
* @return the {@link AsyncConnect} that can be polled for completion.
*/
public static AsyncConnect asyncConnect()
{
return asyncConnect(new Context());
}
/**
* Begin an attempt at creating a connection which can be completed by calling {@link AsyncConnect#poll()} until
* it returns the client, before complete it will return null.
*
* @param ctx for the archive connection.
* @return the {@link AsyncConnect} that can be polled for completion.
*/
public static AsyncConnect asyncConnect(final Context ctx)
{
try
{
ctx.conclude();
return new AsyncConnect(ctx);
}
catch (final ConcurrentConcludeException ex)
{
throw ex;
}
catch (final Exception ex)
{
ctx.close();
throw ex;
}
}
/**
* Get the {@link Context} used to connect this archive client.
*
* @return the {@link Context} used to connect this archive client.
*/
public Context context()
{
return context;
}
/**
* The last correlation id used for sending a request to the archive via method on this class.
*
* @return last correlation id used for sending a request to the archive.
*/
public long lastCorrelationId()
{
return lastCorrelationId;
}
/**
* The control session id allocated for this connection to the archive.
*
* @return control session id allocated for this connection to the archive.
*/
public long controlSessionId()
{
return controlSessionId;
}
/**
* The {@link ArchiveProxy} for send asynchronous messages to the connected archive.
*
* @return the {@link ArchiveProxy} for send asynchronous messages to the connected archive.
*/
public ArchiveProxy archiveProxy()
{
return archiveProxy;
}
/**
* Get the {@link ControlResponsePoller} for polling additional events on the control channel.
*
* @return the {@link ControlResponsePoller} for polling additional events on the control channel.
*/
public ControlResponsePoller controlResponsePoller()
{
return controlResponsePoller;
}
/**
* Get the {@link RecordingDescriptorPoller} for polling recording descriptors on the control channel.
*
* @return the {@link RecordingDescriptorPoller} for polling recording descriptors on the control channel.
*/
public RecordingDescriptorPoller recordingDescriptorPoller()
{
if (null == recordingDescriptorPoller)
{
recordingDescriptorPoller = new RecordingDescriptorPoller(
controlResponsePoller.subscription(),
context.errorHandler(),
context.recordingSignalConsumer(),
controlSessionId,
FRAGMENT_LIMIT);
}
return recordingDescriptorPoller;
}
/**
* The {@link RecordingSubscriptionDescriptorPoller} for polling subscription descriptors on the control channel.
*
* @return the {@link RecordingSubscriptionDescriptorPoller} for polling subscription descriptors on the control
* channel.
*/
public RecordingSubscriptionDescriptorPoller recordingSubscriptionDescriptorPoller()
{
if (null == recordingSubscriptionDescriptorPoller)
{
recordingSubscriptionDescriptorPoller = new RecordingSubscriptionDescriptorPoller(
controlResponsePoller.subscription(),
context.errorHandler(),
context.recordingSignalConsumer(),
controlSessionId,
FRAGMENT_LIMIT);
}
return recordingSubscriptionDescriptorPoller;
}
/**
* Poll the response stream once for an error. If another message is present then it will be skipped over
* so only call when not expecting another response. If not connected then return {@link #NOT_CONNECTED_MSG}.
*
* @return the error String otherwise null if no error is found.
*/
public String pollForErrorResponse()
{
lock.lock();
try
{
ensureOpen();
final ControlResponsePoller poller = controlResponsePoller;
if (!poller.subscription().isConnected())
{
return NOT_CONNECTED_MSG;
}
if (poller.poll() != 0 && poller.isPollComplete())
{
if (poller.controlSessionId() == controlSessionId)
{
if (poller.code() == ControlResponseCode.ERROR)
{
return poller.errorMessage();
}
else if (poller.templateId() == RecordingSignalEventDecoder.TEMPLATE_ID)
{
dispatchRecordingSignal(poller);
}
}
}
return null;
}
finally
{
lock.unlock();
}
}
/**
* Check if an error has been returned for the control session, or if it is no longer connected, and then throw
* a {@link ArchiveException} if {@link Context#errorHandler(ErrorHandler)} is not set.
*
* To check for an error response without raising an exception then try {@link #pollForErrorResponse()}.
*
* @see #pollForErrorResponse()
*/
public void checkForErrorResponse()
{
lock.lock();
try
{
ensureOpen();
final ControlResponsePoller poller = controlResponsePoller;
if (!poller.subscription().isConnected())
{
if (null != context.errorHandler())
{
context.errorHandler().onError(new ArchiveException(NOT_CONNECTED_MSG));
}
else
{
throw new ArchiveException(NOT_CONNECTED_MSG);
}
}
else if (poller.poll() != 0 && poller.isPollComplete())
{
if (poller.controlSessionId() == controlSessionId)
{
if (poller.code() == ControlResponseCode.ERROR)
{
final ArchiveException ex = new ArchiveException(
poller.errorMessage(),
(int)poller.relevantId(),
poller.correlationId());
if (null != context.errorHandler())
{
context.errorHandler().onError(ex);
}
else
{
throw ex;
}
}
else if (poller.templateId() == RecordingSignalEventDecoder.TEMPLATE_ID)
{
dispatchRecordingSignal(poller);
}
}
}
}
finally
{
lock.unlock();
}
}
/**
* Poll for {@link RecordingSignal}s for this session which will be dispatched to
* {@link Context#recordingSignalConsumer}.
*
* @return positive value if signals dispatched otherwise 0.
*/
public int pollForRecordingSignals()
{
lock.lock();
try
{
ensureOpen();
final ControlResponsePoller poller = controlResponsePoller;
if (poller.poll() != 0 && poller.isPollComplete())
{
if (poller.controlSessionId() == controlSessionId)
{
if (poller.code() == ControlResponseCode.ERROR)
{
final ArchiveException ex = new ArchiveException(
poller.errorMessage(),
(int)poller.relevantId(),
poller.correlationId());
if (null != context.errorHandler())
{
context.errorHandler().onError(ex);
}
else
{
throw ex;
}
}
else if (poller.templateId() == RecordingSignalEventDecoder.TEMPLATE_ID)
{
dispatchRecordingSignal(poller);
return 1;
}
}
}
return 0;
}
finally
{
lock.unlock();
}
}
/**
* Add a {@link Publication} and set it up to be recorded. If this is not the first,
* i.e. {@link Publication#isOriginal()} is true, then an {@link ArchiveException}
* will be thrown and the recording not initiated.
*
* This is a sessionId specific recording.
*
* @param channel for the publication.
* @param streamId for the publication.
* @return the {@link Publication} ready for use.
*/
public Publication addRecordedPublication(final String channel, final int streamId)
{
Publication publication = null;
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
publication = aeron.addPublication(channel, streamId);
if (!publication.isOriginal())
{
throw new ArchiveException(
"publication already added for channel=" + channel + " streamId=" + streamId);
}
startRecording(ChannelUri.addSessionId(channel, publication.sessionId()), streamId, SourceLocation.LOCAL);
}
catch (final RuntimeException ex)
{
CloseHelper.quietClose(publication);
throw ex;
}
finally
{
lock.unlock();
}
return publication;
}
/**
* Add an {@link ExclusivePublication} and set it up to be recorded.
*
* This is a sessionId specific recording.
*
* @param channel for the publication.
* @param streamId for the publication.
* @return the {@link ExclusivePublication} ready for use.
*/
public ExclusivePublication addRecordedExclusivePublication(final String channel, final int streamId)
{
ExclusivePublication publication = null;
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
publication = aeron.addExclusivePublication(channel, streamId);
startRecording(ChannelUri.addSessionId(channel, publication.sessionId()), streamId, SourceLocation.LOCAL);
}
catch (final RuntimeException ex)
{
CloseHelper.quietClose(publication);
throw ex;
}
finally
{
lock.unlock();
}
return publication;
}
/**
* Start recording a channel and stream pairing.
*
* Channels that include sessionId parameters are considered different from channels without sessionIds. If a
* publication matches both a sessionId specific channel recording and a non-sessionId specific recording,
* it will be recorded twice.
*
* @param channel to be recorded.
* @param streamId to be recorded.
* @param sourceLocation of the publication to be recorded.
* @return the subscriptionId, i.e. {@link Subscription#registrationId()}, of the recording. This can be
* passed to {@link #stopRecording(long)}.
*/
public long startRecording(final String channel, final int streamId, final SourceLocation sourceLocation)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.startRecording(channel, streamId, sourceLocation, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send start recording request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Start recording a channel and stream pairing.
*
* Channels that include sessionId parameters are considered different from channels without sessionIds. If a
* publication matches both a sessionId specific channel recording and a non-sessionId specific recording,
* it will be recorded twice.
*
* @param channel to be recorded.
* @param streamId to be recorded.
* @param sourceLocation of the publication to be recorded.
* @param autoStop if the recording should be automatically stopped when complete.
* @return the subscriptionId, i.e. {@link Subscription#registrationId()}, of the recording. This can be
* passed to {@link #stopRecording(long)}. However, if is autoStop is true then no need to stop the recording
* unless you want to abort early.
*/
public long startRecording(
final String channel, final int streamId, final SourceLocation sourceLocation, final boolean autoStop)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.startRecording(
channel, streamId, sourceLocation, autoStop, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send start recording request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Extend an existing, non-active recording of a channel and stream pairing.
*
* The channel must be configured for the initial position from which it will be extended. This can be done
* with {@link ChannelUriStringBuilder#initialPosition(long, int, int)}. The details required to initialise can
* be found by calling {@link #listRecording(long, RecordingDescriptorConsumer)}.
*
* @param recordingId of the existing recording.
* @param channel to be recorded.
* @param streamId to be recorded.
* @param sourceLocation of the publication to be recorded.
* @return the subscriptionId, i.e. {@link Subscription#registrationId()}, of the recording. This can be
* passed to {@link #stopRecording(long)}.
*/
public long extendRecording(
final long recordingId,
final String channel,
final int streamId,
final SourceLocation sourceLocation)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.extendRecording(
channel, streamId, sourceLocation, recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send extend recording request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Extend an existing, non-active recording of a channel and stream pairing.
*
* The channel must be configured for the initial position from which it will be extended. This can be done
* with {@link ChannelUriStringBuilder#initialPosition(long, int, int)}. The details required to initialise can
* be found by calling {@link #listRecording(long, RecordingDescriptorConsumer)}.
*
* @param recordingId of the existing recording.
* @param channel to be recorded.
* @param streamId to be recorded.
* @param sourceLocation of the publication to be recorded.
* @param autoStop if the recording should be automatically stopped when complete.
* @return the subscriptionId, i.e. {@link Subscription#registrationId()}, of the recording. This can be
* passed to {@link #stopRecording(long)}. However, if is autoStop is true then no need to stop the recording
* unless you want to abort early.
*/
public long extendRecording(
final long recordingId,
final String channel,
final int streamId,
final SourceLocation sourceLocation,
final boolean autoStop)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.extendRecording(
channel, streamId, sourceLocation, autoStop, recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send extend recording request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Stop recording for a channel and stream pairing.
*
* Channels that include sessionId parameters are considered different from channels without sessionIds. Stopping
* a recording on a channel without a sessionId parameter will not stop the recording of any sessionId specific
* recordings that use the same channel and streamId.
*
* @param channel to stop recording for.
* @param streamId to stop recording for.
*/
public void stopRecording(final String channel, final int streamId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopRecording(channel, streamId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop recording request");
}
pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Try to stop a recording for a channel and stream pairing.
*
* Channels that include sessionId parameters are considered different from channels without sessionIds. Stopping
* a recording on a channel without a sessionId parameter will not stop the recording of any sessionId specific
* recordings that use the same channel and streamId.
*
* @param channel to stop recording for.
* @param streamId to stop recording for.
* @return {@code true} if the recording was stopped or false if the subscription is not currently active.
*/
public boolean tryStopRecording(final String channel, final int streamId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopRecording(channel, streamId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop recording request");
}
return pollForResponseAllowingError(lastCorrelationId, ArchiveException.UNKNOWN_SUBSCRIPTION);
}
finally
{
lock.unlock();
}
}
/**
* Stop recording for a subscriptionId that has been returned from
* {@link #startRecording(String, int, SourceLocation)} or
* {@link #extendRecording(long, String, int, SourceLocation)}.
*
* @param subscriptionId is the {@link Subscription#registrationId()} for the recording in the archive.
*/
public void stopRecording(final long subscriptionId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopRecording(subscriptionId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop recording request");
}
pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Try stop a recording for a subscriptionId that has been returned from
* {@link #startRecording(String, int, SourceLocation)} or
* {@link #extendRecording(long, String, int, SourceLocation)}.
*
* @param subscriptionId is the {@link Subscription#registrationId()} for the recording in the archive.
* @return {@code true} if the recording was stopped or false if the subscription is not currently active.
*/
public boolean tryStopRecording(final long subscriptionId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopRecording(subscriptionId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop recording request");
}
return pollForResponseAllowingError(lastCorrelationId, ArchiveException.UNKNOWN_SUBSCRIPTION);
}
finally
{
lock.unlock();
}
}
/**
* Try stop an active recording by its recording id.
*
* @param recordingId for which active recording should be stopped.
* @return {@code true} if the recording was stopped or false if the recording is not currently active.
*/
public boolean tryStopRecordingByIdentity(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopRecordingByIdentity(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop recording request");
}
return pollForResponse(lastCorrelationId) != 0;
}
finally
{
lock.unlock();
}
}
/**
* Stop recording a sessionId specific recording that pertains to the given {@link Publication}.
*
* @param publication to stop recording for.
*/
public void stopRecording(final Publication publication)
{
stopRecording(ChannelUri.addSessionId(publication.channel(), publication.sessionId()), publication.streamId());
}
/**
* Start a replay for a length in bytes of a recording from a position. If the position is {@link #NULL_POSITION}
* then the stream will be replayed from the start.
*
* The lower 32-bits of the returned value contains the {@link Image#sessionId()} of the received replay. All
* 64-bits are required to uniquely identify the replay when calling {@link #stopReplay(long)}. The lower 32-bits
* can be obtained by casting the {@code long} value to an {@code int}.
*
* @param recordingId to be replayed.
* @param position from which the replay should begin or {@link #NULL_POSITION} if from the start.
* @param length of the stream to be replayed. Use {@link Long#MAX_VALUE} to follow a live recording or
* {@link #NULL_LENGTH} to replay the whole stream of unknown length.
* @param replayChannel to which the replay should be sent.
* @param replayStreamId to which the replay should be sent.
* @return the id of the replay session which will be the same as the {@link Image#sessionId()} of the received
* replay for correlation with the matching channel and stream id in the lower 32 bits.
*/
public long startReplay(
final long recordingId,
final long position,
final long length,
final String replayChannel,
final int replayStreamId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.replay(
recordingId,
position,
length,
replayChannel,
replayStreamId,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send replay request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Start a replay for a length in bytes of a recording from a position bounded by a position counter.
* If the position is {@link #NULL_POSITION} then the stream will be replayed from the start.
*
* The lower 32-bits of the returned value contains the {@link Image#sessionId()} of the received replay. All
* 64-bits are required to uniquely identify the replay when calling {@link #stopReplay(long)}. The lower 32-bits
* can be obtained by casting the {@code long} value to an {@code int}.
*
* @param recordingId to be replayed.
* @param position from which the replay should begin or {@link #NULL_POSITION} if from the start.
* @param length of the stream to be replayed. Use {@link Long#MAX_VALUE} to follow a live recording or
* {@link #NULL_LENGTH} to replay the whole stream of unknown length.
* @param limitCounterId to use to bound replay.
* @param replayChannel to which the replay should be sent.
* @param replayStreamId to which the replay should be sent.
* @return the id of the replay session which will be the same as the {@link Image#sessionId()} of the received
* replay for correlation with the matching channel and stream id in the lower 32 bits.
*/
public long startBoundedReplay(
final long recordingId,
final long position,
final long length,
final int limitCounterId,
final String replayChannel,
final int replayStreamId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.boundedReplay(
recordingId,
position,
length,
limitCounterId,
replayChannel,
replayStreamId,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send bounded replay request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Start a replay for a recording based upon the parameters set in ReplayParams. By default, it will replay
* all the recording from the start. The ReplayParams is free to be reused when this call completes.
*
* @param recordingId to be replayed.
* @param replayChannel to which the replay should be sent.
* @param replayStreamId to which the replay should be sent.
* @param replayParams optional parameters for the replay
* @return the id of the replay session which will be the same as the {@link Image#sessionId()} of the received
* replay for correlation with the matching channel and stream id in the lower 32 bits.
* @see ReplayParams
*/
public long startReplay(
final long recordingId,
final String replayChannel,
final int replayStreamId,
final ReplayParams replayParams)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
final ChannelUri replayChannelUri = ChannelUri.parse(replayChannel);
if (replayChannelUri.hasControlModeResponse())
{
return startReplayViaResponseChannel(recordingId, replayChannel, replayStreamId, replayParams);
}
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.replay(
recordingId,
replayChannel,
replayStreamId,
replayParams,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send bounded replay request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Stop an existing replay session.
*
* @param replaySessionId to stop replay for which would have been returned from
* {@link #startReplay(long, long, long, String, int)}.
*/
public void stopReplay(final long replaySessionId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopReplay(replaySessionId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop replay request");
}
pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Stop all replay sessions for a given recording id or all replays in general.
*
* @param recordingId to stop replay for or {@link Aeron#NULL_VALUE} for all replays.
*/
public void stopAllReplays(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopAllReplays(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop all replays request");
}
pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Replay a length in bytes of a recording from a position and for convenience create a {@link Subscription}
* to receive the replay. If the position is {@link #NULL_POSITION} then the stream will be replayed from the start.
*
* @param recordingId to be replayed.
* @param position from which the replay should begin or {@link #NULL_POSITION} if from the start.
* @param length of the stream to be replayed or {@link Long#MAX_VALUE} to follow a live recording.
* @param replayChannel to which the replay should be sent.
* @param replayStreamId to which the replay should be sent.
* @return the {@link Subscription} for consuming the replay.
*/
public Subscription replay(
final long recordingId,
final long position,
final long length,
final String replayChannel,
final int replayStreamId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
final ChannelUri replayChannelUri = ChannelUri.parse(replayChannel);
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.replay(
recordingId,
position,
length,
replayChannel,
replayStreamId,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send replay request");
}
final int replaySessionId = (int)pollForResponse(lastCorrelationId);
replayChannelUri.put(CommonContext.SESSION_ID_PARAM_NAME, Integer.toString(replaySessionId));
return aeron.addSubscription(replayChannelUri.toString(), replayStreamId);
}
finally
{
lock.unlock();
}
}
/**
* Replay a length in bytes of a recording from a position and for convenience create a {@link Subscription}
* to receive the replay. If the position is {@link #NULL_POSITION} then the stream will be replayed from the start.
*
* @param recordingId to be replayed.
* @param position from which the replay should begin or {@link #NULL_POSITION} if from the start.
* @param length of the stream to be replayed or {@link Long#MAX_VALUE} to follow a live recording.
* @param replayChannel to which the replay should be sent.
* @param replayStreamId to which the replay should be sent.
* @param availableImageHandler to be called when the replay image becomes available.
* @param unavailableImageHandler to be called when the replay image goes unavailable.
* @return the {@link Subscription} for consuming the replay.
*/
public Subscription replay(
final long recordingId,
final long position,
final long length,
final String replayChannel,
final int replayStreamId,
final AvailableImageHandler availableImageHandler,
final UnavailableImageHandler unavailableImageHandler)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
final ChannelUri replayChannelUri = ChannelUri.parse(replayChannel);
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.replay(
recordingId,
position,
length,
replayChannel,
replayStreamId,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send replay request");
}
final int replaySessionId = (int)pollForResponse(lastCorrelationId);
replayChannelUri.put(CommonContext.SESSION_ID_PARAM_NAME, Integer.toString(replaySessionId));
return aeron.addSubscription(
replayChannelUri.toString(), replayStreamId, availableImageHandler, unavailableImageHandler);
}
finally
{
lock.unlock();
}
}
/**
* Replay a recording based upon the parameters set in ReplayParams. By default, it will replay all the recording
* from the start. The ReplayParams is free to be reused when this call completes.
*
* @param recordingId to be replayed.
* @param replayChannel to which the replay should be sent.
* @param replayStreamId to which the replay should be sent.
* @param replayParams optional parameters for the replay
* @return the {@link Subscription} for consuming the replay.
* @see ReplayParams
*/
public Subscription replay(
final long recordingId,
final String replayChannel,
final int replayStreamId,
final ReplayParams replayParams)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
final ChannelUri replayChannelUri = ChannelUri.parse(replayChannel);
if (replayChannelUri.hasControlModeResponse())
{
return replayViaResponseChannel(recordingId, replayChannel, replayStreamId, replayParams);
}
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.replay(
recordingId,
replayChannel,
replayStreamId,
replayParams,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send replay request");
}
final int replaySessionId = (int)pollForResponse(lastCorrelationId);
replayChannelUri.put(CommonContext.SESSION_ID_PARAM_NAME, Integer.toString(replaySessionId));
return aeron.addSubscription(replayChannelUri.toString(), replayStreamId);
}
finally
{
lock.unlock();
}
}
/**
* List all recording descriptors from a recording id with a limit of record count.
*
* If the recording id is greater than the largest known id then nothing is returned.
*
* @param fromRecordingId at which to begin the listing.
* @param recordCount to limit for each query.
* @param consumer to which the descriptors are dispatched.
* @return the number of descriptors found and consumed.
*/
public int listRecordings(
final long fromRecordingId, final int recordCount, final RecordingDescriptorConsumer consumer)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
isInCallback = true;
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.listRecordings(fromRecordingId, recordCount, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send list recordings request");
}
return pollForDescriptors(lastCorrelationId, recordCount, consumer);
}
finally
{
isInCallback = false;
lock.unlock();
}
}
/**
* List recording descriptors from a recording id with a limit of record count for a given channelFragment and
* stream id.
*
* If the recording id is greater than the largest known id then nothing is returned.
*
* @param fromRecordingId at which to begin the listing.
* @param recordCount to limit for each query.
* @param channelFragment for a contains match on the original channel stored with the archive descriptor.
* @param streamId to match.
* @param consumer to which the descriptors are dispatched.
* @return the number of descriptors found and consumed.
*/
public int listRecordingsForUri(
final long fromRecordingId,
final int recordCount,
final String channelFragment,
final int streamId,
final RecordingDescriptorConsumer consumer)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
isInCallback = true;
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.listRecordingsForUri(
fromRecordingId,
recordCount,
channelFragment,
streamId,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send list recordings request");
}
return pollForDescriptors(lastCorrelationId, recordCount, consumer);
}
finally
{
isInCallback = false;
lock.unlock();
}
}
/**
* List a recording descriptor for a single recording id.
*
* If the recording id is greater than the largest known id then nothing is returned.
*
* @param recordingId at which to begin the listing.
* @param consumer to which the descriptors are dispatched.
* @return the number of descriptors found and consumed.
*/
public int listRecording(final long recordingId, final RecordingDescriptorConsumer consumer)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
isInCallback = true;
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.listRecording(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send list recording request");
}
return pollForDescriptors(lastCorrelationId, 1, consumer);
}
finally
{
isInCallback = false;
lock.unlock();
}
}
/**
* Get the start position for a recording.
*
* @param recordingId of the recording for which the position is required.
* @return the start position of a recording.
* @see #getStopPosition(long)
*/
public long getStartPosition(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.getStartPosition(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send get start position request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Get the position recorded for an active recording. If no active recording then return {@link #NULL_POSITION}.
*
* @param recordingId of the active recording for which the position is required.
* @return the recorded position for the active recording or {@link #NULL_POSITION} if recording not active.
* @see #getStopPosition(long)
*/
public long getRecordingPosition(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.getRecordingPosition(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send get recording position request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Get the stop position for a recording.
*
* @param recordingId of the active recording for which the position is required.
* @return the stop position, or {@link #NULL_POSITION} if still active.
* @see #getRecordingPosition(long)
*/
public long getStopPosition(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.getStopPosition(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send get stop position request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Get the stop or active recorded position of a recording.
*
* @param recordingId of the recording that the stop of active recording position is being requested for.
* @return the length of the recording.
* @since 1.44.0
*/
public long getMaxRecordedPosition(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.getMaxRecordedPosition(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send get recorded length request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Get the id of the Archive.
*
* @return the id of the Archive.
* @since 1.44.0
*/
public long archiveId()
{
return archiveId;
}
/**
* Find the last recording that matches the given criteria.
*
* @param minRecordingId to search back to.
* @param channelFragment for a contains match on the original channel stored with the archive descriptor.
* @param streamId of the recording to match.
* @param sessionId of the recording to match.
* @return the recordingId if found otherwise {@link Aeron#NULL_VALUE} if not found.
*/
public long findLastMatchingRecording(
final long minRecordingId, final String channelFragment, final int streamId, final int sessionId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.findLastMatchingRecording(
minRecordingId, channelFragment, streamId, sessionId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send find last matching recording request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Truncate a stopped recording to a given position that is less than the stopped position. The provided position
* must be on a fragment boundary. Truncating a recording to the start position effectively deletes the recording.
*
* @param recordingId of the stopped recording to be truncated.
* @param position to which the recording will be truncated.
* @return count of deleted segment files.
*/
public long truncateRecording(final long recordingId, final long position)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.truncateRecording(recordingId, position, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send truncate recording request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Purge a stopped recording, i.e. mark recording as {@link io.aeron.archive.codecs.RecordingState#INVALID}
* and delete the corresponding segment files. The space in the Catalog will be reclaimed upon compaction.
*
* @param recordingId of the stopped recording to be purged.
* @return count of deleted segment files.
*/
public long purgeRecording(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.purgeRecording(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send invalidate recording request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* List active recording subscriptions in the archive. These are the result of requesting one of
* {@link #startRecording(String, int, SourceLocation)} or a
* {@link #extendRecording(long, String, int, SourceLocation)}. The returned subscription id can be used for
* passing to {@link #stopRecording(long)}.
*
* @param pseudoIndex in the active list at which to begin for paging.
* @param subscriptionCount to get in a listing.
* @param channelFragment to do a contains match on the stripped channel URI. Empty string is match all.
* @param streamId to match on the subscription.
* @param applyStreamId true if the stream id should be matched.
* @param consumer for the matched subscription descriptors.
* @return the count of matched subscriptions.
*/
public int listRecordingSubscriptions(
final int pseudoIndex,
final int subscriptionCount,
final String channelFragment,
final int streamId,
final boolean applyStreamId,
final RecordingSubscriptionDescriptorConsumer consumer)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
isInCallback = true;
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.listRecordingSubscriptions(
pseudoIndex,
subscriptionCount,
channelFragment,
streamId,
applyStreamId,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send list recording subscriptions request");
}
return pollForSubscriptionDescriptors(lastCorrelationId, subscriptionCount, consumer);
}
finally
{
isInCallback = false;
lock.unlock();
}
}
/**
* Replicate a recording from a source archive to a destination which can be considered a backup for a primary
* archive. The source recording will be replayed via the provided replay channel and use the original stream id.
* If the destination recording id is {@link io.aeron.Aeron#NULL_VALUE} then a new destination recording is created,
* otherwise the provided destination recording id will be extended. The details of the source recording
* descriptor will be replicated.
*
* For a source recording that is still active the replay can merge with the live stream and then follow it
* directly and no longer require the replay from the source. This would require a multicast live destination.
*
* Errors will be reported asynchronously and can be checked for with {@link AeronArchive#pollForErrorResponse()}
* or {@link AeronArchive#checkForErrorResponse()}.
*
* @param srcRecordingId recording id which must exist in the source archive.
* @param dstRecordingId recording to extend in the destination, otherwise {@link io.aeron.Aeron#NULL_VALUE}.
* @param srcControlStreamId remote control stream id for the source archive to instruct the replay on.
* @param srcControlChannel remote control channel for the source archive to instruct the replay on.
* @param liveDestination destination for the live stream if merge is required. Empty or null for no merge.
* @return return the replication session id which can be passed later to {@link #stopReplication(long)}.
*/
public long replicate(
final long srcRecordingId,
final long dstRecordingId,
final int srcControlStreamId,
final String srcControlChannel,
final String liveDestination)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.replicate(
srcRecordingId,
dstRecordingId,
srcControlStreamId,
srcControlChannel,
liveDestination,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send replicate request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Replicate a recording from a source archive to a destination which can be considered a backup for a primary
* archive. The source recording will be replayed via the provided replay channel and use the original stream id.
* If the destination recording id is {@link io.aeron.Aeron#NULL_VALUE} then a new destination recording is created,
* otherwise the provided destination recording id will be extended. The details of the source recording
* descriptor will be replicated.
*
* For a source recording that is still active the replay can merge with the live stream and then follow it
* directly and no longer require the replay from the source. This would require a multicast live destination.
*
* Errors will be reported asynchronously and can be checked for with {@link AeronArchive#pollForErrorResponse()}
* or {@link AeronArchive#checkForErrorResponse()}.
*
* Stop recording this stream when the position of the destination reaches the specified stop position.
*
* @param srcRecordingId recording id which must exist in the source archive.
* @param dstRecordingId recording to extend in the destination, otherwise {@link io.aeron.Aeron#NULL_VALUE}.
* @param stopPosition position to stop the replication. {@link AeronArchive#NULL_POSITION} to stop at end
* of current recording.
* @param srcControlStreamId remote control stream id for the source archive to instruct the replay on.
* @param srcControlChannel remote control channel for the source archive to instruct the replay on.
* @param liveDestination destination for the live stream if merge is required. Empty or null for no merge.
* @param replicationChannel channel over which the replication will occur. Empty or null for default channel.
* @return return the replication session id which can be passed later to {@link #stopReplication(long)}.
*/
public long replicate(
final long srcRecordingId,
final long dstRecordingId,
final long stopPosition,
final int srcControlStreamId,
final String srcControlChannel,
final String liveDestination,
final String replicationChannel)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.replicate(
srcRecordingId,
dstRecordingId,
stopPosition,
srcControlStreamId,
srcControlChannel,
liveDestination,
replicationChannel,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send replicate request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Replicate a recording from a source archive to a destination which can be considered a backup for a primary
* archive. The source recording will be replayed via the provided replay channel and use the original stream id.
* If the destination recording id is {@link io.aeron.Aeron#NULL_VALUE} then a new destination recording is created,
* otherwise the provided destination recording id will be extended. The details of the source recording
* descriptor will be replicated. The subscription used in the archive will be tagged with the provided tags.
*
* For a source recording that is still active the replay can merge with the live stream and then follow it
* directly and no longer require the replay from the source. This would require a multicast live destination.
*
* Errors will be reported asynchronously and can be checked for with {@link AeronArchive#pollForErrorResponse()}
* or {@link AeronArchive#checkForErrorResponse()}.
*
* @param srcRecordingId recording id which must exist in the source archive.
* @param dstRecordingId recording to extend in the destination, otherwise {@link io.aeron.Aeron#NULL_VALUE}.
* @param channelTagId used to tag the replication subscription.
* @param subscriptionTagId used to tag the replication subscription.
* @param srcControlStreamId remote control stream id for the source archive to instruct the replay on.
* @param srcControlChannel remote control channel for the source archive to instruct the replay on.
* @param liveDestination destination for the live stream if merge is required. Empty or null for no merge.
* @return return the replication session id which can be passed later to {@link #stopReplication(long)}.
*/
public long taggedReplicate(
final long srcRecordingId,
final long dstRecordingId,
final long channelTagId,
final long subscriptionTagId,
final int srcControlStreamId,
final String srcControlChannel,
final String liveDestination)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.taggedReplicate(
srcRecordingId,
dstRecordingId,
channelTagId,
subscriptionTagId,
srcControlStreamId,
srcControlChannel,
liveDestination,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send tagged replicate request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Replicate a recording from a source archive to a destination which can be considered a backup for a primary
* archive. The source recording will be replayed via the provided replay channel and use the original stream id.
* If the destination recording id is {@link io.aeron.Aeron#NULL_VALUE} then a new destination recording is created,
* otherwise the provided destination recording id will be extended. The details of the source recording
* descriptor will be replicated. The subscription used in the archive will be tagged with the provided tags.
*
* For a source recording that is still active the replay can merge with the live stream and then follow it
* directly and no longer require the replay from the source. This would require a multicast live destination.
*
* Errors will be reported asynchronously and can be checked for with {@link AeronArchive#pollForErrorResponse()}
* or {@link AeronArchive#checkForErrorResponse()}.
*
* @param srcRecordingId recording id which must exist in the source archive.
* @param dstRecordingId recording to extend in the destination, otherwise {@link io.aeron.Aeron#NULL_VALUE}.
* @param stopPosition position to stop the replication. {@link AeronArchive#NULL_POSITION} to stop at end
* of current recording.
* @param channelTagId used to tag the replication subscription.
* @param subscriptionTagId used to tag the replication subscription.
* @param srcControlStreamId remote control stream id for the source archive to instruct the replay on.
* @param srcControlChannel remote control channel for the source archive to instruct the replay on.
* @param liveDestination destination for the live stream if merge is required. Empty or null for no merge.
* @param replicationChannel channel over which the replication will occur. Empty or null for default channel.
* @return return the replication session id which can be passed later to {@link #stopReplication(long)}.
*/
public long taggedReplicate(
final long srcRecordingId,
final long dstRecordingId,
final long stopPosition,
final long channelTagId,
final long subscriptionTagId,
final int srcControlStreamId,
final String srcControlChannel,
final String liveDestination,
final String replicationChannel)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.taggedReplicate(
srcRecordingId,
dstRecordingId,
stopPosition,
channelTagId,
subscriptionTagId,
srcControlStreamId,
srcControlChannel,
liveDestination,
replicationChannel,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send tagged replicate request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Replicate a recording from a source archive to a destination which can be considered a backup for a primary
* archive. The behaviour of the replication is controlled through the {@link ReplicationParams}.
*
* For a source recording that is still active the replay can merge with the live stream and then follow it
* directly and no longer require the replay from the source. This would require a multicast live destination.
*
* Errors will be reported asynchronously and can be checked for with {@link AeronArchive#pollForErrorResponse()}
* or {@link AeronArchive#checkForErrorResponse()}.
*
* The ReplicationParams is free to be reused when this call completes.
*
* @param srcRecordingId recording id which must exist in the source archive.
* @param srcControlStreamId remote control stream id for the source archive to instruct the replay on.
* @param srcControlChannel remote control channel for the source archive to instruct the replay on.
* @param replicationParams Optional parameters to control the behaviour of the replication.
* @return return the replication session id which can be passed later to {@link #stopReplication(long)}.
*/
public long replicate(
final long srcRecordingId,
final int srcControlStreamId,
final String srcControlChannel,
final ReplicationParams replicationParams)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.replicate(
srcRecordingId,
srcControlStreamId,
srcControlChannel,
replicationParams,
lastCorrelationId,
controlSessionId))
{
throw new ArchiveException("failed to send replicate request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Stop a replication session by id returned from {@link #replicate(long, long, int, String, String)}.
*
* @param replicationId to stop replication for.
* @see #replicate(long, long, int, String, String)
*/
public void stopReplication(final long replicationId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopReplication(replicationId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop replication request");
}
pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Attempt to stop a replication session by id returned from {@link #replicate(long, long, int, String, String)}.
*
* @param replicationId to stop replication for.
* @return {@code true} if the replication was stopped, false if the replication is not active.
* @see #replicate(long, long, int, String, String)
*/
public boolean tryStopReplication(final long replicationId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.stopReplication(replicationId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send stop replication request");
}
return pollForResponseAllowingError(lastCorrelationId, ArchiveException.UNKNOWN_REPLICATION);
}
finally
{
lock.unlock();
}
}
/**
* Detach segments from the beginning of a recording up to the provided new start position.
*
* The new start position must be first byte position of a segment after the existing start position.
*
* It is not possible to detach segments which are active for recording or being replayed.
*
* @param recordingId to which the operation applies.
* @param newStartPosition for the recording after the segments are detached.
* @see #segmentFileBasePosition(long, long, int, int)
*/
public void detachSegments(final long recordingId, final long newStartPosition)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.detachSegments(recordingId, newStartPosition, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send detach segments request");
}
pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Delete segments which have been previously detached from a recording.
*
* @param recordingId to which the operation applies.
* @return count of deleted segment files.
* @see #detachSegments(long, long)
*/
public long deleteDetachedSegments(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.deleteDetachedSegments(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send delete detached segments request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Purge (detach and delete) segments from the beginning of a recording up to the provided new start position.
*
* The new start position must be first byte position of a segment after the existing start position.
*
* It is not possible to detach segments which are active for recording or being replayed.
*
* @param recordingId to which the operation applies.
* @param newStartPosition for the recording after the segments are detached.
* @return count of deleted segment files.
* @see #detachSegments(long, long)
* @see #deleteDetachedSegments(long)
* @see #segmentFileBasePosition(long, long, int, int)
*/
public long purgeSegments(final long recordingId, final long newStartPosition)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.purgeSegments(recordingId, newStartPosition, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send purge segments request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Attach segments to the beginning of a recording to restore history that was previously detached.
*
* Segment files must match the existing recording and join exactly to the start position of the recording
* they are being attached to.
*
* @param recordingId to which the operation applies.
* @return count of attached segment files.
* @see #detachSegments(long, long)
*/
public long attachSegments(final long recordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.attachSegments(recordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send attach segments request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
/**
* Migrate segments from a source recording and attach them to the beginning or end of a destination recording.
*
* The source recording must match the destination recording for segment length, term length, mtu length,
* stream id. The source recording must join to the destination recording on a segment boundary and without gaps,
* i.e., the stop position and term id of one must match the start position and term id of the other.
*
* The source recording must be stopped. The destination recording must be stopped if migrating segments
* to the end of the destination recording.
*
* The source recording will be effectively truncated back to its start position after the migration.
*
* @param srcRecordingId source recording from which the segments will be migrated.
* @param dstRecordingId destination recording to which the segments will be attached.
* @return count of attached segment files.
*/
public long migrateSegments(final long srcRecordingId, final long dstRecordingId)
{
lock.lock();
try
{
ensureOpen();
ensureNotReentrant();
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.migrateSegments(srcRecordingId, dstRecordingId, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send migrate segments request");
}
return pollForResponse(lastCorrelationId);
}
finally
{
lock.unlock();
}
}
private void checkDeadline(final long deadlineNs, final String errorMessage, final long correlationId)
{
if (deadlineNs - nanoClock.nanoTime() < 0)
{
throw new TimeoutException(
errorMessage + " - correlationId=" + correlationId + " messageTimeout=" + messageTimeoutNs + "ns");
}
if (Thread.currentThread().isInterrupted())
{
throw new AeronException("unexpected interrupt");
}
}
private void pollNextResponse(final long correlationId, final long deadlineNs, final ControlResponsePoller poller)
{
idleStrategy.reset();
while (true)
{
final int fragments = poller.poll();
if (poller.isPollComplete())
{
if (poller.templateId() == RecordingSignalEventDecoder.TEMPLATE_ID &&
poller.controlSessionId() == controlSessionId)
{
dispatchRecordingSignal(poller);
continue;
}
break;
}
if (fragments > 0)
{
continue;
}
if (!poller.subscription().isConnected())
{
throw new ArchiveException("response channel from archive is not connected");
}
checkDeadline(deadlineNs, "awaiting response", correlationId);
idleStrategy.idle();
invokeInvokers();
}
}
private long pollForResponse(final long correlationId)
{
final long deadlineNs = nanoClock.nanoTime() + messageTimeoutNs;
final ControlResponsePoller poller = controlResponsePoller;
while (true)
{
pollNextResponse(correlationId, deadlineNs, poller);
if (poller.controlSessionId() != controlSessionId)
{
invokeInvokers();
continue;
}
final ControlResponseCode code = poller.code();
if (ControlResponseCode.ERROR == code)
{
final ArchiveException ex = new ArchiveException(
"response for correlationId=" + correlationId + ", error: " + poller.errorMessage(),
(int)poller.relevantId(),
poller.correlationId());
if (poller.correlationId() == correlationId)
{
throw ex;
}
else if (context.errorHandler() != null)
{
context.errorHandler().onError(ex);
}
}
else if (poller.correlationId() == correlationId)
{
if (ControlResponseCode.OK != code)
{
throw new ArchiveException("unexpected response code: " + code);
}
return poller.relevantId();
}
}
}
private boolean pollForResponseAllowingError(final long correlationId, final int allowedErrorCode)
{
final long deadlineNs = nanoClock.nanoTime() + messageTimeoutNs;
final ControlResponsePoller poller = controlResponsePoller;
while (true)
{
pollNextResponse(correlationId, deadlineNs, poller);
if (poller.controlSessionId() != controlSessionId)
{
invokeInvokers();
continue;
}
final ControlResponseCode code = poller.code();
if (ControlResponseCode.ERROR == code)
{
final long relevantId = poller.relevantId();
if (poller.correlationId() == correlationId)
{
if (relevantId == allowedErrorCode)
{
return false;
}
throw new ArchiveException(
"response for correlationId=" + correlationId + ", error: " + poller.errorMessage(),
(int)relevantId,
poller.correlationId());
}
else if (context.errorHandler() != null)
{
context.errorHandler().onError(new ArchiveException(
"response for correlationId=" + correlationId + ", error: " + poller.errorMessage(),
(int)relevantId,
poller.correlationId()));
}
}
else if (poller.correlationId() == correlationId)
{
if (ControlResponseCode.OK != code)
{
throw new ArchiveException("unexpected response code: " + code);
}
return true;
}
}
}
private int pollForDescriptors(
final long correlationId, final int count, final RecordingDescriptorConsumer consumer)
{
int existingRemainCount = count;
long deadlineNs = nanoClock.nanoTime() + messageTimeoutNs;
final RecordingDescriptorPoller poller = recordingDescriptorPoller();
poller.reset(correlationId, count, consumer);
idleStrategy.reset();
while (true)
{
final int fragments = poller.poll();
final int remainingRecordCount = poller.remainingRecordCount();
if (poller.isDispatchComplete())
{
return count - remainingRecordCount;
}
if (remainingRecordCount != existingRemainCount)
{
existingRemainCount = remainingRecordCount;
deadlineNs = nanoClock.nanoTime() + messageTimeoutNs;
}
invokeInvokers();
if (fragments > 0)
{
continue;
}
if (!poller.subscription().isConnected())
{
throw new ArchiveException("response channel from archive is not connected");
}
checkDeadline(deadlineNs, "awaiting recording descriptors", correlationId);
idleStrategy.idle();
}
}
private int pollForSubscriptionDescriptors(
final long correlationId, final int count, final RecordingSubscriptionDescriptorConsumer consumer)
{
int existingRemainCount = count;
long deadlineNs = nanoClock.nanoTime() + messageTimeoutNs;
final RecordingSubscriptionDescriptorPoller poller = recordingSubscriptionDescriptorPoller();
poller.reset(correlationId, count, consumer);
idleStrategy.reset();
while (true)
{
final int fragments = poller.poll();
final int remainingSubscriptionCount = poller.remainingSubscriptionCount();
if (poller.isDispatchComplete())
{
return count - remainingSubscriptionCount;
}
if (remainingSubscriptionCount != existingRemainCount)
{
existingRemainCount = remainingSubscriptionCount;
deadlineNs = nanoClock.nanoTime() + messageTimeoutNs;
}
invokeInvokers();
if (fragments > 0)
{
continue;
}
if (!poller.subscription().isConnected())
{
throw new ArchiveException("response channel from archive is not connected");
}
checkDeadline(deadlineNs, "awaiting subscription descriptors", correlationId);
idleStrategy.idle();
}
}
private void dispatchRecordingSignal(final ControlResponsePoller poller)
{
context.recordingSignalConsumer().onSignal(
poller.controlSessionId(),
poller.correlationId(),
poller.recordingId(),
poller.subscriptionId(),
poller.position(),
poller.recordingSignal());
}
private void invokeInvokers()
{
if (null != aeronClientInvoker)
{
aeronClientInvoker.invoke();
}
if (null != agentInvoker)
{
agentInvoker.invoke();
}
}
private void ensureOpen()
{
if (isClosed)
{
throw new ArchiveException("client is closed");
}
}
private void ensureNotReentrant()
{
if (isInCallback)
{
throw new AeronException("reentrant calls not permitted during callbacks");
}
}
/**
* Common configuration properties for communicating with an Aeron archive.
*/
@Config(existsInC = false)
public static final class Configuration
{
/**
* Major version of the network protocol from client to archive. If these don't match then client and archive
* are not compatible.
*/
public static final int PROTOCOL_MAJOR_VERSION = 1;
/**
* Minor version of the network protocol from client to archive. If these don't match then some features may
* not be available.
*/
public static final int PROTOCOL_MINOR_VERSION = 11;
/**
* Patch version of the network protocol from client to archive. If these don't match then bug fixes may not
* have been applied.
*/
public static final int PROTOCOL_PATCH_VERSION = 0;
/**
* Combined semantic version for the archive control protocol.
*
* @see SemanticVersion
*/
public static final int PROTOCOL_SEMANTIC_VERSION = SemanticVersion.compose(
PROTOCOL_MAJOR_VERSION, PROTOCOL_MINOR_VERSION, PROTOCOL_PATCH_VERSION);
/**
* Timeout in nanoseconds when waiting on a message to be sent or received.
*/
@Config
public static final String MESSAGE_TIMEOUT_PROP_NAME = "aeron.archive.message.timeout";
/**
* Timeout when waiting on a message to be sent or received.
*/
@Config(defaultType = DefaultType.LONG, defaultLong = 10L * 1000 * 1000 * 1000)
public static final long MESSAGE_TIMEOUT_DEFAULT_NS = TimeUnit.SECONDS.toNanos(10);
/**
* Channel for sending control messages to an archive.
*/
@Config(defaultType = DefaultType.STRING, defaultString = "")
public static final String CONTROL_CHANNEL_PROP_NAME = "aeron.archive.control.channel";
/**
* Stream id within a channel for sending control messages to an archive.
*/
@Config
public static final String CONTROL_STREAM_ID_PROP_NAME = "aeron.archive.control.stream.id";
/**
* Stream id within a channel for sending control messages to an archive.
*/
@Config
public static final int CONTROL_STREAM_ID_DEFAULT = 10;
/**
* Channel for sending control messages to a driver local archive.
*/
@Config(hasContext = false)
public static final String LOCAL_CONTROL_CHANNEL_PROP_NAME = "aeron.archive.local.control.channel";
/**
* Channel for sending control messages to a driver local archive. Default to IPC.
*/
@Config
public static final String LOCAL_CONTROL_CHANNEL_DEFAULT = CommonContext.IPC_CHANNEL;
/**
* Stream id within a channel for sending control messages to a driver local archive.
*/
@Config(hasContext = false)
public static final String LOCAL_CONTROL_STREAM_ID_PROP_NAME = "aeron.archive.local.control.stream.id";
/**
* Stream id within a channel for sending control messages to a driver local archive.
*/
@Config
public static final int LOCAL_CONTROL_STREAM_ID_DEFAULT = CONTROL_STREAM_ID_DEFAULT;
/**
* Channel for receiving control response messages from an archive.
*
*
* Channel's endpoint can be specified explicitly (i.e. by providing address and port pair) or
* by using zero as a port number. Here is an example of valid response channels:
*
* - {@code aeron:udp?endpoint=localhost:8020} - listen on port {@code 8020} on localhost.
* - {@code aeron:udp?endpoint=192.168.10.10:8020} - listen on port {@code 8020} on
* {@code 192.168.10.10}.
* - {@code aeron:udp?endpoint=localhost:0} - in this case the port is unspecified and the OS
* will assign a free port from the
* ephemeral port range.
*
*/
@Config(defaultType = DefaultType.STRING, defaultString = "")
public static final String CONTROL_RESPONSE_CHANNEL_PROP_NAME = "aeron.archive.control.response.channel";
/**
* Stream id within a channel for receiving control messages from an archive.
*/
@Config
public static final String CONTROL_RESPONSE_STREAM_ID_PROP_NAME = "aeron.archive.control.response.stream.id";
/**
* Stream id within a channel for receiving control messages from an archive.
*/
@Config
public static final int CONTROL_RESPONSE_STREAM_ID_DEFAULT = 20;
/**
* Channel for receiving progress events of recordings from an archive.
*/
@Config
public static final String RECORDING_EVENTS_CHANNEL_PROP_NAME = "aeron.archive.recording.events.channel";
/**
* Channel for receiving progress events of recordings from an archive.
* For production, it is recommended that multicast or dynamic multi-destination-cast (MDC) is used to allow
* for dynamic subscribers, an endpoint can be added to the subscription side for controlling port usage.
*/
@Config
public static final String RECORDING_EVENTS_CHANNEL_DEFAULT =
"aeron:udp?control-mode=dynamic|control=localhost:8030";
/**
* Stream id within a channel for receiving progress of recordings from an archive.
*/
@Config
public static final String RECORDING_EVENTS_STREAM_ID_PROP_NAME = "aeron.archive.recording.events.stream.id";
/**
* Stream id within a channel for receiving progress of recordings from an archive.
*/
@Config
public static final int RECORDING_EVENTS_STREAM_ID_DEFAULT = 30;
/**
* Is channel enabled for recording progress events of recordings from an archive.
*/
@Config
public static final String RECORDING_EVENTS_ENABLED_PROP_NAME = "aeron.archive.recording.events.enabled";
/**
* Channel enabled for recording progress events of recordings from an archive which defaults to false.
*/
@Config
public static final boolean RECORDING_EVENTS_ENABLED_DEFAULT = false;
/**
* Sparse term buffer indicator for control streams.
*/
@Config
public static final String CONTROL_TERM_BUFFER_SPARSE_PROP_NAME = "aeron.archive.control.term.buffer.sparse";
/**
* Overrides {@link io.aeron.driver.Configuration#TERM_BUFFER_SPARSE_FILE_PROP_NAME} for if term buffer files
* are sparse on the control channel.
*/
@Config
public static final boolean CONTROL_TERM_BUFFER_SPARSE_DEFAULT = true;
/**
* Term length for control streams.
*/
@Config
public static final String CONTROL_TERM_BUFFER_LENGTH_PROP_NAME = "aeron.archive.control.term.buffer.length";
/**
* Low term length for control channel reflects expected low bandwidth usage.
*/
@Config
public static final int CONTROL_TERM_BUFFER_LENGTH_DEFAULT = 64 * 1024;
/**
* MTU length for control streams.
*/
@Config
public static final String CONTROL_MTU_LENGTH_PROP_NAME = "aeron.archive.control.mtu.length";
/**
* MTU to reflect default for the control streams.
*/
@Config(defaultType = DefaultType.INT, defaultValueString = "io.aeron.driver.Configuration.mtuLength()")
public static final int CONTROL_MTU_LENGTH_DEFAULT = io.aeron.driver.Configuration.mtuLength();
/**
* Default no operation {@link RecordingSignalConsumer} to be used when not set explicitly.
*/
public static final RecordingSignalConsumer NO_OP_RECORDING_SIGNAL_CONSUMER =
(controlSessionId, correlationId, recordingId, subscriptionId, position, signal) -> {};
/**
* The timeout in nanoseconds to wait for a message.
*
* @return timeout in nanoseconds to wait for a message.
* @see #MESSAGE_TIMEOUT_PROP_NAME
*/
public static long messageTimeoutNs()
{
return getDurationInNanos(MESSAGE_TIMEOUT_PROP_NAME, MESSAGE_TIMEOUT_DEFAULT_NS);
}
/**
* Should term buffer files be sparse for control request and response streams.
*
* @return {@code true} if term buffer files should be sparse for control request and response streams.
* @see #CONTROL_TERM_BUFFER_SPARSE_PROP_NAME
*/
public static boolean controlTermBufferSparse()
{
final String propValue = System.getProperty(
CONTROL_TERM_BUFFER_SPARSE_PROP_NAME, Boolean.toString(CONTROL_TERM_BUFFER_SPARSE_DEFAULT));
return "true".equals(propValue);
}
/**
* Term buffer length to be used for control request and response streams.
*
* @return term buffer length to be used for control request and response streams.
* @see #CONTROL_TERM_BUFFER_LENGTH_PROP_NAME
*/
public static int controlTermBufferLength()
{
return getSizeAsInt(CONTROL_TERM_BUFFER_LENGTH_PROP_NAME, CONTROL_TERM_BUFFER_LENGTH_DEFAULT);
}
/**
* MTU length to be used for control request and response streams.
*
* @return MTU length to be used for control request and response streams.
* @see #CONTROL_MTU_LENGTH_PROP_NAME
*/
public static int controlMtuLength()
{
return getSizeAsInt(CONTROL_MTU_LENGTH_PROP_NAME, CONTROL_MTU_LENGTH_DEFAULT);
}
/**
* The value of system property {@link #CONTROL_CHANNEL_PROP_NAME} if set, null otherwise
*
* @return system property {@link #CONTROL_CHANNEL_PROP_NAME} if set.
*/
public static String controlChannel()
{
return System.getProperty(CONTROL_CHANNEL_PROP_NAME);
}
/**
* The value {@link #CONTROL_STREAM_ID_DEFAULT} or system property
* {@link #CONTROL_STREAM_ID_PROP_NAME} if set.
*
* @return {@link #CONTROL_STREAM_ID_DEFAULT} or system property
* {@link #CONTROL_STREAM_ID_PROP_NAME} if set.
*/
public static int controlStreamId()
{
return Integer.getInteger(CONTROL_STREAM_ID_PROP_NAME, CONTROL_STREAM_ID_DEFAULT);
}
/**
* The value {@link #LOCAL_CONTROL_CHANNEL_DEFAULT} or system property
* {@link #LOCAL_CONTROL_CHANNEL_PROP_NAME} if set.
*
* @return {@link #LOCAL_CONTROL_CHANNEL_DEFAULT} or system property
* {@link #LOCAL_CONTROL_CHANNEL_PROP_NAME} if set.
*/
public static String localControlChannel()
{
return System.getProperty(LOCAL_CONTROL_CHANNEL_PROP_NAME, LOCAL_CONTROL_CHANNEL_DEFAULT);
}
/**
* The value {@link #LOCAL_CONTROL_STREAM_ID_DEFAULT} or system property
* {@link #LOCAL_CONTROL_STREAM_ID_PROP_NAME} if set.
*
* @return {@link #LOCAL_CONTROL_STREAM_ID_DEFAULT} or system property
* {@link #LOCAL_CONTROL_STREAM_ID_PROP_NAME} if set.
*/
public static int localControlStreamId()
{
return Integer.getInteger(LOCAL_CONTROL_STREAM_ID_PROP_NAME, LOCAL_CONTROL_STREAM_ID_DEFAULT);
}
/**
* The value of system property {@link #CONTROL_RESPONSE_CHANNEL_PROP_NAME} if set, null otherwise.
*
* @return of system property {@link #CONTROL_RESPONSE_CHANNEL_PROP_NAME} if set.
*/
public static String controlResponseChannel()
{
return System.getProperty(CONTROL_RESPONSE_CHANNEL_PROP_NAME);
}
/**
* The value {@link #CONTROL_RESPONSE_STREAM_ID_DEFAULT} or system property
* {@link #CONTROL_RESPONSE_STREAM_ID_PROP_NAME} if set.
*
* @return {@link #CONTROL_RESPONSE_STREAM_ID_DEFAULT} or system property
* {@link #CONTROL_RESPONSE_STREAM_ID_PROP_NAME} if set.
*/
public static int controlResponseStreamId()
{
return Integer.getInteger(CONTROL_RESPONSE_STREAM_ID_PROP_NAME, CONTROL_RESPONSE_STREAM_ID_DEFAULT);
}
/**
* The value of system property {@link #RECORDING_EVENTS_CHANNEL_PROP_NAME} if set, null otherwise.
*
* @return system property {@link #RECORDING_EVENTS_CHANNEL_PROP_NAME} if set.
*/
public static String recordingEventsChannel()
{
return System.getProperty(RECORDING_EVENTS_CHANNEL_PROP_NAME);
}
/**
* The value {@link #RECORDING_EVENTS_STREAM_ID_DEFAULT} or system property
* {@link #RECORDING_EVENTS_STREAM_ID_PROP_NAME} if set.
*
* @return {@link #RECORDING_EVENTS_STREAM_ID_DEFAULT} or system property
* {@link #RECORDING_EVENTS_STREAM_ID_PROP_NAME} if set.
*/
public static int recordingEventsStreamId()
{
return Integer.getInteger(RECORDING_EVENTS_STREAM_ID_PROP_NAME, RECORDING_EVENTS_STREAM_ID_DEFAULT);
}
/**
* Should the recording events stream be enabled.
*
* @return {@code true} if the recording events stream be enabled.
* @see #RECORDING_EVENTS_ENABLED_PROP_NAME
*/
public static boolean recordingEventsEnabled()
{
final String propValue = System.getProperty(
RECORDING_EVENTS_ENABLED_PROP_NAME, Boolean.toString(RECORDING_EVENTS_ENABLED_DEFAULT));
return "true".equals(propValue);
}
}
/**
* Specialised configuration options for communicating with an Aeron Archive.
*
* The context will be owned by {@link AeronArchive} after a successful
* {@link AeronArchive#connect(Context)} and closed via {@link AeronArchive#close()}.
*/
public static final class Context implements Cloneable
{
/**
* Using an integer because there is no support for boolean. 1 is concluded, 0 is not concluded.
*/
private static final AtomicIntegerFieldUpdater IS_CONCLUDED_UPDATER = newUpdater(
Context.class, "isConcluded");
private volatile int isConcluded;
private long messageTimeoutNs = Configuration.messageTimeoutNs();
private String recordingEventsChannel = AeronArchive.Configuration.recordingEventsChannel();
private int recordingEventsStreamId = AeronArchive.Configuration.recordingEventsStreamId();
private String controlRequestChannel = Configuration.controlChannel();
private int controlRequestStreamId = Configuration.controlStreamId();
private String controlResponseChannel = Configuration.controlResponseChannel();
private int controlResponseStreamId = Configuration.controlResponseStreamId();
private boolean controlTermBufferSparse = Configuration.controlTermBufferSparse();
private int controlTermBufferLength = Configuration.controlTermBufferLength();
private int controlMtuLength = Configuration.controlMtuLength();
private IdleStrategy idleStrategy;
private Lock lock;
private String aeronDirectoryName = CommonContext.getAeronDirectoryName();
private Aeron aeron;
private ErrorHandler errorHandler;
private CredentialsSupplier credentialsSupplier;
private RecordingSignalConsumer recordingSignalConsumer = Configuration.NO_OP_RECORDING_SIGNAL_CONSUMER;
private AgentInvoker agentInvoker;
private boolean ownsAeronClient = false;
/**
* Perform a shallow copy of the object.
*
* @return a shallow copy of the object.
*/
public Context clone()
{
try
{
return (Context)super.clone();
}
catch (final CloneNotSupportedException ex)
{
throw new RuntimeException(ex);
}
}
/**
* Conclude configuration by setting up defaults when specifics are not provided.
*/
public void conclude()
{
if (0 != IS_CONCLUDED_UPDATER.getAndSet(this, 1))
{
throw new ConcurrentConcludeException();
}
if (null == controlRequestChannel)
{
throw new ConfigurationException("AeronArchive.Context.controlRequestChannel must be set");
}
if (null == controlResponseChannel)
{
throw new ConfigurationException("AeronArchive.Context.controlResponseChannel must be set");
}
if (null == aeron)
{
aeron = Aeron.connect(
new Aeron.Context()
.aeronDirectoryName(aeronDirectoryName)
.errorHandler(errorHandler));
ownsAeronClient = true;
}
if (null == idleStrategy)
{
idleStrategy = new BackoffIdleStrategy(
IDLE_MAX_SPINS, IDLE_MAX_YIELDS, IDLE_MIN_PARK_NS, IDLE_MAX_PARK_NS);
}
if (null == credentialsSupplier)
{
credentialsSupplier = new NullCredentialsSupplier();
}
if (null == lock)
{
lock = new ReentrantLock();
}
controlRequestChannel = applyDefaultParams(controlRequestChannel);
controlResponseChannel = applyDefaultParams(controlResponseChannel);
}
/**
* Has the context had the {@link #conclude()} method called.
*
* @return true of the {@link #conclude()} method has been called.
*/
public boolean isConcluded()
{
return 1 == isConcluded;
}
/**
* Set the message timeout in nanoseconds to wait for sending or receiving a message.
*
* @param messageTimeoutNs to wait for sending or receiving a message.
* @return this for a fluent API.
* @see Configuration#MESSAGE_TIMEOUT_PROP_NAME
*/
public Context messageTimeoutNs(final long messageTimeoutNs)
{
this.messageTimeoutNs = messageTimeoutNs;
return this;
}
/**
* The message timeout in nanoseconds to wait for sending or receiving a message.
*
* @return the message timeout in nanoseconds to wait for sending or receiving a message.
* @see Configuration#MESSAGE_TIMEOUT_PROP_NAME
*/
@Config
public long messageTimeoutNs()
{
return messageTimeoutNs;
}
/**
* Get the channel URI on which the recording events publication will publish.
*
* @return the channel URI on which the recording events publication will publish.
*/
@Config
public String recordingEventsChannel()
{
return recordingEventsChannel;
}
/**
* Set the channel URI on which the recording events publication will publish.
*
* To support dynamic subscribers then this can be set to multicast or MDC (Multi-Destination-Cast) if
* multicast cannot be supported for on the available the network infrastructure.
*
* @param recordingEventsChannel channel URI on which the recording events publication will publish.
* @return this for a fluent API.
* @see io.aeron.CommonContext#MDC_CONTROL_PARAM_NAME
*/
public Context recordingEventsChannel(final String recordingEventsChannel)
{
this.recordingEventsChannel = recordingEventsChannel;
return this;
}
/**
* Get the stream id on which the recording events publication will publish.
*
* @return the stream id on which the recording events publication will publish.
*/
@Config
public int recordingEventsStreamId()
{
return recordingEventsStreamId;
}
/**
* Set the stream id on which the recording events publication will publish.
*
* @param recordingEventsStreamId stream id on which the recording events publication will publish.
* @return this for a fluent API.
*/
public Context recordingEventsStreamId(final int recordingEventsStreamId)
{
this.recordingEventsStreamId = recordingEventsStreamId;
return this;
}
/**
* Set the channel parameter for the control request channel.
*
* @param channel parameter for the control request channel.
* @return this for a fluent API.
* @see Configuration#CONTROL_CHANNEL_PROP_NAME
*/
public Context controlRequestChannel(final String channel)
{
controlRequestChannel = channel;
return this;
}
/**
* Get the channel parameter for the control request channel.
*
* @return the channel parameter for the control request channel.
* @see Configuration#CONTROL_CHANNEL_PROP_NAME
*/
@Config(id = "CONTROL_CHANNEL")
public String controlRequestChannel()
{
return controlRequestChannel;
}
/**
* Set the stream id for the control request channel.
*
* @param streamId for the control request channel.
* @return this for a fluent API
* @see Configuration#CONTROL_STREAM_ID_PROP_NAME
*/
public Context controlRequestStreamId(final int streamId)
{
controlRequestStreamId = streamId;
return this;
}
/**
* Get the stream id for the control request channel.
*
* @return the stream id for the control request channel.
* @see Configuration#CONTROL_STREAM_ID_PROP_NAME
*/
@Config(id = "CONTROL_STREAM_ID")
public int controlRequestStreamId()
{
return controlRequestStreamId;
}
/**
* Set the channel parameter for the control response channel.
*
* @param channel parameter for the control response channel.
* @return this for a fluent API.
* @see Configuration#CONTROL_RESPONSE_CHANNEL_PROP_NAME
*/
public Context controlResponseChannel(final String channel)
{
controlResponseChannel = channel;
return this;
}
/**
* Get the channel parameter for the control response channel.
*
* @return the channel parameter for the control response channel.
* @see Configuration#CONTROL_RESPONSE_CHANNEL_PROP_NAME
*/
@Config
public String controlResponseChannel()
{
return controlResponseChannel;
}
/**
* Set the stream id for the control response channel.
*
* @param streamId for the control response channel.
* @return this for a fluent API
* @see Configuration#CONTROL_RESPONSE_STREAM_ID_PROP_NAME
*/
public Context controlResponseStreamId(final int streamId)
{
controlResponseStreamId = streamId;
return this;
}
/**
* Get the stream id for the control response channel.
*
* @return the stream id for the control response channel.
* @see Configuration#CONTROL_RESPONSE_STREAM_ID_PROP_NAME
*/
@Config
public int controlResponseStreamId()
{
return controlResponseStreamId;
}
/**
* Should the control streams use sparse file term buffers.
*
* @param controlTermBufferSparse for the control stream.
* @return this for a fluent API.
* @see Configuration#CONTROL_TERM_BUFFER_SPARSE_PROP_NAME
*/
public Context controlTermBufferSparse(final boolean controlTermBufferSparse)
{
this.controlTermBufferSparse = controlTermBufferSparse;
return this;
}
/**
* Should the control streams use sparse file term buffers.
*
* @return {@code true} if the control stream should use sparse file term buffers.
* @see Configuration#CONTROL_TERM_BUFFER_SPARSE_PROP_NAME
*/
@Config
public boolean controlTermBufferSparse()
{
return controlTermBufferSparse;
}
/**
* Set the term buffer length for the control streams.
*
* @param controlTermBufferLength for the control streams.
* @return this for a fluent API.
* @see Configuration#CONTROL_TERM_BUFFER_LENGTH_PROP_NAME
*/
public Context controlTermBufferLength(final int controlTermBufferLength)
{
this.controlTermBufferLength = controlTermBufferLength;
return this;
}
/**
* Get the term buffer length for the control streams.
*
* @return the term buffer length for the control streams.
* @see Configuration#CONTROL_TERM_BUFFER_LENGTH_PROP_NAME
*/
@Config
public int controlTermBufferLength()
{
return controlTermBufferLength;
}
/**
* Set the MTU length for the control streams.
*
* @param controlMtuLength for the control streams.
* @return this for a fluent API.
* @see Configuration#CONTROL_MTU_LENGTH_PROP_NAME
*/
public Context controlMtuLength(final int controlMtuLength)
{
this.controlMtuLength = controlMtuLength;
return this;
}
/**
* Get the MTU length for the control streams.
*
* @return the MTU length for the control streams.
* @see Configuration#CONTROL_MTU_LENGTH_PROP_NAME
*/
@Config
public int controlMtuLength()
{
return controlMtuLength;
}
/**
* Set the {@link IdleStrategy} used when waiting for responses.
*
* @param idleStrategy used when waiting for responses.
* @return this for a fluent API.
*/
public Context idleStrategy(final IdleStrategy idleStrategy)
{
this.idleStrategy = idleStrategy;
return this;
}
/**
* Get the {@link IdleStrategy} used when waiting for responses.
*
* @return the {@link IdleStrategy} used when waiting for responses.
*/
public IdleStrategy idleStrategy()
{
return idleStrategy;
}
/**
* Set the top level Aeron directory used for communication between the Aeron client and Media Driver.
*
* @param aeronDirectoryName the top level Aeron directory.
* @return this for a fluent API.
*/
public Context aeronDirectoryName(final String aeronDirectoryName)
{
this.aeronDirectoryName = aeronDirectoryName;
return this;
}
/**
* Get the top level Aeron directory used for communication between the Aeron client and Media Driver.
*
* @return The top level Aeron directory.
*/
public String aeronDirectoryName()
{
return aeronDirectoryName;
}
/**
* {@link Aeron} client for communicating with the local Media Driver.
*
* This client will be closed when the {@link AeronArchive#close()} or {@link #close()} methods are called if
* {@link #ownsAeronClient()} is true.
*
* @param aeron client for communicating with the local Media Driver.
* @return this for a fluent API.
* @see Aeron#connect()
*/
public Context aeron(final Aeron aeron)
{
this.aeron = aeron;
return this;
}
/**
* {@link Aeron} client for communicating with the local Media Driver.
*
* If not provided then a default will be established during {@link #conclude()} by calling
* {@link Aeron#connect()}.
*
* @return client for communicating with the local Media Driver.
*/
public Aeron aeron()
{
return aeron;
}
/**
* Does this context own the {@link #aeron()} client and thus takes responsibility for closing it?
*
* @param ownsAeronClient does this context own the {@link #aeron()} client?
* @return this for a fluent API.
*/
public Context ownsAeronClient(final boolean ownsAeronClient)
{
this.ownsAeronClient = ownsAeronClient;
return this;
}
/**
* Does this context own the {@link #aeron()} client and thus takes responsibility for closing it?
*
* @return does this context own the {@link #aeron()} client and thus takes responsibility for closing it?
*/
public boolean ownsAeronClient()
{
return ownsAeronClient;
}
/**
* The {@link Lock} that is used to provide mutual exclusion in the {@link AeronArchive} client.
*
* If the {@link AeronArchive} is used from only a single thread then the lock can be set to
* {@link NoOpLock} to elide the lock overhead.
*
* @param lock that is used to provide mutual exclusion in the {@link AeronArchive} client.
* @return this for a fluent API.
*/
public Context lock(final Lock lock)
{
this.lock = lock;
return this;
}
/**
* Get the {@link Lock} that is used to provide mutual exclusion in the {@link AeronArchive} client.
*
* @return the {@link Lock} that is used to provide mutual exclusion in the {@link AeronArchive} client.
*/
public Lock lock()
{
return lock;
}
/**
* Handle errors returned asynchronously from the archive for a control session.
*
* @param errorHandler method to handle objects of type Throwable.
* @return this for a fluent API.
*/
public Context errorHandler(final ErrorHandler errorHandler)
{
this.errorHandler = errorHandler;
return this;
}
/**
* Get the error handler that will be called for asynchronous errors.
*
* @return the error handler that will be called for asynchronous errors.
*/
public ErrorHandler errorHandler()
{
return errorHandler;
}
/**
* Set the {@link CredentialsSupplier} to be used for authentication with the archive.
*
* @param credentialsSupplier to be used for authentication with the archive.
* @return this for fluent API.
*/
public Context credentialsSupplier(final CredentialsSupplier credentialsSupplier)
{
this.credentialsSupplier = credentialsSupplier;
return this;
}
/**
* Get the {@link CredentialsSupplier} to be used for authentication with the archive.
*
* @return the {@link CredentialsSupplier} to be used for authentication with the archive.
*/
public CredentialsSupplier credentialsSupplier()
{
return credentialsSupplier;
}
/**
* Set the {@link RecordingSignalConsumer} to will be called when polling for responses from an Archive.
*
* @param recordingSignalConsumer to called with recording signal events.
* @return this for a fluent API.
*/
public Context recordingSignalConsumer(final RecordingSignalConsumer recordingSignalConsumer)
{
this.recordingSignalConsumer = recordingSignalConsumer;
return this;
}
/**
* Set the {@link RecordingSignalConsumer} to will be called when polling for responses from an Archive.
*
* @return a recording signal consumer.
*/
public RecordingSignalConsumer recordingSignalConsumer()
{
return recordingSignalConsumer;
}
/**
* Set the {@link AgentInvoker} to be invoked in addition to any invoker used by the {@link #aeron()} instance.
*
* Useful for when running on a low thread count scenario.
*
* @param agentInvoker to be invoked while awaiting a response in the client.
* @return this for a fluent API.
*/
public Context agentInvoker(final AgentInvoker agentInvoker)
{
this.agentInvoker = agentInvoker;
return this;
}
/**
* Get the {@link AgentInvoker} to be invoked in addition to any invoker used by the {@link #aeron()} instance.
*
* @return the {@link AgentInvoker} that is used.
*/
public AgentInvoker agentInvoker()
{
return agentInvoker;
}
/**
* Close the context and free applicable resources.
*
* If {@link #ownsAeronClient()} is true then the {@link #aeron()} client will be closed.
*/
public void close()
{
if (ownsAeronClient)
{
CloseHelper.close(aeron);
}
}
/**
* {@inheritDoc}
*/
public String toString()
{
return "AeronArchive.Context" +
"\n{" +
"\n isConcluded=" + isConcluded() +
"\n ownsAeronClient=" + ownsAeronClient +
"\n aeronDirectoryName='" + aeronDirectoryName + '\'' +
"\n aeron=" + aeron +
"\n messageTimeoutNs=" + messageTimeoutNs +
"\n recordingEventsChannel='" + recordingEventsChannel + '\'' +
"\n recordingEventsStreamId=" + recordingEventsStreamId +
"\n controlRequestChannel='" + controlRequestChannel + '\'' +
"\n controlRequestStreamId=" + controlRequestStreamId +
"\n controlResponseChannel='" + controlResponseChannel + '\'' +
"\n controlResponseStreamId=" + controlResponseStreamId +
"\n controlTermBufferSparse=" + controlTermBufferSparse +
"\n controlTermBufferLength=" + controlTermBufferLength +
"\n controlMtuLength=" + controlMtuLength +
"\n idleStrategy=" + idleStrategy +
"\n lock=" + lock +
"\n errorHandler=" + errorHandler +
"\n credentialsSupplier=" + credentialsSupplier +
"\n}";
}
private String applyDefaultParams(final String channel)
{
final ChannelUri channelUri = ChannelUri.parse(channel);
if (!channelUri.containsKey(CommonContext.TERM_LENGTH_PARAM_NAME))
{
channelUri.put(CommonContext.TERM_LENGTH_PARAM_NAME, Integer.toString(controlTermBufferLength));
}
if (!channelUri.containsKey(CommonContext.MTU_LENGTH_PARAM_NAME))
{
channelUri.put(CommonContext.MTU_LENGTH_PARAM_NAME, Integer.toString(controlMtuLength));
}
if (!channelUri.containsKey(CommonContext.SPARSE_PARAM_NAME))
{
channelUri.put(CommonContext.SPARSE_PARAM_NAME, Boolean.toString(controlTermBufferSparse));
}
return channelUri.toString();
}
}
/**
* Allows for the async establishment of an archive session.
*/
public static final class AsyncConnect implements AutoCloseable
{
/**
* Represents connection state.
*/
public enum State
{
/**
* Initial state of adding a publication for control request channel.
*/
ADD_PUBLICATION(0),
/**
* Await publication being added.
*/
AWAIT_PUBLICATION_CONNECTED(1),
/**
* Sending {@code connect} request to the Archive.
*/
SEND_CONNECT_REQUEST(2),
/**
* Await response subscription connected.
*/
AWAIT_SUBSCRIPTION_CONNECTED(3),
/**
* Await connect response.
*/
AWAIT_CONNECT_RESPONSE(4),
/**
* Send {@code archive-id} request.
*/
SEND_ARCHIVE_ID_REQUEST(5),
/**
* Await response for the {@code archive-id} request.
*/
AWAIT_ARCHIVE_ID_RESPONSE(6),
/**
* Archive connection established.
*/
DONE(7),
/**
* Sending a challenge response.
*/
SEND_CHALLENGE_RESPONSE(8),
/**
* Await challenge response.
*/
AWAIT_CHALLENGE_RESPONSE(9);
final int step;
State(final int step)
{
this.step = step;
}
}
static final int PROTOCOL_VERSION_WITH_ARCHIVE_ID = SemanticVersion.compose(1, 11, 0);
private final Context ctx;
private final ControlResponsePoller controlResponsePoller;
private ArchiveProxy archiveProxy;
private final long deadlineNs;
private long publicationRegistrationId = Aeron.NULL_VALUE;
private long correlationId = Aeron.NULL_VALUE;
private long controlSessionId = Aeron.NULL_VALUE;
private byte[] encodedCredentialsFromChallenge = null;
private State state = State.ADD_PUBLICATION;
AsyncConnect(final Context ctx)
{
this.ctx = ctx;
final Aeron aeron = ctx.aeron();
controlResponsePoller = new ControlResponsePoller(
aeron.addSubscription(ctx.controlResponseChannel(), ctx.controlResponseStreamId()));
checkAndSetupResponseChannel(ctx, controlResponsePoller.subscription());
publicationRegistrationId = aeron.asyncAddExclusivePublication(
ctx.controlRequestChannel(), ctx.controlRequestStreamId());
deadlineNs = aeron.context().nanoClock().nanoTime() + ctx.messageTimeoutNs();
}
AsyncConnect(
final Context ctx, final ControlResponsePoller controlResponsePoller, final ArchiveProxy archiveProxy)
{
this.ctx = ctx;
this.controlResponsePoller = controlResponsePoller;
this.archiveProxy = archiveProxy;
deadlineNs = ctx.aeron().context().nanoClock().nanoTime() + ctx.messageTimeoutNs();
state = State.AWAIT_PUBLICATION_CONNECTED;
}
/**
* Close any allocated resources.
*/
public void close()
{
if (State.DONE != state)
{
final ErrorHandler errorHandler = ctx.errorHandler();
CloseHelper.close(errorHandler, controlResponsePoller.subscription());
if (null != archiveProxy)
{
CloseHelper.close(errorHandler, archiveProxy.publication());
}
else if (Aeron.NULL_VALUE != publicationRegistrationId)
{
ctx.aeron().asyncRemovePublication(publicationRegistrationId);
}
ctx.close();
}
}
/**
* Get the {@link AeronArchive.Context} used for this client.
*
* @return the {@link AeronArchive.Context} used for this client.
*/
public Context context()
{
return ctx;
}
/**
* Get the index of the current step.
*
* @return the index of the current step.
*/
public int step()
{
return state.step;
}
/**
* Get the current connection state.
*
* @return current state.
*/
public State state()
{
return state;
}
/**
* Poll for a complete connection.
*
* @return a new {@link AeronArchive} if successfully connected otherwise null.
*/
@SuppressWarnings("MethodLength")
public AeronArchive poll()
{
checkDeadline();
AeronArchive aeronArchive = null;
if (State.ADD_PUBLICATION == state)
{
final ExclusivePublication publication = ctx.aeron().getExclusivePublication(publicationRegistrationId);
if (null != publication)
{
publicationRegistrationId = Aeron.NULL_VALUE;
archiveProxy = new ArchiveProxy(
publication,
ctx.idleStrategy(),
ctx.aeron().context().nanoClock(),
ctx.messageTimeoutNs(),
DEFAULT_RETRY_ATTEMPTS,
ctx.credentialsSupplier());
state(State.AWAIT_PUBLICATION_CONNECTED);
}
}
if (State.AWAIT_PUBLICATION_CONNECTED == state)
{
if (!archiveProxy.publication().isConnected())
{
return null;
}
state(State.SEND_CONNECT_REQUEST);
}
if (State.SEND_CONNECT_REQUEST == state)
{
final String responseChannel = controlResponsePoller.subscription().tryResolveChannelEndpointPort();
if (null == responseChannel)
{
return null;
}
correlationId = ctx.aeron().nextCorrelationId();
if (!archiveProxy.tryConnect(responseChannel, ctx.controlResponseStreamId(), correlationId))
{
return null;
}
state(State.AWAIT_SUBSCRIPTION_CONNECTED);
}
if (State.AWAIT_SUBSCRIPTION_CONNECTED == state)
{
if (!controlResponsePoller.subscription().isConnected())
{
return null;
}
state(State.AWAIT_CONNECT_RESPONSE);
}
if (State.SEND_ARCHIVE_ID_REQUEST == state)
{
if (!archiveProxy.archiveId(correlationId, controlSessionId))
{
return null;
}
state(State.AWAIT_ARCHIVE_ID_RESPONSE);
}
if (State.SEND_CHALLENGE_RESPONSE == state)
{
if (!archiveProxy.tryChallengeResponse(
encodedCredentialsFromChallenge, correlationId, controlSessionId))
{
return null;
}
state(State.AWAIT_CHALLENGE_RESPONSE);
}
controlResponsePoller.poll();
if (controlResponsePoller.isPollComplete() && controlResponsePoller.correlationId() == correlationId)
{
controlSessionId = controlResponsePoller.controlSessionId();
if (controlResponsePoller.wasChallenged())
{
encodedCredentialsFromChallenge = ctx.credentialsSupplier().onChallenge(
controlResponsePoller.encodedChallenge());
correlationId = ctx.aeron().nextCorrelationId();
state(State.SEND_CHALLENGE_RESPONSE);
}
else
{
final ControlResponseCode code = controlResponsePoller.code();
if (ControlResponseCode.OK != code)
{
archiveProxy.closeSession(controlSessionId);
if (ControlResponseCode.ERROR == code)
{
final String errorMessage = controlResponsePoller.errorMessage();
final int errorCode = (int)controlResponsePoller.relevantId();
throw new ArchiveException(errorMessage, errorCode, correlationId);
}
throw new ArchiveException(
"unexpected response: code=" + code, correlationId, AeronException.Category.ERROR);
}
if (State.AWAIT_ARCHIVE_ID_RESPONSE == state)
{
final long archiveId = controlResponsePoller.relevantId();
aeronArchive = transitionToDone(archiveId);
}
else
{
final int archiveProtocolVersion = controlResponsePoller.version();
if (archiveProtocolVersion < PROTOCOL_VERSION_WITH_ARCHIVE_ID)
{
aeronArchive = transitionToDone(Aeron.NULL_VALUE);
}
else
{
correlationId = ctx.aeron().nextCorrelationId();
state(State.SEND_ARCHIVE_ID_REQUEST);
}
}
}
}
return aeronArchive;
}
long correlationId()
{
return correlationId;
}
long controlSessionId()
{
return controlSessionId;
}
private void state(final State newState)
{
// System.out.println(state + " -> " + newState);
state = newState;
}
private void checkDeadline()
{
if (deadlineNs - ctx.aeron().context().nanoClock().nanoTime() < 0)
{
throw new TimeoutException("Archive connect timeout: step=" + state +
(state.step < 3 ?
" publication.uri=" + ctx.controlRequestChannel() :
" subscription.uri=" + ctx.controlResponseChannel()));
}
if (Thread.currentThread().isInterrupted())
{
throw new AeronException("unexpected interrupt");
}
}
private AeronArchive transitionToDone(final long archiveId)
{
if (!archiveProxy.keepAlive(controlSessionId, Aeron.NULL_VALUE))
{
archiveProxy.closeSession(controlSessionId);
throw new ArchiveException("failed to send keep alive after archive connect");
}
final AeronArchive aeronArchive = new AeronArchive(
ctx, controlResponsePoller, archiveProxy, controlSessionId, archiveId);
state(State.DONE);
return aeronArchive;
}
}
static Exception quietClose(final Exception previousException, final AutoCloseable closeable)
{
Exception resultException = previousException;
if (null != closeable)
{
try
{
closeable.close();
}
catch (final Exception ex)
{
if (null != resultException)
{
resultException.addSuppressed(ex);
}
else
{
resultException = ex;
}
}
}
return resultException;
}
private static void checkAndSetupResponseChannel(final Context ctx, final Subscription subscription)
{
if (ChannelUri.isControlModeResponse(ctx.controlResponseChannel()))
{
final String requestChannel = new ChannelUriStringBuilder(ctx.controlRequestChannel())
.responseCorrelationId(subscription.registrationId())
.toString();
ctx.controlRequestChannel(requestChannel);
}
}
private Subscription replayViaResponseChannel(
final long recordingId,
final String replayChannel,
final int replayStreamId,
final ReplayParams replayParams)
{
lastCorrelationId = aeron.nextCorrelationId();
if (!archiveProxy.requestReplayToken(lastCorrelationId, controlSessionId, recordingId))
{
throw new ArchiveException("failed to send replay token request");
}
final long replayToken = pollForResponse(lastCorrelationId);
replayParams.replayToken(replayToken);
final Subscription replaySubscription = aeron.addSubscription(replayChannel, replayStreamId);
final ChannelUriStringBuilder uriBuilder = new ChannelUriStringBuilder(context.controlRequestChannel())
.responseCorrelationId(replaySubscription.registrationId())
.termId((Integer)null).initialTermId((Integer)null).termOffset((Integer)null)
.termLength(64 * 1024)
.spiesSimulateConnection(false);
final String channel = uriBuilder.build();
try (Publication publication = aeron.addExclusivePublication(channel, context().controlRequestStreamId()))
{
final ArchiveProxy responseArchiveProxy = new ArchiveProxy(publication);
final int pubLmtCounterId = aeron.countersReader().findByTypeIdAndRegistrationId(
AeronCounters.DRIVER_PUBLISHER_LIMIT_TYPE_ID, publication.registrationId());
final long deadlineNs = aeron.context().nanoClock().nanoTime() + context.messageTimeoutNs();
while (!publication.isConnected() || 0 == aeron.countersReader().getCounterValue(pubLmtCounterId))
{
if (deadlineNs <= aeron.context().nanoClock().nanoTime())
{
throw new ArchiveException("timed out wait for replay publication to connect");
}
idleStrategy.idle();
}
if (!responseArchiveProxy.replay(
recordingId, replayChannel, replayStreamId, replayParams, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send replay request");
}
pollForResponse(lastCorrelationId);
while (!replaySubscription.isConnected())
{
idleStrategy.idle();
}
return replaySubscription;
}
catch (final Exception ex)
{
CloseHelper.close(replaySubscription);
throw ex;
}
}
private long startReplayViaResponseChannel(
final long recordingId,
final String replayChannel,
final int replayStreamId,
final ReplayParams replayParams)
{
lastCorrelationId = aeron.nextCorrelationId();
if (Aeron.NULL_VALUE == replayParams.subscriptionRegistrationId())
{
throw new ArchiveException(
"when using startReplay with a response channel, ReplayParams::subscriptionRegistrationId must be set");
}
if (!archiveProxy.requestReplayToken(lastCorrelationId, controlSessionId, recordingId))
{
throw new ArchiveException("failed to send replay token request");
}
final long replayToken = pollForResponse(lastCorrelationId);
replayParams.replayToken(replayToken);
final ChannelUriStringBuilder uriBuilder = new ChannelUriStringBuilder(context.controlRequestChannel())
.responseCorrelationId(replayParams.subscriptionRegistrationId())
.termId((Integer)null).initialTermId((Integer)null).termOffset((Integer)null)
.termLength(64 * 1024)
.spiesSimulateConnection(false);
final String channel = uriBuilder.build();
try (Publication publication = aeron.addExclusivePublication(channel, context().controlRequestStreamId()))
{
final ArchiveProxy responseArchiveProxy = new ArchiveProxy(publication);
final long deadlineNs = aeron.context().nanoClock().nanoTime() + context.messageTimeoutNs();
while (!publication.isConnected())
{
checkDeadline(
idleStrategy,
aeron.context().nanoClock(),
deadlineNs,
"timed out waiting to establish replay connection");
}
while (0 == publication.positionLimit())
{
checkDeadline(
idleStrategy,
aeron.context().nanoClock(),
deadlineNs,
"timed out waiting for replay connection to have available publication limit");
}
if (!responseArchiveProxy.replay(
recordingId, replayChannel, replayStreamId, replayParams, lastCorrelationId, controlSessionId))
{
throw new ArchiveException("failed to send replay request");
}
pollForResponse(lastCorrelationId);
return lastCorrelationId;
}
}
private static void checkDeadline(
final IdleStrategy idleStrategy,
final NanoClock nanoClock,
final long deadlineNs,
final String msg)
{
if (deadlineNs <= nanoClock.nanoTime())
{
throw new ArchiveException(msg);
}
idleStrategy.idle();
}
}