/*
 * Copyright 2014-2019 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.archive;

import io.aeron.Aeron;
import io.aeron.CommonContext;
import io.aeron.Image;
import io.aeron.archive.client.AeronArchive;
import io.aeron.archive.client.ArchiveException;
import io.aeron.logbuffer.LogBufferDescriptor;
import org.agrona.BitUtil;
import org.agrona.CloseHelper;
import org.agrona.ErrorHandler;
import org.agrona.IoUtil;
import org.agrona.concurrent.*;
import org.agrona.concurrent.status.AtomicCounter;
import org.agrona.concurrent.status.StatusIndicator;

import java.io.File;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Objects;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import static io.aeron.driver.status.SystemCounterDescriptor.SYSTEM_COUNTER_TYPE_ID;
import static io.aeron.logbuffer.LogBufferDescriptor.TERM_MAX_LENGTH;
import static io.aeron.logbuffer.LogBufferDescriptor.TERM_MIN_LENGTH;
import static java.lang.System.getProperty;
import static org.agrona.SystemUtil.getDurationInNanos;
import static org.agrona.SystemUtil.getSizeAsInt;
import static org.agrona.SystemUtil.loadPropertiesFiles;

/**
 * The Aeron Archive which allows for the recording and replay of local and remote {@link io.aeron.Publication}s .
 */
public class Archive implements AutoCloseable
{
    private final Context ctx;
    private final AgentRunner conductorRunner;
    private final AgentInvoker conductorInvoker;

    Archive(final Context ctx)
    {
        this.ctx = ctx;
        ctx.conclude();

        final ArchiveConductor conductor = ArchiveThreadingMode.DEDICATED == ctx.threadingMode() ?
            new DedicatedModeArchiveConductor(ctx) :
            new SharedModeArchiveConductor(ctx);

        if (ArchiveThreadingMode.INVOKER == ctx.threadingMode())
        {
            conductorInvoker = new AgentInvoker(ctx.errorHandler(), ctx.errorCounter(), conductor);
            conductorRunner = null;
        }
        else
        {
            conductorInvoker = null;
            conductorRunner = new AgentRunner(ctx.idleStrategy(), ctx.errorHandler(), ctx.errorCounter(), conductor);
        }
    }

    /**
     * Launch an {@link Archive} with that communicates with an out of process {@link io.aeron.driver.MediaDriver}
     * and await a shutdown signal.
     *
     * @param args command line argument which is a list for properties files as URLs or filenames.
     */
    public static void main(final String[] args)
    {
        loadPropertiesFiles(args);

        try (Archive ignore = launch())
        {
            new ShutdownSignalBarrier().await();

            System.out.println("Shutdown Archive...");
        }
    }

    /**
     * Get the {@link Archive.Context} that is used by this {@link Archive}.
     *
     * @return the {@link Archive.Context} that is used by this {@link Archive}.
     */
    public Context context()
    {
        return ctx;
    }

    public void close()
    {
        CloseHelper.close(conductorInvoker);
        CloseHelper.close(conductorRunner);
    }

    private Archive start()
    {
        if (ArchiveThreadingMode.INVOKER == ctx.threadingMode())
        {
            conductorInvoker.start();
        }
        else
        {
            AgentRunner.startOnThread(conductorRunner, ctx.threadFactory());
        }

        return this;
    }

    /**
     * Get the {@link AgentInvoker} for the archive if it is running in {@link ArchiveThreadingMode#INVOKER}.
     *
     * @return the {@link AgentInvoker} for the archive if it is running in {@link ArchiveThreadingMode#INVOKER}
     * otherwise null.
     */
    public AgentInvoker invoker()
    {
        return conductorInvoker;
    }

    /**
     * Launch an Archive using a default configuration.
     *
     * @return a new instance of an Archive.
     */
    public static Archive launch()
    {
        return launch(new Context());
    }

    /**
     * Launch an Archive by providing a configuration context.
     *
     * @param ctx for the configuration parameters.
     * @return a new instance of an Archive.
     */
    public static Archive launch(final Context ctx)
    {
        return new Archive(ctx).start();
    }

    /**
     * Configuration for system properties and defaults.
     *

     * Details for the individual parameters can be found in the Javadoc for the {@link Context} setters.
     */
    public static class Configuration
    {
        public static final String ARCHIVE_DIR_PROP_NAME = "aeron.archive.dir";
        public static final String ARCHIVE_DIR_DEFAULT = "aeron-archive";

        public static final String SEGMENT_FILE_LENGTH_PROP_NAME = "aeron.archive.segment.file.length";
        public static final int SEGMENT_FILE_LENGTH_DEFAULT = 128 * 1024 * 1024;

        public static final String FILE_SYNC_LEVEL_PROP_NAME = "aeron.archive.file.sync.level";
        public static final int FILE_SYNC_LEVEL_DEFAULT = 0;

        public static final String CATALOG_FILE_SYNC_LEVEL_PROP_NAME = "aeron.archive.catalog.file.sync.level";
        public static final int CATALOG_FILE_SYNC_LEVEL_DEFAULT = FILE_SYNC_LEVEL_DEFAULT;

        public static final String THREADING_MODE_PROP_NAME = "aeron.archive.threading.mode";
        public static final String ARCHIVE_IDLE_STRATEGY_PROP_NAME = "aeron.archive.idle.strategy";
        public static final String DEFAULT_IDLE_STRATEGY = "org.agrona.concurrent.BackoffIdleStrategy";

        public static final String MAX_CONCURRENT_RECORDINGS_PROP_NAME = "aeron.archive.max.concurrent.recordings";
        public static final int MAX_CONCURRENT_RECORDINGS_DEFAULT = 50;

        public static final String MAX_CONCURRENT_REPLAYS_PROP_NAME = "aeron.archive.max.concurrent.replays";
        public static final int MAX_CONCURRENT_REPLAYS_DEFAULT = 50;

        public static final String MAX_CATALOG_ENTRIES_PROP_NAME = "aeron.archive.max.catalog.entries";
        public static final long MAX_CATALOG_ENTRIES_DEFAULT = Catalog.DEFAULT_MAX_ENTRIES;

        public static final String CONNECT_TIMEOUT_PROP_NAME = "aeron.archive.connect.timeout";
        public static final long CONNECT_TIMEOUT_DEFAULT_NS = TimeUnit.SECONDS.toNanos(5);

        public static final String REPLAY_LINGER_TIMEOUT_PROP_NAME = "aeron.archive.replay.linger.timeout";
        public static final long REPLAY_LINGER_TIMEOUT_DEFAULT_NS =
            io.aeron.driver.Configuration.publicationLingerTimeoutNs();

        public static final String ARCHIVE_DIR_DELETE_ON_START_PROP_NAME = "aeron.archive.dir.delete.on.start";

        static final String CATALOG_FILE_NAME = "archive.catalog";
        static final String RECORDING_SEGMENT_POSTFIX = ".rec";
        static final int MAX_BLOCK_LENGTH = 2 * 1024 * 1024;

        /**
         * Get the directory name to be used for storing the archive.
         *
         * @return the directory name to be used for storing the archive.
         */
        public static String archiveDirName()
        {
            return System.getProperty(ARCHIVE_DIR_PROP_NAME, ARCHIVE_DIR_DEFAULT);
        }

        /**
         * The length of file to be used for storing recording segments that must be a power of 2.
         *

         * If the {@link Image#termBufferLength()} is greater then this will take priority.
         *
         * @return length of file to be used for storing recording segments.
         */
        public static int segmentFileLength()
        {
            return getSizeAsInt(SEGMENT_FILE_LENGTH_PROP_NAME, SEGMENT_FILE_LENGTH_DEFAULT);
        }

        /**
         * The level at which files should be sync'ed to disk.
         *

         *
0 - normal writes.

         *
1 - sync file data.

         *
2 - sync file data + metadata.

         *

         *
         * @return level at which files should be sync'ed to disk.
         */
        public static int fileSyncLevel()
        {
            return Integer.getInteger(FILE_SYNC_LEVEL_PROP_NAME, FILE_SYNC_LEVEL_DEFAULT);
        }

        /**
         * The level at which the catalog file should be sync'ed to disk.
         *

         *
0 - normal writes.

         *
1 - sync file data.

         *
2 - sync file data + metadata.

         *

         *
         * @return level at which files should be sync'ed to disk.
         */
        public static int catalogFileSyncLevel()
        {
            return Integer.getInteger(CATALOG_FILE_SYNC_LEVEL_PROP_NAME, CATALOG_FILE_SYNC_LEVEL_DEFAULT);
        }

        /**
         * The threading mode to be employed by the archive.
         *
         * @return the threading mode to be employed by the archive.
         */
        public static ArchiveThreadingMode threadingMode()
        {
            return ArchiveThreadingMode.valueOf(System.getProperty(
                THREADING_MODE_PROP_NAME, ArchiveThreadingMode.DEDICATED.name()));
        }

        /**
         * Create a supplier of {@link IdleStrategy}s that will use the system property.
         *
         * @param controllableStatus if a {@link org.agrona.concurrent.ControllableIdleStrategy} is required.
         * @return the new idle strategy
         */
        public static Supplier<IdleStrategy> idleStrategySupplier(final StatusIndicator controllableStatus)
        {
            return () ->
            {
                final String name = System.getProperty(ARCHIVE_IDLE_STRATEGY_PROP_NAME, DEFAULT_IDLE_STRATEGY);
                return io.aeron.driver.Configuration.agentIdleStrategy(name, controllableStatus);
            };
        }

