/*
* Copyright 2014-2017 Real Logic Ltd.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.aeron.archiver;
import io.aeron.*;
import org.agrona.CloseHelper;
import org.agrona.collections.*;
import org.agrona.concurrent.*;
import java.io.File;
import java.util.ArrayList;
import java.util.function.Consumer;
class ArchiveConductor implements Agent
{
/**
* Limit on the number of items drained from the available image queue per work cycle.
*/
private static final int QUEUE_DRAIN_LIMIT = 10;
interface Session
{
void abort();
boolean isDone();
void remove(ArchiveConductor conductor);
int doWork();
}
private final Aeron aeron;
private final AgentInvoker aeronClientAgentInvoker;
private final AgentInvoker driverAgentInvoker;
private final Subscription controlSubscription;
private final ArrayList<Session> sessions = new ArrayList<>();
private final Long2ObjectHashMap<ReplaySession> replaySessionByIdMap = new Long2ObjectHashMap<>();
private final Catalog catalog;
private final OneToOneConcurrentArrayQueue<Image> availableImageQueue = new OneToOneConcurrentArrayQueue<>(512);
private final File archiveDir;
private final Consumer<Image> newImageConsumer = this::availableImageHandler;
private final AvailableImageHandler availableImageHandler = this::onAvailableImage;
private final NotificationsProxy notificationsProxy;
private final ControlSessionProxy clientProxy;
private final EpochClock epochClock;
private volatile boolean isClosed = false;
private final Recorder.Builder imageRecorderBuilder = new Recorder.Builder();
private int replaySessionId;
ArchiveConductor(final Aeron aeron, final Archiver.Context ctx)
{
this.aeron = aeron;
this.aeronClientAgentInvoker = ctx.clientContext().conductorAgentInvoker();
this.driverAgentInvoker = ctx.driverAgentInvoker();
archiveDir = ctx.archiveDir();
catalog = new Catalog(archiveDir);
imageRecorderBuilder
.recordingFileLength(ctx.segmentFileLength())
.archiveDir(ctx.archiveDir())
.epochClock(ctx.epochClock())
.forceMetadataUpdates(ctx.forceMetadataUpdates())
.forceWrites(ctx.forceWrites());
controlSubscription = aeron.addSubscription(
ctx.controlRequestChannel(),
ctx.controlRequestStreamId(),
availableImageHandler,
null);
final Publication archiverNotificationPublication = aeron.addPublication(
ctx.recordingEventsChannel(), ctx.recordingEventsStreamId());
notificationsProxy = new NotificationsProxy(ctx.idleStrategy(), archiverNotificationPublication);
clientProxy = new ControlSessionProxy(ctx.idleStrategy());
epochClock = ctx.epochClock();
}
public String roleName()
{
return "archiver";
}
public int doWork() throws Exception
{
int workDone = 0;
if (null != driverAgentInvoker)
{
workDone += driverAgentInvoker.invoke();
}
workDone += aeronClientAgentInvoker.invoke();
workDone += availableImageQueue.drain(newImageConsumer, QUEUE_DRAIN_LIMIT);
workDone += doSessionsWork();
return workDone;
}
public void onClose()
{
if (isClosed)
{
return;
}
isClosed = true;
for (final Session session : sessions)
{
session.abort();
while (!session.isDone())
{
session.doWork();
}
session.remove(this);
}
sessions.clear();
if (!replaySessionByIdMap.isEmpty())
{
// TODO: Use an error log.
System.err.println("ERROR: expected empty replaySessionByIdMap");
}
CloseHelper.close(catalog);
}
void removeReplaySession(final long sessionId)
{
replaySessionByIdMap.remove(sessionId);
}
private int doSessionsWork()
{
int workDone = 0;
final ArrayList<Session> sessions = this.sessions;
for (int lastIndex = sessions.size() - 1, i = lastIndex; i >= 0; i--)
{
final Session session = sessions.get(i);
workDone += session.doWork();
if (session.isDone())
{
session.remove(this);
ArrayListUtil.fastUnorderedRemove(sessions, i, lastIndex);
lastIndex--;
}
}
return workDone;
}
private void availableImageHandler(final Image image)
{
final Session session;
if (image.subscription() == controlSubscription)
{
session = new ControlSession(image, clientProxy, this);
}
else
{
// TODO: What happens to the state of the builder if two recordings are added before their
// TODO: init() get called? Should the setup not happen in the constructor?
session = new RecordingSession(notificationsProxy, catalog, image, imageRecorderBuilder);
}
sessions.add(session);
}
private void onAvailableImage(final Image image)
{
if (!isClosed && availableImageQueue.offer(image))
{
return;
}
// This is required since image available handler is called from the client conductor thread
// we can either bridge via a queue or protect access to the sessions list, which seems clumsy.
while (!isClosed && !availableImageQueue.offer(image))
{
Thread.yield();
}
}
void stopRecording(final long recordingId)
{
catalog.getRecordingSession(recordingId).abort();
}
public void startRecording(final String channel, final int streamId)
{
final Subscription recordingSubscription = aeron.addSubscription(
channel, streamId, availableImageHandler, null);
}
public void listRecordings(
final long correlationId,
final ExclusivePublication replyPublication,
final long fromId,
final long toId)
{
final Session listSession = new ListRecordingsSession(
correlationId, replyPublication, fromId, toId, catalog, clientProxy);
sessions.add(listSession);
}
public void stopReplay(final int sessionId)
{
final ReplaySession session = replaySessionByIdMap.get(sessionId);
if (session == null)
{
throw new IllegalStateException("Trying to abort an unknown replay session: " + sessionId);
}
session.abort();
}
void startReplay(
final long correlationId,
final ExclusivePublication reply,
final int replayStreamId,
// TODO: replace with host/port?
final String replayChannel,
final long recordingId,
final long position,
final long length)
{
final int newId = replaySessionId++;
final ReplaySession replaySession = new ReplaySession(
recordingId,
position,
length,
this,
reply,
archiveDir,
clientProxy,
newId,
correlationId,
this.epochClock,
replayChannel,
replayStreamId);
replaySessionByIdMap.put(newId, replaySession);
sessions.add(replaySession);
}
ExclusivePublication clientConnect(final String channel, final int streamId)
{
return aeron.addExclusivePublication(channel, streamId);
}
ExclusivePublication newReplayPublication(
final String replayChannel,
final int replayStreamId,
final long fromPosition,
final int mtuLength,
final int initialTermId,
final int termBufferLength)
{
final int termId = (int)(fromPosition / termBufferLength + initialTermId);
final int termOffset = (int)(fromPosition % termBufferLength);
// TODO: can cache and reuse builder
final StringBuilder builder = new StringBuilder(replayChannel.length() + 128);
builder.append(replayChannel);
if (replayChannel.contains("?"))
{
builder.append("|");
}
else
{
builder.append("?");
}
builder
.append(CommonContext.INITIAL_TERM_ID_PARAM_NAME).append('=').append(initialTermId)
.append('|')
.append(CommonContext.MTU_LENGTH_URI_PARAM_NAME).append('=').append(mtuLength)
.append('|')
.append(CommonContext.TERM_LENGTH_PARAM_NAME).append('=').append(termBufferLength)
.append('|')
.append(CommonContext.TERM_ID_PARAM_NAME).append('=').append(termId)
.append('|')
.append(CommonContext.TERM_OFFSET_PARAM_NAME).append('=').append(termOffset);
return aeron.addExclusivePublication(builder.toString(), replayStreamId);
}
}