        /**
         * The maximum number of recordings that can operate concurrently after which new requests will be rejected.
         *
         * @return the maximum number of recordings that can operate concurrently.
         */
        public static int maxConcurrentRecordings()
        {
            return Integer.getInteger(MAX_CONCURRENT_RECORDINGS_PROP_NAME, MAX_CONCURRENT_RECORDINGS_DEFAULT);
        }

        /**
         * The maximum number of replays that can operate concurrently after which new requests will be rejected.
         *
         * @return the maximum number of replays that can operate concurrently.
         */
        public static int maxConcurrentReplays()
        {
            return Integer.getInteger(MAX_CONCURRENT_REPLAYS_PROP_NAME, MAX_CONCURRENT_REPLAYS_DEFAULT);
        }

        /**
         * Maximum number of catalog entries to allocate for the catalog file.
         *
         * @return the maximum number of catalog entries to support for the catalog file.
         */
        public static long maxCatalogEntries()
        {
            return Long.getLong(MAX_CATALOG_ENTRIES_PROP_NAME, MAX_CATALOG_ENTRIES_DEFAULT);
        }

        /**
         * The timeout in nanoseconds to wait for a connection.
         *
         * @return timeout in nanoseconds to wait for a connection.
         * @see #CONNECT_TIMEOUT_PROP_NAME
         */
        public static long connectTimeoutNs()
        {
            return getDurationInNanos(CONNECT_TIMEOUT_PROP_NAME, CONNECT_TIMEOUT_DEFAULT_NS);
        }

        /**
         * The timeout in nanoseconds to for a replay network publication to linger after draining.
         *
         * @return timeout in nanoseconds for a replay network publication to wait in linger.
         * @see #REPLAY_LINGER_TIMEOUT_PROP_NAME
         * @see io.aeron.driver.Configuration#PUBLICATION_LINGER_PROP_NAME
         */
        public static long replayLingerTimeoutNs()
        {
            return getDurationInNanos(REPLAY_LINGER_TIMEOUT_PROP_NAME, REPLAY_LINGER_TIMEOUT_DEFAULT_NS);
        }

        /**
         * Whether to delete directory on start or not.
         *
         * @return whether to delete directory on start or not.
         * @see #ARCHIVE_DIR_DELETE_ON_START_PROP_NAME
         */
        public static boolean deleteArchiveOnStart()
        {
            return "true".equalsIgnoreCase(getProperty(ARCHIVE_DIR_DELETE_ON_START_PROP_NAME, "false"));
        }
    }

    /**
     * Overrides for the defaults and system properties.
     *

     * The context will be owned by {@link ArchiveConductor} after a successful
     * {@link Archive#launch(Context)} and closed via {@link Archive#close()}.
     */
    public static class Context implements Cloneable
    {
        private boolean deleteArchiveOnStart = Configuration.deleteArchiveOnStart();
        private boolean ownsAeronClient = false;
        private String aeronDirectoryName = CommonContext.getAeronDirectoryName();
        private Aeron aeron;
        private File archiveDir;
        private String archiveDirectoryName = Configuration.archiveDirName();
        private FileChannel archiveDirChannel;
        private Catalog catalog;
        private ArchiveMarkFile markFile;

        private String controlChannel = AeronArchive.Configuration.controlChannel();
        private int controlStreamId = AeronArchive.Configuration.controlStreamId();
        private String localControlChannel = AeronArchive.Configuration.localControlChannel();
        private int localControlStreamId = AeronArchive.Configuration.localControlStreamId();
        private boolean controlTermBufferSparse = AeronArchive.Configuration.controlTermBufferSparse();
        private int controlTermBufferLength = AeronArchive.Configuration.controlTermBufferLength();
        private int controlMtuLength = AeronArchive.Configuration.controlMtuLength();
        private String recordingEventsChannel = AeronArchive.Configuration.recordingEventsChannel();
        private int recordingEventsStreamId = AeronArchive.Configuration.recordingEventsStreamId();

        private long connectTimeoutNs = Configuration.connectTimeoutNs();
        private long replayLingerTimeoutNs = Configuration.replayLingerTimeoutNs();
        private long maxCatalogEntries = Configuration.maxCatalogEntries();
        private int segmentFileLength = Configuration.segmentFileLength();
        private int fileSyncLevel = Configuration.fileSyncLevel();
        private int catalogFileSyncLevel = Configuration.catalogFileSyncLevel();

        private ArchiveThreadingMode threadingMode = Configuration.threadingMode();
        private ThreadFactory threadFactory;

        private Supplier<IdleStrategy> idleStrategySupplier;
        private EpochClock epochClock;

        private ErrorHandler errorHandler;
        private AtomicCounter errorCounter;
        private CountedErrorHandler countedErrorHandler;

        private AgentInvoker mediaDriverAgentInvoker;
        private int maxConcurrentRecordings = Configuration.maxConcurrentRecordings();
        private int maxConcurrentReplays = Configuration.maxConcurrentReplays();

        /**
         * 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 the configuration parameters by resolving dependencies and null values to use defaults.
         */
        public void conclude()
        {
            Objects.requireNonNull(errorHandler, "Error handler must be supplied");

            if (null == epochClock)
            {
                epochClock = new SystemEpochClock();
            }

            if (null == aeron)
            {
                ownsAeronClient = true;

                aeron = Aeron.connect(
                    new Aeron.Context()
                        .aeronDirectoryName(aeronDirectoryName)
                        .errorHandler(errorHandler)
                        .epochClock(epochClock)
                        .driverAgentInvoker(mediaDriverAgentInvoker)
                        .useConductorAgentInvoker(true)
                        .clientLock(new NoOpLock()));

                if (null == errorCounter)
                {
                    errorCounter = aeron.addCounter(SYSTEM_COUNTER_TYPE_ID, "Archive errors");
                }
            }

            if (null == aeron.conductorAgentInvoker())
            {
                throw new ArchiveException("Aeron client must use conductor agent invoker");
            }

            Objects.requireNonNull(errorCounter, "Error counter must be supplied if aeron client is");

            if (null == countedErrorHandler)
            {
                countedErrorHandler = new CountedErrorHandler(errorHandler, errorCounter);
                if (ownsAeronClient)
                {
                    aeron.context().errorHandler(countedErrorHandler);
                }
            }

            if (null == threadFactory)
            {
                threadFactory = Thread::new;
            }

            if (null == idleStrategySupplier)
            {
                idleStrategySupplier = Configuration.idleStrategySupplier(null);
            }

            if (null == archiveDir)
            {
                archiveDir = new File(archiveDirectoryName);
            }

            if (deleteArchiveOnStart && archiveDir.exists())
            {
                IoUtil.delete(archiveDir, false);
            }

            if (!archiveDir.exists() && !archiveDir.mkdirs())
            {
                throw new ArchiveException(
                    "failed to create archive dir: " + archiveDir.getAbsolutePath());
            }

            archiveDirChannel = channelForDirectorySync(archiveDir, catalogFileSyncLevel);

            if (!BitUtil.isPowerOfTwo(segmentFileLength))
            {
                throw new ArchiveException("segment file length not a power of 2: " + segmentFileLength);
            }
            else if (segmentFileLength < TERM_MIN_LENGTH || segmentFileLength > TERM_MAX_LENGTH)
            {
                throw new ArchiveException("segment file length not in valid range: " + segmentFileLength);
            }

            if (null == markFile)
            {
                markFile = new ArchiveMarkFile(this);
            }

            if (null == catalog)
            {
                catalog = new Catalog(
                    archiveDir, archiveDirChannel, catalogFileSyncLevel, maxCatalogEntries, epochClock);
            }
        }

        /**
         * Should an existing archive be deleted on start. Useful only for testing.
         *
         * @param deleteArchiveOnStart true if an existing archive should be deleted on startup.
         * @return this for a fluent API.
         */
        public Context deleteArchiveOnStart(final boolean deleteArchiveOnStart)
        {
            this.deleteArchiveOnStart = deleteArchiveOnStart;
            return this;
        }

        /**
         * Should an existing archive be deleted on start. Useful only for testing.
         *
         * @return true if an existing archive should be deleted on start up.
         */
        public boolean deleteArchiveOnStart()
        {
            return deleteArchiveOnStart;
        }

        /**
         * Set the directory name to be used for the archive to store recordings and the {@link Catalog}.
         * This name is used if {@link #archiveDir(File)} is not set.
         *
         * @param archiveDirectoryName to store recordings and the {@link Catalog}.
         * @return this for a fluent API.
         * @see Configuration#ARCHIVE_DIR_PROP_NAME
         */
        public Context archiveDirectoryName(final String archiveDirectoryName)
        {
            this.archiveDirectoryName = archiveDirectoryName;
            return this;
        }

        /**
         * Get the directory name to be used to store recordings and the {@link Catalog}.
         *
         * @return the directory name to be used for the archive to store recordings and the {@link Catalog}.
         */
        public String archiveDirectoryName()
        {
            return archiveDirectoryName;
        }

        /**
         * Get the directory in which the Archive will store recordings and the {@link Catalog}.
         *
         * @return the directory in which the Archive will store recordings and the {@link Catalog}.
         */
        public File archiveDir()
        {
            return archiveDir;
        }

        /**
         * Set the the directory in which the Archive will store recordings and the {@link Catalog}.
         *
         * @param archiveDir the directory in which the Archive will store recordings and the {@link Catalog}.
         * @return this for a fluent API.
         */
        public Context archiveDir(final File archiveDir)
        {
            this.archiveDir = archiveDir;
            return this;
        }

        /**
         * Get the {@link FileChannel} for the directory in which the Archive will store recordings and the
         * {@link Catalog}. This can be used for sync'ing the directory.
         *
         * @return the directory in which the Archive will store recordings and the {@link Catalog}.
         */
        public FileChannel archiveDirChannel()
        {
            return archiveDirChannel;
        }

        /**
         * Get the channel URI on which the control request subscription will listen.
         *
         * @return the channel URI on which the control request subscription will listen.
         * @see io.aeron.archive.client.AeronArchive.Configuration#CONTROL_CHANNEL_PROP_NAME
         */
        public String controlChannel()
        {
            return controlChannel;
        }

        /**
         * Set the channel URI on which the control request subscription will listen.
         *
         * @param controlChannel channel URI on which the control request subscription will listen.
         * @return this for a fluent API.
         * @see io.aeron.archive.client.AeronArchive.Configuration#CONTROL_CHANNEL_PROP_NAME
         */
        public Context controlChannel(final String controlChannel)
        {
            this.controlChannel = controlChannel;
            return this;
        }

        /**
         * Get the stream id on which the control request subscription will listen.
         *
         * @return the stream id on which the control request subscription will listen.
         * @see io.aeron.archive.client.AeronArchive.Configuration#CONTROL_STREAM_ID_PROP_NAME
         */
        public int controlStreamId()
        {
            return controlStreamId;
        }

        /**
         * Set the stream id on which the control request subscription will listen.
         *
         * @param controlStreamId stream id on which the control request subscription will listen.
         * @return this for a fluent API.
         * @see io.aeron.archive.client.AeronArchive.Configuration#CONTROL_STREAM_ID_PROP_NAME
         */
        public Context controlStreamId(final int controlStreamId)
        {
            this.controlStreamId = controlStreamId;
            return this;
        }

        /**
         * Get the driver local channel URI on which the control request subscription will listen.
         *
         * @return the channel URI on which the control request subscription will listen.
         * @see io.aeron.archive.client.AeronArchive.Configuration#LOCAL_CONTROL_CHANNEL_PROP_NAME
         */
        public String localControlChannel()
        {
            return localControlChannel;
        }

        /**
         * Set the driver local channel URI on which the control request subscription will listen.
         *
         * @param controlChannel channel URI on which the control request subscription will listen.
         * @return this for a fluent API.
         * @see io.aeron.archive.client.AeronArchive.Configuration#LOCAL_CONTROL_CHANNEL_PROP_NAME
         */
        public Context localControlChannel(final String controlChannel)
        {
            this.localControlChannel = controlChannel;
            return this;
        }

        /**
         * Get the local stream id on which the control request subscription will listen.
         *
         * @return the stream id on which the control request subscription will listen.
         * @see io.aeron.archive.client.AeronArchive.Configuration#LOCAL_CONTROL_STREAM_ID_PROP_NAME
         */
        public int localControlStreamId()
        {
            return localControlStreamId;
        }

        /**
         * Should the control streams use sparse file term buffers.
         *
         * @param controlTermBufferSparse for the control stream.
         * @return this for a fluent API.
         * @see io.aeron.archive.client.AeronArchive.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 true if the control stream should use sparse file term buffers.
         * @see io.aeron.archive.client.AeronArchive.Configuration#CONTROL_TERM_BUFFER_SPARSE_PROP_NAME
         */
        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 io.aeron.archive.client.AeronArchive.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 io.aeron.archive.client.AeronArchive.Configuration#CONTROL_TERM_BUFFER_LENGTH_PROP_NAME
         */
        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 io.aeron.archive.client.AeronArchive.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 io.aeron.archive.client.AeronArchive.Configuration#CONTROL_MTU_LENGTH_PROP_NAME
         */
        public int controlMtuLength()
        {
            return controlMtuLength;
        }

        /**
         * Set the local stream id on which the control request subscription will listen.
         *
         * @param controlStreamId stream id on which the control request subscription will listen.
         * @return this for a fluent API.
         * @see io.aeron.archive.client.AeronArchive.Configuration#LOCAL_CONTROL_STREAM_ID_PROP_NAME
         */
        public Context localControlStreamId(final int controlStreamId)
        {
            this.localControlStreamId = controlStreamId;
            return this;
        }

        /**
         * Get the channel URI on which the recording events publication will publish.
         *
         * @return the channel URI on which the recording events publication will publish.
         * @see io.aeron.archive.client.AeronArchive.Configuration#RECORDING_EVENTS_CHANNEL_PROP_NAME
         */
        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.archive.client.AeronArchive.Configuration#RECORDING_EVENTS_CHANNEL_PROP_NAME
         * @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.
         * @see io.aeron.archive.client.AeronArchive.Configuration#RECORDING_EVENTS_STREAM_ID_PROP_NAME
         */
        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.
         * @see io.aeron.archive.client.AeronArchive.Configuration#RECORDING_EVENTS_STREAM_ID_PROP_NAME
         */
        public Context recordingEventsStreamId(final int recordingEventsStreamId)
        {
            this.recordingEventsStreamId = recordingEventsStreamId;
            return this;
        }

        /**
         * The timeout in nanoseconds to wait for connection to be established.
         *
         * @param connectTimeoutNs to wait for a connection to be established.
         * @return this for a fluent API.
         * @see Configuration#CONNECT_TIMEOUT_PROP_NAME
         */
        public Context connectTimeoutNs(final long connectTimeoutNs)
        {
            this.connectTimeoutNs = connectTimeoutNs;
            return this;
        }

        /**
         * The timeout in nanoseconds to wait for connection to be established.
         *
         * @return the message timeout in nanoseconds to wait for a connection to be established.
         * @see Configuration#CONNECT_TIMEOUT_PROP_NAME
         */
        public long connectTimeoutNs()
        {
            return connectTimeoutNs;
        }

        /**
         * The timeout in nanoseconds for or a replay publication to linger after draining.
         *
         * @param replayLingerTimeoutNs in nanoseconds for a replay publication to linger after draining.
         * @return this for a fluent API.
         * @see Configuration#REPLAY_LINGER_TIMEOUT_PROP_NAME
         * @see io.aeron.driver.Configuration#PUBLICATION_LINGER_PROP_NAME
         */
        public Context replayLingerTimeoutNs(final long replayLingerTimeoutNs)
        {
            this.replayLingerTimeoutNs = replayLingerTimeoutNs;
            return this;
        }

        /**
         * The timeout in nanoseconds for a replay publication to linger after draining.
         *
         * @return the timeout in nanoseconds for a replay publication to linger after draining.
         * @see Configuration#REPLAY_LINGER_TIMEOUT_PROP_NAME
         * @see io.aeron.driver.Configuration#PUBLICATION_LINGER_PROP_NAME
         */
        public long replayLingerTimeoutNs()
        {
            return replayLingerTimeoutNs;
        }

        /**
         * Provides an {@link IdleStrategy} supplier for the thread responsible for publication/subscription backoff.
         *
         * @param idleStrategySupplier supplier of thread idle strategy for publication/subscription backoff.
         * @return this for a fluent API.
         */
        public Context idleStrategySupplier(final Supplier<IdleStrategy> idleStrategySupplier)
        {
            this.idleStrategySupplier = idleStrategySupplier;
            return this;
        }

        /**
         * Get a new {@link IdleStrategy} based on configured supplier.
         *
         * @return a new {@link IdleStrategy} based on configured supplier.
         */
        public IdleStrategy idleStrategy()
        {
            return idleStrategySupplier.get();
        }

        /**
         * Set the {@link EpochClock} to be used for tracking wall clock time.
         *
         * @param clock {@link EpochClock} to be used for tracking wall clock time.
         * @return this for a fluent API.
         */
        public Context epochClock(final EpochClock clock)
        {
            this.epochClock = clock;
            return this;
        }

        /**
         * Get the {@link EpochClock} to used for tracking wall clock time.
         *
         * @return the {@link EpochClock} to used for tracking wall clock time.
         */
        public EpochClock epochClock()
        {
            return epochClock;
        }

        /**
         * Get the file length used for recording data segment files.
         *
         * @return the file length used for recording data segment files
         */
        int segmentFileLength()
        {
            return segmentFileLength;
        }

        /**
         * Set the file length to be used for recording data segment files. If the {@link Image#termBufferLength()} is
         * larger than the segment file length then the term length will be used.
         *
         * @param segmentFileLength the file length to be used for recording data segment files.
         * @return this for a fluent API.
         */
        public Context segmentFileLength(final int segmentFileLength)
        {
            this.segmentFileLength = segmentFileLength;
            return this;
        }

        /**
         * Get level at which files should be sync'ed to disk.
         *

         *
0 - normal writes.

         *
1 - sync file data.

         *
2 - sync file data + metadata.

         *

         *
         * @return the level to be applied for file write.
         */
        int fileSyncLevel()
        {
            return fileSyncLevel;
        }

        /**
         * Set level at which files should be sync'ed to disk.
         *

         *
0 - normal writes.

         *
1 - sync file data.

         *
2 - sync file data + metadata.

         *

         *
         * @param syncLevel to be applied for file writes.
         * @return this for a fluent API.
         */
        public Context fileSyncLevel(final int syncLevel)
        {
            this.fileSyncLevel = syncLevel;
            return this;
        }

        /**
         * Get level at which the catalog file should be sync'ed to disk.
         *

         *
0 - normal writes.

         *
1 - sync file data.

         *
2 - sync file data + metadata.

         *

         *
         * @return the level to be applied for file write.
         */
        int catalogFileSyncLevel()
        {
            return catalogFileSyncLevel;
        }

        /**
         * Set level at which the catalog file should be sync'ed to disk.
         *

         *
0 - normal writes.

         *
1 - sync file data.

         *
2 - sync file data + metadata.

         *

         *
         * @param syncLevel to be applied for file writes.
         * @return this for a fluent API.
         */
        public Context catalogFileSyncLevel(final int syncLevel)
        {
            this.catalogFileSyncLevel = syncLevel;
            return this;
        }

        /**
         * Get the {@link AgentInvoker} that should be used for the Media Driver if running in a lightweight mode.
         *
         * @return the {@link AgentInvoker} that should be used for the Media Driver if running in a lightweight mode.
         */
        AgentInvoker mediaDriverAgentInvoker()
        {
            return mediaDriverAgentInvoker;
        }

        /**
         * Set the {@link AgentInvoker} that should be used for the Media Driver if running in a lightweight mode.
         *
         * @param mediaDriverAgentInvoker that should be used for the Media Driver if running in a lightweight mode.
         * @return this for a fluent API.
         */
        public Context mediaDriverAgentInvoker(final AgentInvoker mediaDriverAgentInvoker)
        {
            this.mediaDriverAgentInvoker = mediaDriverAgentInvoker;
            return this;
        }

        /**
         * Get the {@link ErrorHandler} to be used by the Archive.
         *
         * @return the {@link ErrorHandler} to be used by the Archive.
         */
        public ErrorHandler errorHandler()
        {
            return errorHandler;
        }

        /**
         * Set the {@link ErrorHandler} to be used by the Archive.
         *
         * @param errorHandler the error handler to be used by the Archive.
         * @return this for a fluent API
         */
        public Context errorHandler(final ErrorHandler errorHandler)
        {
            this.errorHandler = errorHandler;
            return this;
        }

        /**
         * Non-default for context.
         *
         * @param countedErrorHandler to override the default.
         * @return this for a fluent API.
         */
        public Context countedErrorHandler(final CountedErrorHandler countedErrorHandler)
        {
            this.countedErrorHandler = countedErrorHandler;
            return this;
        }

        /**
         * The {@link #errorHandler()} that will increment {@link #errorCounter()} by default.
         *
         * @return {@link #errorHandler()} that will increment {@link #errorCounter()} by default.
         */
        public CountedErrorHandler countedErrorHandler()
        {
            return countedErrorHandler;
        }

        /**
         * Get the archive threading mode.
         *
         * @return the archive threading mode.
         */
        public ArchiveThreadingMode threadingMode()
        {
            return threadingMode;
        }

        /**
         * Set the archive threading mode.
         *
         * @param threadingMode archive threading mode.
         * @return this for a fluent API.
         */
        public Context threadingMode(final ArchiveThreadingMode threadingMode)
        {
            this.threadingMode = threadingMode;
            return this;
        }

        /**
         * Get the thread factory used for creating threads in {@link ArchiveThreadingMode#SHARED} and
         * {@link ArchiveThreadingMode#DEDICATED} threading modes.
         *
         * @return thread factory used for creating threads in SHARED and DEDICATED threading modes.
         */
        public ThreadFactory threadFactory()
        {
            return threadFactory;
        }

        /**
         * Set the thread factory used for creating threads in {@link ArchiveThreadingMode#SHARED} and
         * {@link ArchiveThreadingMode#DEDICATED} threading modes.
         *
         * @param threadFactory used for creating threads in SHARED and DEDICATED threading modes.
         * @return this for a fluent API.
         */
        public Context threadFactory(final ThreadFactory threadFactory)
        {
            this.threadFactory = threadFactory;
            return this;
        }

        /**
         * Get the error counter that will record the number of errors observed.
         *
         * @return the error counter that will record the number of errors observed.
         */
        public AtomicCounter errorCounter()
        {
            return errorCounter;
        }

        /**
         * Set the error counter that will record the number of errors observed.
         *
         * @param errorCounter the error counter that will record the number of errors observed.
         * @return this for a fluent API.
         */
        public Context errorCounter(final AtomicCounter errorCounter)
        {
            this.errorCounter = errorCounter;
            return this;
        }

        /**
         * Get the max number of concurrent recordings.
         *
         * @return the max number of concurrent recordings.
         */
        public int maxConcurrentRecordings()
        {
            return maxConcurrentRecordings;
        }

        /**
         * Set the max number of concurrent recordings.
         *
         * @param maxConcurrentRecordings the max number of concurrent recordings.
         * @return this for a fluent API.
         */
        public Context maxConcurrentRecordings(final int maxConcurrentRecordings)
        {
            this.maxConcurrentRecordings = maxConcurrentRecordings;
            return this;
        }

        /**
         * Get the max number of concurrent replays.
         *
         * @return the max number of concurrent replays.
         */
        public int maxConcurrentReplays()
        {
            return maxConcurrentReplays;
        }

        /**
         * Set the max number of concurrent replays.
         *
         * @param maxConcurrentReplays the max number of concurrent replays.
         * @return this for a fluent API.
         */
        public Context maxConcurrentReplays(final int maxConcurrentReplays)
        {
            this.maxConcurrentReplays = maxConcurrentReplays;
            return this;
        }

        /**
         * Delete the archive directory if the {@link #archiveDir()} value is not null.
         */
        public void deleteArchiveDirectory()
        {
            if (null != archiveDir)
            {
                IoUtil.delete(archiveDir, false);
            }
        }

        /**
         * 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 Archive#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 this 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 this takes responsibility for closing it?
         *
         * @return does this context own the {@link #aeron()} client and this takes responsibility for closing it?
         */
        public boolean ownsAeronClient()
        {
            return ownsAeronClient;
        }

        /**
         * The {@link Catalog} describing the contents of the Archive.
         *
         * @param catalog {@link Catalog} describing the contents of the Archive.
         * @return this for a fluent API.
         */
        public Context catalog(final Catalog catalog)
        {
            this.catalog = catalog;
            return this;
        }

        /**
         * The {@link Catalog} describing the contents of the Archive.
         *
         * @return the {@link Catalog} describing the contents of the Archive.
         */
        public Catalog catalog()
        {
            return catalog;
        }

        /**
         * The {@link ArchiveMarkFile} for the Archive.
         *
         * @param archiveMarkFile {@link ArchiveMarkFile} for the Archive.
         * @return this for a fluent API.
         */
        public Context archiveMarkFile(final ArchiveMarkFile archiveMarkFile)
        {
            this.markFile = archiveMarkFile;
            return this;
        }

        /**
         * The {@link ArchiveMarkFile} for the Archive.
         *
         * @return {@link ArchiveMarkFile} for the Archive.
         */
        public ArchiveMarkFile archiveMarkFile()
        {
            return markFile;
        }

        /**
         * Maximum number of catalog entries for the Archive.
         *
         * @param maxCatalogEntries for the archive.
         * @return this for a fluent API.
         */
        public Context maxCatalogEntries(final long maxCatalogEntries)
        {
            this.maxCatalogEntries = maxCatalogEntries;
            return this;
        }

        /**
         * Maximum number of catalog entries for the Archive.
         *
         * @return maximum number of catalog entries for the Archive.
         */
        public long maxCatalogEntries()
        {
            return maxCatalogEntries;
        }

        /**
         * 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);
            }

            CloseHelper.close(catalog);
            CloseHelper.close(markFile);
            CloseHelper.close(archiveDirChannel);
            archiveDirChannel = null;
        }
    }

    static int segmentFileIndex(final long startPosition, final long position, final int segmentFileLength)
    {
        return (int)((position - startPosition) >> LogBufferDescriptor.positionBitsToShift(segmentFileLength));
    }

    static String segmentFileName(final long recordingId, final int segmentIndex)
    {
        return recordingId + "-" + segmentIndex + Configuration.RECORDING_SEGMENT_POSTFIX;
    }

    static FileChannel channelForDirectorySync(final File directory, final int fileSyncLevel)
    {
        if (fileSyncLevel > 0)
        {
            try
            {
                return FileChannel.open(directory.toPath());
            }
            catch (final IOException ignore)
            {
            }
        }

        return null;
    }
}


/*
 * Copyright 2014-2019 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.command;

import org.agrona.DirectBuffer;

import static org.agrona.BitUtil.SIZE_OF_INT;
import static org.agrona.BitUtil.SIZE_OF_LONG;

/**
 * Command message flyweight to ask the driver process to terminate.
 *
 * @see ControlProtocolEvents
 *

 *   0                   1                   2                   3
 *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                         Correlation ID                        |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                         Token Length                          |
 *  +---------------------------------------------------------------+
 *  |                         Token Buffer                         ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *

 */
public class TerminateDriverFlyweight extends CorrelatedMessageFlyweight
{
    private static final int TOKEN_LENGTH_OFFSET = CORRELATION_ID_FIELD_OFFSET + SIZE_OF_LONG;

    /**
     * Offset of the token buffer
     *
     * @return offset of the token buffer
     */
    public int tokenBufferOffset()
    {
        return TOKEN_LENGTH_OFFSET + SIZE_OF_INT;
    }

    /**
     * Length of the token buffer in bytes
     *
     * @return length of token buffer in bytes
     */
    public int tokenBufferLength()
    {
        return buffer.getInt(offset + TOKEN_LENGTH_OFFSET);
    }

    /**
     * Fill the token buffer.
     *
     * @param tokenBuffer containing the optional token for the request.
     * @param tokenOffset within the tokenBuffer at which the token begins.
     * @param tokenLength of the token in the tokenBuffer.
     * @return flyweight
     */
    public TerminateDriverFlyweight tokenBuffer(
        final DirectBuffer tokenBuffer, final int tokenOffset, final int tokenLength)
    {
        buffer.putInt(TOKEN_LENGTH_OFFSET, tokenLength);
        if (null != tokenBuffer && tokenLength > 0)
        {
            buffer.putBytes(tokenBufferOffset(), tokenBuffer, tokenOffset, tokenLength);
        }

        return this;
    }

    /**
     * Get the length of the current message.
     *

     * NB: must be called after the data is written in order to be accurate.
     *
     * @return the length of the current message
     */
    public int length()
    {
        return tokenBufferOffset() + buffer.getInt(offset + TOKEN_LENGTH_OFFSET);
    }
}


/*
 * Copyright 2014-2019 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.cluster.service;

import io.aeron.Aeron;
import io.aeron.Counter;
import io.aeron.cluster.client.ClusterException;
import org.agrona.BitUtil;
import org.agrona.DirectBuffer;
import org.agrona.MutableDirectBuffer;
import org.agrona.concurrent.status.CountersReader;

import static io.aeron.Aeron.NULL_VALUE;
import static org.agrona.BitUtil.SIZE_OF_INT;
import static org.agrona.BitUtil.SIZE_OF_LONG;
import static org.agrona.concurrent.status.CountersReader.*;

/**
 * Counter representing the Recovery state for the cluster.
 *

 * Key layout as follows:
 *

 *   0                   1                   2                   3
 *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                     Leadership Term ID                        |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                  Log position for Snapshot                    |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |              Timestamp at beginning of Recovery               |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                    Replay required flag                       |
 *  +---------------------------------------------------------------+
 *  |                     Count of Services                         |
 *  +---------------------------------------------------------------+
 *  |             Snapshot Recording ID (Service ID 0)              |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |             Snapshot Recording ID (Service ID n)              |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *

 */
public class RecoveryState
{
    /**
     * Type id of a recovery state counter.
     */
    public static final int RECOVERY_STATE_TYPE_ID = 204;

    /**
     * Human readable name for the counter.
     */
    public static final String NAME = "cluster recovery: leadershipTermId=";

    public static final int LEADERSHIP_TERM_ID_OFFSET = 0;
    public static final int LOG_POSITION_OFFSET = LEADERSHIP_TERM_ID_OFFSET + SIZE_OF_LONG;
    public static final int TIMESTAMP_OFFSET = LOG_POSITION_OFFSET + SIZE_OF_LONG;
    public static final int REPLAY_FLAG_OFFSET = TIMESTAMP_OFFSET + SIZE_OF_LONG;
    public static final int SERVICE_COUNT_OFFSET = REPLAY_FLAG_OFFSET + SIZE_OF_INT;
    public static final int SNAPSHOT_RECORDING_IDS_OFFSET = SERVICE_COUNT_OFFSET + SIZE_OF_INT;

    /**
     * Allocate a counter to represent the snapshot services should load on start.
     *
     * @param aeron                to allocate the counter.
     * @param tempBuffer           to use for building the key and label without allocation.
     * @param leadershipTermId     at which the snapshot was taken.
     * @param logPosition          at which the snapshot was taken.
     * @param timestamp            the snapshot was taken.
     * @param hasReplay            flag is true if all or part of the log must be replayed.
     * @param snapshotRecordingIds for the services to use during recovery indexed by service id.
     * @return the {@link Counter} for the recovery state.
     */
    public static Counter allocate(
        final Aeron aeron,
        final MutableDirectBuffer tempBuffer,
        final long leadershipTermId,
        final long logPosition,
        final long timestamp,
        final boolean hasReplay,
        final long... snapshotRecordingIds)
    {
        tempBuffer.putLong(LEADERSHIP_TERM_ID_OFFSET, leadershipTermId);
        tempBuffer.putLong(LOG_POSITION_OFFSET, logPosition);
        tempBuffer.putLong(TIMESTAMP_OFFSET, timestamp);
        tempBuffer.putInt(REPLAY_FLAG_OFFSET, hasReplay ? 1 : 0);

        final int serviceCount = snapshotRecordingIds.length;
        tempBuffer.putInt(SERVICE_COUNT_OFFSET, serviceCount);

        final int keyLength = SNAPSHOT_RECORDING_IDS_OFFSET + (serviceCount * SIZE_OF_LONG);
        if (keyLength > MAX_KEY_LENGTH)
        {
            throw new ClusterException(keyLength + " exceeds max key length " + MAX_KEY_LENGTH);
        }

        for (int i = 0; i < serviceCount; i++)
        {
            tempBuffer.putLong(SNAPSHOT_RECORDING_IDS_OFFSET + (i * SIZE_OF_LONG), snapshotRecordingIds[i]);
        }

        final int labelOffset = BitUtil.align(keyLength, SIZE_OF_INT);
        int labelLength = 0;
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, NAME);
        labelLength += tempBuffer.putLongAscii(keyLength + labelLength, leadershipTermId);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " logPosition=");
        labelLength += tempBuffer.putLongAscii(labelOffset + labelLength, logPosition);
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, " hasReplay=" + hasReplay);

        return aeron.addCounter(RECOVERY_STATE_TYPE_ID, tempBuffer, 0, keyLength, tempBuffer, labelOffset, labelLength);
    }

    /**
     * Find the active counter id for recovery state.
     *
     * @param counters to search within.
     * @return the counter id if found otherwise {@link CountersReader#NULL_COUNTER_ID}.
     */
    public static int findCounterId(final CountersReader counters)
    {
        final DirectBuffer buffer = counters.metaDataBuffer();

        for (int i = 0, size = counters.maxCounterId(); i < size; i++)
        {
            if (counters.getCounterState(i) == RECORD_ALLOCATED)
            {
                final int recordOffset = CountersReader.metaDataOffset(i);

                if (buffer.getInt(recordOffset + TYPE_ID_OFFSET) == RECOVERY_STATE_TYPE_ID)
                {
                    return i;
                }
            }
        }

        return NULL_COUNTER_ID;
    }

    /**
     * Get the leadership term id for the snapshot state. {@link Aeron#NULL_VALUE} if no snapshot for recovery.
     *
     * @param counters  to search within.
     * @param counterId for the active recovery counter.
     * @return the leadership term id if found otherwise {@link Aeron#NULL_VALUE}.
     */
    public static long getLeadershipTermId(final CountersReader counters, final int counterId)
    {
        final DirectBuffer buffer = counters.metaDataBuffer();

        if (counters.getCounterState(counterId) == RECORD_ALLOCATED)
        {
            final int recordOffset = CountersReader.metaDataOffset(counterId);

            if (buffer.getInt(recordOffset + TYPE_ID_OFFSET) == RECOVERY_STATE_TYPE_ID)
            {
                return buffer.getLong(recordOffset + KEY_OFFSET + LEADERSHIP_TERM_ID_OFFSET);
            }
        }

        return NULL_VALUE;
    }

    /**
     * Get the position at which the snapshot was taken. {@link Aeron#NULL_VALUE} if no snapshot for recovery.
     *
     * @param counters  to search within.
     * @param counterId for the active recovery counter.
     * @return the log position if found otherwise {@link Aeron#NULL_VALUE}.
     */
    public static long getLogPosition(final CountersReader counters, final int counterId)
    {
        final DirectBuffer buffer = counters.metaDataBuffer();

        if (counters.getCounterState(counterId) == RECORD_ALLOCATED)
        {
            final int recordOffset = CountersReader.metaDataOffset(counterId);

            if (buffer.getInt(recordOffset + TYPE_ID_OFFSET) == RECOVERY_STATE_TYPE_ID)
            {
                return buffer.getLong(recordOffset + KEY_OFFSET + LOG_POSITION_OFFSET);
            }
        }

        return NULL_VALUE;
    }

    /**
     * Get the timestamp at the beginning of recovery. {@link Aeron#NULL_VALUE} if no snapshot for recovery.
     *
     * @param counters  to search within.
     * @param counterId for the active recovery counter.
     * @return the timestamp if found otherwise {@link Aeron#NULL_VALUE}.
     */
    public static long getTimestamp(final CountersReader counters, final int counterId)
    {
        final DirectBuffer buffer = counters.metaDataBuffer();

        if (counters.getCounterState(counterId) == RECORD_ALLOCATED)
        {
            final int recordOffset = CountersReader.metaDataOffset(counterId);

            if (buffer.getInt(recordOffset + TYPE_ID_OFFSET) == RECOVERY_STATE_TYPE_ID)
            {
                return buffer.getLong(recordOffset + KEY_OFFSET + TIMESTAMP_OFFSET);
            }
        }

        return NULL_VALUE;
    }

    /**
     * Has the recovery process got a log to replay?
     *
     * @param counters  to search within.
     * @param counterId for the active recovery counter.
     * @return true if a replay is required.
     */
    public static boolean hasReplay(final CountersReader counters, final int counterId)
    {
        final DirectBuffer buffer = counters.metaDataBuffer();

        if (counters.getCounterState(counterId) == RECORD_ALLOCATED)
        {
            final int recordOffset = CountersReader.metaDataOffset(counterId);

            if (buffer.getInt(recordOffset + TYPE_ID_OFFSET) == RECOVERY_STATE_TYPE_ID)
            {
                return buffer.getInt(recordOffset + KEY_OFFSET + REPLAY_FLAG_OFFSET) == 1;
            }
        }

        return false;
    }

    /**
     * Get the recording id of the snapshot for a service.
     *
     * @param counters  to search within.
     * @param counterId for the active recovery counter.
     * @param serviceId for the snapshot required.
     * @return the count of replay terms if found otherwise {@link Aeron#NULL_VALUE}.
     */
    public static long getSnapshotRecordingId(final CountersReader counters, final int counterId, final int serviceId)
    {
        final DirectBuffer buffer = counters.metaDataBuffer();

        if (counters.getCounterState(counterId) == RECORD_ALLOCATED)
        {
            final int recordOffset = CountersReader.metaDataOffset(counterId);

            if (buffer.getInt(recordOffset + TYPE_ID_OFFSET) == RECOVERY_STATE_TYPE_ID)
            {
                final int serviceCount = buffer.getInt(recordOffset + KEY_OFFSET + SERVICE_COUNT_OFFSET);
                if (serviceId < 0 || serviceId >= serviceCount)
                {
                    throw new ClusterException("invalid serviceId " + serviceId + " for count of " + serviceCount);
                }

                return buffer.getLong(
                    recordOffset + KEY_OFFSET + SNAPSHOT_RECORDING_IDS_OFFSET + (serviceId * SIZE_OF_LONG));
            }
        }

        throw new ClusterException("Active counter not found " + counterId);
    }
}


/*
 * Copyright 2014-2019 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.protocol;

import org.agrona.concurrent.UnsafeBuffer;

import java.nio.ByteBuffer;

import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static org.agrona.BitUtil.SIZE_OF_SHORT;

/**
 * Flyweight for general Aeron network protocol header of a message frame.
 * <pre>
 *   0                   1                   2                   3
 *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                        Frame Length                           |
 *  +---------------------------------------------------------------+
 *  |  Version    |     Flags     |               Type              |
 *  +-------------+---------------+---------------------------------+
 *  |                       Depends on Type                        ...
 * </pre>
 */
public class HeaderFlyweight extends UnsafeBuffer
{
    /**
     * header type PAD
     */
    public static final int HDR_TYPE_PAD = 0x00;

    /**
     * header type DATA
     */
    public static final int HDR_TYPE_DATA = 0x01;

    /**
     * header type NAK
     */
    public static final int HDR_TYPE_NAK = 0x02;

    /**
     * header type SM
     */
    public static final int HDR_TYPE_SM = 0x03;

    /**
     * header type ERR
     */
    public static final int HDR_TYPE_ERR = 0x04;

    /**
     * header type SETUP
     */
    public static final int HDR_TYPE_SETUP = 0x05;
    /**
     * header type RTT Measurement
     */
    public static final int HDR_TYPE_RTTM = 0x06;

    /**
     * header type EXT
     */
    public static final int HDR_TYPE_EXT = 0xFFFF;

    /**
     * default version
     */
    public static final byte CURRENT_VERSION = 0x0;

    public static final int FRAME_LENGTH_FIELD_OFFSET = 0;
    public static final int VERSION_FIELD_OFFSET = 4;
    public static final int FLAGS_FIELD_OFFSET = 5;
    public static final int TYPE_FIELD_OFFSET = 6;
    public static final int MIN_HEADER_LENGTH = TYPE_FIELD_OFFSET + SIZE_OF_SHORT;

    public HeaderFlyweight()
    {
    }

    public HeaderFlyweight(final UnsafeBuffer buffer)
    {
        super(buffer);
    }

    public HeaderFlyweight(final ByteBuffer buffer)
    {
        super(buffer);
    }

    /**
     * return version field value
     *
     * @return ver field value
     */
    public short version()
    {
        return (short)(getByte(VERSION_FIELD_OFFSET) & 0xFF);
    }

    /**
     * set version field value
     *
     * @param version field value
     * @return flyweight
     */
    public HeaderFlyweight version(final short version)
    {
        putByte(VERSION_FIELD_OFFSET, (byte)version);

        return this;
    }

    /**
     * return flags field value
     *
     * @return flags field value
     */
    public short flags()
    {
        return (short)(getByte(FLAGS_FIELD_OFFSET) & 0xFF);
    }

    /**
     * set the flags field value
     *
     * @param flags field value
     * @return flyweight
     */
    public HeaderFlyweight flags(final short flags)
    {
        putByte(FLAGS_FIELD_OFFSET, (byte)flags);

        return this;
    }

    /**
     * return header type field
     *
     * @return type field value
     */
    public int headerType()
    {
        return getShort(TYPE_FIELD_OFFSET, LITTLE_ENDIAN) & 0xFFFF;
    }

    /**
     * set header type field
     *
     * @param type field value
     * @return flyweight
     */
    public HeaderFlyweight headerType(final int type)
    {
        putShort(TYPE_FIELD_OFFSET, (short)type, LITTLE_ENDIAN);

        return this;
    }

    /**
     * return frame length field
     *
     * @return frame length field
     */
    public int frameLength()
    {
        return getInt(FRAME_LENGTH_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * set frame length field
     *
     * @param length field value
     * @return flyweight
     */
    public HeaderFlyweight frameLength(final int length)
    {
        putInt(FRAME_LENGTH_FIELD_OFFSET, length, LITTLE_ENDIAN);

        return this;
    }

    /**
     * Convert header flags to an array of chars to be human readable.
     *
     * @param flags to be converted.
     * @return header flags converted to an array of chars to be human readable.
     */
    public static char[] flagsToChars(final short flags)
    {
        final char[] chars = new char[]{ '0', '0', '0', '0', '0', '0', '0', '0' };
        final int length = chars.length;
        short mask = (short)(1 << (length - 1));

        for (int i = 0; i < length; i++)
        {
            if ((flags & mask) == mask)
            {
                chars[i] = '1';
            }

            mask >>= 1;
        }

        return chars;
    }
}


/*
 * Copyright 2014-2019 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.driver.reports;

import org.agrona.BitUtil;
import org.agrona.concurrent.AtomicBuffer;

import static org.agrona.BitUtil.*;

/**
 * A report of loss events on a message stream.
 *

 * The provided {@link AtomicBuffer} can wrap a memory-mapped file so logging can be out of process. This provides
 * the benefit that if a crash or lockup occurs then the log can be read externally without loss of data.
 *

 * Note:This class is NOT threadsafe to be used from multiple logging threads.
 *

 * The error records are recorded to the memory mapped buffer in the following format.
 *

 *   0                   1                   2                   3
 *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |R|                    Observation Count                        |
 *  |                                                               |
 *  +-+-------------------------------------------------------------+
 *  |R|                     Total Bytes Lost                        |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                 First Observation Timestamp                   |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                  Last Observation Timestamp                   |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                          Session ID                           |
 *  +---------------------------------------------------------------+
 *  |                           Stream ID                           |
 *  +---------------------------------------------------------------+
 *  |                 Channel encoded in US-ASCII                  ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *  |                  Source encoded in US-ASCII                  ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *

 */
public class LossReport
{
    /**
     * Alignment to be applied for each entry offset.
     */
    public static final int ENTRY_ALIGNMENT = CACHE_LINE_LENGTH;

    /**
     * Offset within an entry at which the observation count begins.
     */
    public static final int OBSERVATION_COUNT_OFFSET = 0;

    /**
     * Offset within an entry at which the total bytes field begins.
     */
    public static final int TOTAL_BYTES_LOST_OFFSET = OBSERVATION_COUNT_OFFSET + SIZE_OF_LONG;

    /**
     * Offset within an entry at which the first observation field begins.
     */
    public static final int FIRST_OBSERVATION_OFFSET = TOTAL_BYTES_LOST_OFFSET + SIZE_OF_LONG;

    /**
     * Offset within an entry at which the last observation field begins.
     */
    public static final int LAST_OBSERVATION_OFFSET = FIRST_OBSERVATION_OFFSET + SIZE_OF_LONG;

    /**
     * Offset within an entry at which the session id field begins.
     */
    public static final int SESSION_ID_OFFSET = LAST_OBSERVATION_OFFSET + SIZE_OF_LONG;

    /**
     * Offset within an entry at which the stream id field begins.
     */
    public static final int STREAM_ID_OFFSET = SESSION_ID_OFFSET + SIZE_OF_INT;

    /**
     * Offset within an entry at which the channel field begins.
     */
    public static final int CHANNEL_OFFSET = STREAM_ID_OFFSET + SIZE_OF_INT;

    private int nextRecordOffset = 0;
    private final AtomicBuffer buffer;

    /**
     * Create a loss report which wraps a buffer which is ideally memory mapped so it can
     * be read from another process.
     *
     * @param buffer to be wrapped.
     */
    public LossReport(final AtomicBuffer buffer)
    {
        buffer.verifyAlignment();
        this.buffer = buffer;
    }

    /**
     * Create a new entry for recording loss on a given stream.
     *

     * If not space is remaining in the error report then null is returned.
     *
     * @param initialBytesLost on the stream.
     * @param timestampMs      at which the first loss was observed.
     * @param sessionId        for the stream.
     * @param streamId         for the stream.
     * @param channel          for the stream.
     * @param source           of the stream.
     * @return a new record or null if the error log has insufficient space.
     */
    public ReportEntry createEntry(
        final long initialBytesLost,
        final long timestampMs,
        final int sessionId,
        final int streamId,
        final String channel,
        final String source)
    {
        ReportEntry reportEntry = null;

        final int requiredCapacity = CHANNEL_OFFSET + (SIZE_OF_INT * 2) + channel.length() + source.length();

        if (requiredCapacity <= (buffer.capacity() - nextRecordOffset))
        {
            final int offset = nextRecordOffset;

            buffer.putLong(offset + TOTAL_BYTES_LOST_OFFSET, initialBytesLost);
            buffer.putLong(offset + FIRST_OBSERVATION_OFFSET, timestampMs);
            buffer.putLong(offset + LAST_OBSERVATION_OFFSET, timestampMs);
            buffer.putInt(offset + SESSION_ID_OFFSET, sessionId);
            buffer.putInt(offset + STREAM_ID_OFFSET, streamId);

            final int encodedChannelLength = buffer.putStringAscii(offset + CHANNEL_OFFSET, channel);
            buffer.putStringAscii(offset + CHANNEL_OFFSET + encodedChannelLength, source);

            buffer.putLongOrdered(offset + OBSERVATION_COUNT_OFFSET, 1);

            reportEntry = new ReportEntry(buffer, offset);
            nextRecordOffset += BitUtil.align(requiredCapacity, ENTRY_ALIGNMENT);
        }

        return reportEntry;
    }

    /**
     * Report entry for a specific stream. Once an entry has been created it can then be used repeatably
     * to capture the aggregate loss on a stream.
     */
    public static class ReportEntry
    {
        private final AtomicBuffer buffer;
        private final int offset;

        ReportEntry(final AtomicBuffer buffer, final int offset)
        {
            this.buffer = buffer;
            this.offset = offset;
        }

        /**
         * Record a loss observation for a particular stream.
         *
         * @param bytesLost   in this observation.
         * @param timestampMs when this observation occurred.
         */
        public void recordObservation(final long bytesLost, final long timestampMs)
        {
            buffer.putLong(offset + LAST_OBSERVATION_OFFSET, timestampMs);
            buffer.getAndAddLong(offset + TOTAL_BYTES_LOST_OFFSET, bytesLost);
            buffer.getAndAddLong(offset + OBSERVATION_COUNT_OFFSET, 1);
        }
    }
}


/*
 *  Copyright 2014-2019 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.cluster.client;

import io.aeron.Aeron;
import io.aeron.Publication;
import io.aeron.cluster.codecs.IngressMessageHeaderEncoder;
import io.aeron.cluster.codecs.MessageHeaderEncoder;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.UnsafeBuffer;

/**
 * Encapsulate applying a client message header for ingress to the cluster.
 *

 * The client message header is applied to the {@link Publication} before the offered buffer.
 *

 * Note: This class is NOT threadsafe for updating {@link #clusterSessionId(long)} or
 * {@link #leadershipTermId(long)}. Each publisher thread requires its own instance.
 */
public class IngressSessionDecorator
{
    /**
     * Length of the session header that will be prepended to the message.
     */
    public static final int HEADER_LENGTH =
        MessageHeaderEncoder.ENCODED_LENGTH + IngressMessageHeaderEncoder.BLOCK_LENGTH;

    private final IngressMessageHeaderEncoder ingressMessageHeaderEncoder = new IngressMessageHeaderEncoder();
    private final UnsafeBuffer headerBuffer = new UnsafeBuffer(new byte[HEADER_LENGTH]);

    /**
     * Construct a new ingress session header wrapper that defaults all fields to {@link Aeron#NULL_VALUE}.
     */
    public IngressSessionDecorator()
    {
        this(Aeron.NULL_VALUE, Aeron.NULL_VALUE);
    }

    /**
     * Construct a new session header wrapper.
     *
     * @param clusterSessionId that has been allocated by the cluster.
     * @param leadershipTermId of the current leader.
     */
    public IngressSessionDecorator(final long clusterSessionId, final long leadershipTermId)
    {
        ingressMessageHeaderEncoder
            .wrapAndApplyHeader(headerBuffer, 0, new MessageHeaderEncoder())
            .leadershipTermId(leadershipTermId)
            .clusterSessionId(clusterSessionId)
            .timestamp(Aeron.NULL_VALUE);
    }

    /**
     * Reset the cluster session id in the header.
     *
     * @param clusterSessionId to be set in the header.
     * @return this for a fluent API.
     */
    public IngressSessionDecorator clusterSessionId(final long clusterSessionId)
    {
        ingressMessageHeaderEncoder.clusterSessionId(clusterSessionId);
        return this;
    }

    /**
     * Reset the leadership term id in the header.
     *
     * @param leadershipTermId to be set in the header.
     * @return this for a fluent API.
     */
    public IngressSessionDecorator leadershipTermId(final long leadershipTermId)
    {
        ingressMessageHeaderEncoder.leadershipTermId(leadershipTermId);
        return this;
    }

    /**
     * Non-blocking publish of a partial buffer containing a message plus session header to a cluster.
     *

     * This version of the method will set the timestamp value in the header to {@link Aeron#NULL_VALUE}.
     *
     * @param publication to be offer to.
     * @param buffer      containing message.
     * @param offset      offset in the buffer at which the encoded message begins.
     * @param length      in bytes of the encoded message.
     * @return the same as {@link Publication#offer(DirectBuffer, int, int)}.
     */
    public long offer(final Publication publication, final DirectBuffer buffer, final int offset, final int length)
    {
        return publication.offer(headerBuffer, 0, HEADER_LENGTH, buffer, offset, length, null);
    }
}


/*
 * Copyright 2014-2019 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.command;

import static org.agrona.BitUtil.SIZE_OF_INT;
import static org.agrona.BitUtil.SIZE_OF_LONG;

/**
 * Control message for adding or removing a publication
 *

 *   0                   1                   2                   3
 *   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                       Correlation ID                          |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                          Stream ID                            |
 *  +---------------------------------------------------------------+
 *  |                       Channel Length                          |
 *  +---------------------------------------------------------------+
 *  |                       Channel (ASCII)                        ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *

 */
public class PublicationMessageFlyweight extends CorrelatedMessageFlyweight
{
    private static final int STREAM_ID_FIELD_OFFSET = CORRELATION_ID_FIELD_OFFSET + SIZE_OF_LONG;
    private static final int CHANNEL_OFFSET = STREAM_ID_FIELD_OFFSET + SIZE_OF_INT;

    private int lengthOfChannel;

    /**
     * Get the stream id field
     *
     * @return stream id field
     */
    public int streamId()
    {
        return buffer.getInt(offset + STREAM_ID_FIELD_OFFSET);
    }

    /**
     * Set the stream id field
     *
     * @param streamId field value
     * @return flyweight
     */
    public PublicationMessageFlyweight streamId(final int streamId)
    {
        buffer.putInt(offset + STREAM_ID_FIELD_OFFSET, streamId);

        return this;
    }

    /**
     * Get the channel field in ASCII
     *
     * @return channel field
     */
    public String channel()
    {
        return buffer.getStringAscii(offset + CHANNEL_OFFSET);
    }

    /**
     * Set the channel field in ASCII
     *
     * @param channel field value
     * @return flyweight
     */
    public PublicationMessageFlyweight channel(final String channel)
    {
        lengthOfChannel = buffer.putStringAscii(offset + CHANNEL_OFFSET, channel);

        return this;
    }

    /**
     * Get the length of the current message
     *

     * NB: must be called after the data is written in order to be accurate.
     *
     * @return the length of the current message
     */
    public int length()
    {
        return CHANNEL_OFFSET + lengthOfChannel;
    }
}


/*
 * Copyright 2014-2019 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.logbuffer;

import org.agrona.concurrent.UnsafeBuffer;

import static io.aeron.logbuffer.FrameDescriptor.*;
import static io.aeron.logbuffer.LogBufferDescriptor.applyDefaultHeader;
import static io.aeron.protocol.HeaderFlyweight.HDR_TYPE_PAD;

/**
 * Fills a gap in a term with a padding record.
 */
public class TermGapFiller
{
    /**
     * Try to gap fill the current term at a given offset if the gap contains no data.
     * <p>
     * Note: the gap offset plus gap length must end on a {@link FrameDescriptor#FRAME_ALIGNMENT} boundary.
     *
     * @param logMetaDataBuffer containing the default headers
     * @param termBuffer        to gap fill
     * @param termId            for the current term.
     * @param gapOffset         to fill from
     * @param gapLength         to length of the gap.
     * @return true if the gap has been filled with a padding record or false if data found.
     */
    public static boolean tryFillGap(
        final UnsafeBuffer logMetaDataBuffer,
        final UnsafeBuffer termBuffer,
        final int termId,
        final int gapOffset,
        final int gapLength)
    {
        int offset = (gapOffset + gapLength) - FRAME_ALIGNMENT;

        while (offset >= gapOffset)
        {
            if (0 != termBuffer.getInt(offset))
            {
                return false;
            }

            offset -= FRAME_ALIGNMENT;
        }

        applyDefaultHeader(logMetaDataBuffer, termBuffer, gapOffset);
        frameType(termBuffer, gapOffset, HDR_TYPE_PAD);
        frameTermOffset(termBuffer, gapOffset);
        frameTermId(termBuffer, gapOffset, termId);
        frameLengthOrdered(termBuffer, gapOffset, gapLength);

        return true;
    }
}


/*
 * Copyright 2014-2019 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.logbuffer;

import org.agrona.*;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.status.Position;

import static io.aeron.logbuffer.FrameDescriptor.*;
import static io.aeron.protocol.DataHeaderFlyweight.HEADER_LENGTH;

/**
 * Utility functions for reading a term within a log buffer.
 */
public class TermReader
{
    /**
     * Reads data from a term in a log buffer and updates a passed {@link Position} so progress is not lost in the
     * event of an exception.
     *
     * @param termBuffer         to be read for fragments.
     * @param termOffset         within the buffer that the read should begin.
     * @param handler            the handler for data that has been read
     * @param fragmentsLimit     limit the number of fragments read.
     * @param header             to be used for mapping over the header for a given fragment.
     * @param errorHandler       to be notified if an error occurs during the callback.
     * @param currentPosition    prior to reading further fragments
     * @param subscriberPosition to be updated after reading with new position
     * @return the number of fragments read
     */
    public static int read(
        final UnsafeBuffer termBuffer,
        final int termOffset,
        final FragmentHandler handler,
        final int fragmentsLimit,
        final Header header,
        final ErrorHandler errorHandler,
        final long currentPosition,
        final Position subscriberPosition)
    {
        int fragmentsRead = 0;
        int offset = termOffset;
        final int capacity = termBuffer.capacity();
        header.buffer(termBuffer);

        try
        {
            while (fragmentsRead < fragmentsLimit && offset < capacity)
            {
                final int frameLength = frameLengthVolatile(termBuffer, offset);
                if (frameLength <= 0)
                {
                    break;
                }

                final int frameOffset = offset;
                offset += BitUtil.align(frameLength, FRAME_ALIGNMENT);

                if (!isPaddingFrame(termBuffer, frameOffset))
                {
                    header.offset(frameOffset);
                    handler.onFragment(termBuffer, frameOffset + HEADER_LENGTH, frameLength - HEADER_LENGTH, header);

                    ++fragmentsRead;
                }
            }
        }
        catch (final Throwable t)
        {
            errorHandler.onError(t);
        }
        finally
        {
            final long newPosition = currentPosition + (offset - termOffset);
            if (newPosition > currentPosition)
            {
                subscriberPosition.setOrdered(newPosition);
            }
        }

        return fragmentsRead;
    }

    /**
     * Reads data from a term in a log buffer.
     *
     * @param termBuffer     to be read for fragments.
     * @param termOffset     within the buffer that the read should begin.
     * @param handler        the handler for data that has been read
     * @param fragmentsLimit limit the number of fragments read.
     * @param header         to be used for mapping over the header for a given fragment.
     * @param errorHandler   to be notified if an error occurs during the callback.
     * @return the number of fragments read
     */
    public static long read(
        final UnsafeBuffer termBuffer,
        final int termOffset,
        final FragmentHandler handler,
        final int fragmentsLimit,
        final Header header,
        final ErrorHandler errorHandler)
    {
        int fragmentsRead = 0;
        int offset = termOffset;
        final int capacity = termBuffer.capacity();
        header.buffer(termBuffer);

        try
        {
            while (fragmentsRead < fragmentsLimit && offset < capacity)
            {
                final int frameLength = frameLengthVolatile(termBuffer, offset);
                if (frameLength <= 0)
                {
                    break;
                }

                final int frameOffset = offset;
                offset += BitUtil.align(frameLength, FRAME_ALIGNMENT);

                if (!isPaddingFrame(termBuffer, frameOffset))
                {
                    header.offset(frameOffset);
                    handler.onFragment(termBuffer, frameOffset + HEADER_LENGTH, frameLength - HEADER_LENGTH, header);

                    ++fragmentsRead;
                }
            }
        }
        catch (final Throwable t)
        {
            errorHandler.onError(t);
        }

        return pack(offset, fragmentsRead);
    }

    /**
     * Pack the values for fragmentsRead and offset into a long for returning on the stack.
     *
     * @param offset        value to be packed.
     * @param fragmentsRead value to be packed.
     * @return a long with both ints packed into it.
     */
    public static long pack(final int offset, final int fragmentsRead)
    {
        return ((long)offset << 32) | fragmentsRead;
    }

    /**
     * The number of fragments that have been read.
     *
     * @param readOutcome into which the fragments read value has been packed.
     * @return the number of fragments that have been read.
     */
    public static int fragmentsRead(final long readOutcome)
    {
        return (int)readOutcome;
    }

    /**
     * The offset up to which the term has progressed.
     *
     * @param readOutcome into which the offset value has been packed.
     * @return the offset up to which the term has progressed.
     */
    public static int offset(final long readOutcome)
    {
        return (int)(readOutcome >>> 32);
    }
}


/*
 * Copyright 2014-2019 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.logbuffer;

import org.agrona.UnsafeAccess;
import org.agrona.concurrent.UnsafeBuffer;

import java.nio.ByteOrder;

import static java.lang.Integer.reverseBytes;
import static io.aeron.protocol.DataHeaderFlyweight.SESSION_ID_FIELD_OFFSET;
import static io.aeron.protocol.DataHeaderFlyweight.STREAM_ID_FIELD_OFFSET;
import static io.aeron.protocol.DataHeaderFlyweight.TERM_OFFSET_FIELD_OFFSET;
import static io.aeron.protocol.HeaderFlyweight.FRAME_LENGTH_FIELD_OFFSET;
import static io.aeron.protocol.HeaderFlyweight.VERSION_FIELD_OFFSET;

/**
 * Utility for applying a header to a message in a term buffer.
 * <p>
 * This class is designed to be thread safe to be used across multiple producers and makes the header
 * visible in the correct order for consumers.
 */
public class HeaderWriter
{
    protected final long versionFlagsType;
    protected final long sessionId;
    protected final long streamId;

    protected HeaderWriter(final long versionFlagsType, final long sessionId, final long streamId)
    {
        this.versionFlagsType = versionFlagsType;
        this.sessionId = sessionId;
        this.streamId = streamId;
    }

    HeaderWriter(final UnsafeBuffer defaultHeader)
    {
        versionFlagsType = ((long)defaultHeader.getInt(VERSION_FIELD_OFFSET)) << 32;
        sessionId = ((long)defaultHeader.getInt(SESSION_ID_FIELD_OFFSET)) << 32;
        streamId = defaultHeader.getInt(STREAM_ID_FIELD_OFFSET) & 0xFFFF_FFFFL;
    }

    public static HeaderWriter newInstance(final UnsafeBuffer defaultHeader)
    {
        if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN)
        {
            return new HeaderWriter(defaultHeader);
        }
        else
        {
            return new NativeBigEndianHeaderWriter(defaultHeader);
        }
    }

    /**
     * Write a header to the term buffer in {@link ByteOrder#LITTLE_ENDIAN} format using the minimum instructions.
     *
     * @param termBuffer to be written to.
     * @param offset     at which the header should be written.
     * @param length     of the fragment including the header.
     * @param termId     of the current term buffer.
     */
    public void write(final UnsafeBuffer termBuffer, final int offset, final int length, final int termId)
    {
        termBuffer.putLongOrdered(offset + FRAME_LENGTH_FIELD_OFFSET, versionFlagsType | ((-length) & 0xFFFF_FFFFL));
        UnsafeAccess.UNSAFE.storeFence();

        termBuffer.putLong(offset + TERM_OFFSET_FIELD_OFFSET, sessionId | offset);
        termBuffer.putLong(offset + STREAM_ID_FIELD_OFFSET, streamId | (((long)termId) << 32));
    }
}

class NativeBigEndianHeaderWriter extends HeaderWriter
{
    NativeBigEndianHeaderWriter(final UnsafeBuffer defaultHeader)
    {
        super(
            defaultHeader.getInt(VERSION_FIELD_OFFSET) & 0xFFFF_FFFFL,
            defaultHeader.getInt(SESSION_ID_FIELD_OFFSET) & 0xFFFF_FFFFL,
            ((long)defaultHeader.getInt(STREAM_ID_FIELD_OFFSET)) << 32);
    }

    public void write(final UnsafeBuffer termBuffer, final int offset, final int length, final int termId)
    {
        termBuffer.putLongOrdered(
            offset + FRAME_LENGTH_FIELD_OFFSET, versionFlagsType | ((((long)reverseBytes(-length))) << 32));
        UnsafeAccess.UNSAFE.storeFence();

        termBuffer.putLong(offset + TERM_OFFSET_FIELD_OFFSET, sessionId | ((((long)reverseBytes(offset))) << 32));
        termBuffer.putLong(offset + STREAM_ID_FIELD_OFFSET, streamId | (reverseBytes(termId) & 0xFFFF_FFFFL));
    }
}