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

import io.aeron.logbuffer.*;
import io.aeron.logbuffer.ControlledFragmentHandler.Action;
import org.agrona.*;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.status.Position;

import java.nio.channels.FileChannel;

import static io.aeron.logbuffer.ControlledFragmentHandler.Action.*;
import static io.aeron.logbuffer.FrameDescriptor.*;
import static io.aeron.logbuffer.LogBufferDescriptor.endOfStreamPosition;
import static io.aeron.logbuffer.LogBufferDescriptor.indexByPosition;
import static io.aeron.protocol.DataHeaderFlyweight.HEADER_LENGTH;
import static io.aeron.protocol.DataHeaderFlyweight.TERM_ID_FIELD_OFFSET;
import static java.nio.ByteOrder.LITTLE_ENDIAN;

/**
 * Represents a replicated publication {@link Image} from a publisher to a {@link Subscription}.
 * Each {@link Image} identifies a source publisher by session id.
 *

 * By default fragmented messages are not reassembled before delivery. If an application must
 * receive whole messages, whether or not they were fragmented, then the Subscriber
 * should be created with a {@link FragmentAssembler} or a custom implementation.
 *

 * It is an application's responsibility to {@link #poll} the {@link Image} for new messages.
 *

 * Note:Images are not threadsafe and should not be shared between subscribers.
 */
public class Image
{
    private final long correlationId;
    private final long joinPosition;
    private long finalPosition;
    private final int sessionId;
    private final int initialTermId;
    private final int termLengthMask;
    private final int positionBitsToShift;
    private boolean isEos;
    private volatile boolean isClosed;

    private final Position subscriberPosition;
    private final UnsafeBuffer[] termBuffers;
    private final Header header;
    private final ErrorHandler errorHandler;
    private final LogBuffers logBuffers;
    private final String sourceIdentity;
    private final Subscription subscription;

    /**
     * Construct a new image over a log to represent a stream of messages from a {@link Publication}.
     *
     * @param subscription       to which this {@link Image} belongs.
     * @param sessionId          of the stream of messages.
     * @param subscriberPosition for indicating the position of the subscriber in the stream.
     * @param logBuffers         containing the stream of messages.
     * @param errorHandler       to be called if an error occurs when polling for messages.
     * @param sourceIdentity     of the source sending the stream of messages.
     * @param correlationId      of the request to the media driver.
     */
    public Image(
        final Subscription subscription,
        final int sessionId,
        final Position subscriberPosition,
        final LogBuffers logBuffers,
        final ErrorHandler errorHandler,
        final String sourceIdentity,
        final long correlationId)
    {
        this.subscription = subscription;
        this.sessionId = sessionId;
        this.subscriberPosition = subscriberPosition;
        this.logBuffers = logBuffers;
        this.errorHandler = errorHandler;
        this.sourceIdentity = sourceIdentity;
        this.correlationId = correlationId;
        this.joinPosition = subscriberPosition.get();

        termBuffers = logBuffers.duplicateTermBuffers();

        final int termLength = logBuffers.termLength();
        this.termLengthMask = termLength - 1;
        this.positionBitsToShift = LogBufferDescriptor.positionBitsToShift(termLength);
        this.initialTermId = LogBufferDescriptor.initialTermId(logBuffers.metaDataBuffer());
        header = new Header(initialTermId, positionBitsToShift, this);
    }

    /**
     * Number of bits to right shift a position to get a term count for how far the stream has progressed.
     *
     * @return of bits to right shift a position to get a term count for how far the stream has progressed.
     */
    public int positionBitsToShift()
    {
        return positionBitsToShift;
    }

    /**
     * Get the length in bytes for each term partition in the log buffer.
     *
     * @return the length in bytes for each term partition in the log buffer.
     */
    public int termBufferLength()
    {
        return termLengthMask + 1;
    }

    /**
     * The sessionId for the steam of messages. Sessions are unique within a {@link Subscription} and unique across
     * all {@link Publication}s from a {@link #sourceIdentity()}.
     *
     * @return the sessionId for the steam of messages.
     */
    public int sessionId()
    {
        return sessionId;
    }

    /**
     * The source identity of the sending publisher as an abstract concept appropriate for the media.
     *
     * @return source identity of the sending publisher as an abstract concept appropriate for the media.
     */
    public String sourceIdentity()
    {
        return sourceIdentity;
    }

    /**
     * The length in bytes of the MTU (Maximum Transmission Unit) the Sender used for the datagram.
     *
     * @return length in bytes of the MTU (Maximum Transmission Unit) the Sender used for the datagram.
     */
    public int mtuLength()
    {
        return LogBufferDescriptor.mtuLength(logBuffers.metaDataBuffer());
    }

    /**
     * The initial term at which the stream started for this session.
     *
     * @return the initial term id.
     */
    public int initialTermId()
    {
        return initialTermId;
    }

    /**
     * The correlationId for identification of the image with the media driver.
     *
     * @return the correlationId for identification of the image with the media driver.
     */
    public long correlationId()
    {
        return correlationId;
    }

    /**
     * Get the {@link Subscription} to which this {@link Image} belongs.
     *
     * @return the {@link Subscription} to which this {@link Image} belongs.
     */
    public Subscription subscription()
    {
        return subscription;
    }

    /**
     * Has this object been closed and should no longer be used?
     *
     * @return true if it has been closed otherwise false.
     */
    public boolean isClosed()
    {
        return isClosed;
    }

    /**
     * Get the position the subscriber joined this stream at.
     *
     * @return the position the subscriber joined this stream at.
     */
    public long joinPosition()
    {
        return joinPosition;
    }

    /**
     * The position this {@link Image} has been consumed to by the subscriber.
     *
     * @return the position this {@link Image} has been consumed to by the subscriber.
     */
    public long position()
    {
        if (isClosed)
        {
            return finalPosition;
        }

        return subscriberPosition.get();
    }

    /**
     * Set the subscriber position for this {@link Image} to indicate where it has been consumed to.
     *
     * @param newPosition for the consumption point.
     */
    public void position(final long newPosition)
    {
        if (!isClosed)
        {
            validatePosition(newPosition);
            subscriberPosition.setOrdered(newPosition);
        }
    }

    /**
     * The counter id for the subscriber position counter.
     *
     * @return the id for the subscriber position counter.
     */
    public int subscriberPositionId()
    {
        return subscriberPosition.id();
    }

    /**
     * Is the current consumed position at the end of the stream?
     *
     * @return true if at the end of the stream or false if not.
     */
    public boolean isEndOfStream()
    {
        if (isClosed)
        {
            return isEos;
        }

        return subscriberPosition.get() >= endOfStreamPosition(logBuffers.metaDataBuffer());
    }

    /**
     * The {@link FileChannel} to the raw log of the Image.
     *
     * @return the {@link FileChannel} to the raw log of the Image.
     */
    public FileChannel fileChannel()
    {
        return logBuffers.fileChannel();
    }

    /**
     * Poll for new messages in a stream. If new messages are found beyond the last consumed position then they
     * will be delivered to the {@link FragmentHandler} up to a limited number of fragments as specified.
     *

     * Use a {@link FragmentAssembler} to assemble messages which span multiple fragments.
     *
     * @param fragmentHandler to which message fragments are delivered.
     * @param fragmentLimit   for the number of fragments to be consumed during one polling operation.
     * @return the number of fragments that have been consumed.
     * @see FragmentAssembler
     * @see ImageFragmentAssembler
     */
    public int poll(final FragmentHandler fragmentHandler, final int fragmentLimit)
    {
        if (isClosed)
        {
            return 0;
        }

        final long position = subscriberPosition.get();

        return TermReader.read(
            activeTermBuffer(position),
            (int)position & termLengthMask,
            fragmentHandler,
            fragmentLimit,
            header,
            errorHandler,
            position,
            subscriberPosition);
    }

    /**
     * Poll for new messages in a stream. If new messages are found beyond the last consumed position then they
     * will be delivered to the {@link ControlledFragmentHandler} up to a limited number of fragments as specified.
     *

     * Use a {@link ControlledFragmentAssembler} to assemble messages which span multiple fragments.
     *
     * @param handler       to which message fragments are delivered.
     * @param fragmentLimit for the number of fragments to be consumed during one polling operation.
     * @return the number of fragments that have been consumed.
     * @see ControlledFragmentAssembler
     * @see ImageControlledFragmentAssembler
     */
    public int controlledPoll(final ControlledFragmentHandler handler, final int fragmentLimit)
    {
        if (isClosed)
        {
            return 0;
        }

        int fragmentsRead = 0;
        long initialPosition = subscriberPosition.get();
        int initialOffset = (int)initialPosition & termLengthMask;
        int resultingOffset = initialOffset;
        final UnsafeBuffer termBuffer = activeTermBuffer(initialPosition);
        final int capacity = termBuffer.capacity();
        final Header header = this.header;
        header.buffer(termBuffer);

        try
        {
            while (fragmentsRead < fragmentLimit && resultingOffset < capacity)
            {
                final int length = frameLengthVolatile(termBuffer, resultingOffset);
                if (length <= 0)
                {
                    break;
                }

                final int frameOffset = resultingOffset;
                final int alignedLength = BitUtil.align(length, FRAME_ALIGNMENT);
                resultingOffset += alignedLength;

                if (isPaddingFrame(termBuffer, frameOffset))
                {
                    continue;
                }

                header.offset(frameOffset);

                final Action action = handler.onFragment(
                    termBuffer, frameOffset + HEADER_LENGTH, length - HEADER_LENGTH, header);

                if (action == ABORT)
                {
                    resultingOffset -= alignedLength;
                    break;
                }

                ++fragmentsRead;

                if (action == BREAK)
                {
                    break;
                }
                else if (action == COMMIT)
                {
                    initialPosition += (resultingOffset - initialOffset);
                    initialOffset = resultingOffset;
                    subscriberPosition.setOrdered(initialPosition);
                }
            }
        }
        catch (final Throwable t)
        {
            errorHandler.onError(t);
        }
        finally
        {
            final long resultingPosition = initialPosition + (resultingOffset - initialOffset);
            if (resultingPosition > initialPosition)
            {
                subscriberPosition.setOrdered(resultingPosition);
            }
        }

        return fragmentsRead;
    }

    /**
     * Poll for new messages in a stream. If new messages are found beyond the last consumed position then they
     * will be delivered to the {@link ControlledFragmentHandler} up to a limited number of fragments as specified or
     * the maximum position specified.
     *

     * Use a {@link ControlledFragmentAssembler} to assemble messages which span multiple fragments.
     *
     * @param handler       to which message fragments are delivered.
     * @param maxPosition   to consume messages up to.
     * @param fragmentLimit for the number of fragments to be consumed during one polling operation.
     * @return the number of fragments that have been consumed.
     * @see ControlledFragmentAssembler
     * @see ImageControlledFragmentAssembler
     */
    public int boundedControlledPoll(
        final ControlledFragmentHandler handler, final long maxPosition, final int fragmentLimit)
    {
        if (isClosed)
        {
            return 0;
        }

        int fragmentsRead = 0;
        long initialPosition = subscriberPosition.get();
        int initialOffset = (int)initialPosition & termLengthMask;
        int resultingOffset = initialOffset;
        final UnsafeBuffer termBuffer = activeTermBuffer(initialPosition);
        final int endOffset = (int)Math.min(termBuffer.capacity(), maxPosition - initialPosition + initialOffset);
        final Header header = this.header;
        header.buffer(termBuffer);

        try
        {
            while (fragmentsRead < fragmentLimit && resultingOffset < endOffset)
            {
                final int length = frameLengthVolatile(termBuffer, resultingOffset);
                if (length <= 0)
                {
                    break;
                }

                final int frameOffset = resultingOffset;
                final int alignedLength = BitUtil.align(length, FRAME_ALIGNMENT);
                resultingOffset += alignedLength;

                if (isPaddingFrame(termBuffer, frameOffset))
                {
                    continue;
                }

                header.offset(frameOffset);

                final Action action = handler.onFragment(
                    termBuffer, frameOffset + HEADER_LENGTH, length - HEADER_LENGTH, header);

                if (action == ABORT)
                {
                    resultingOffset -= alignedLength;
                    break;
                }

                ++fragmentsRead;

                if (action == BREAK)
                {
                    break;
                }
                else if (action == COMMIT)
                {
                    initialPosition += (resultingOffset - initialOffset);
                    initialOffset = resultingOffset;
                    subscriberPosition.setOrdered(initialPosition);
                }
            }
        }
        catch (final Throwable t)
        {
            errorHandler.onError(t);
        }
        finally
        {
            final long resultingPosition = initialPosition + (resultingOffset - initialOffset);
            if (resultingPosition > initialPosition)
            {
                subscriberPosition.setOrdered(resultingPosition);
            }
        }

        return fragmentsRead;
    }

    /**
     * Peek for new messages in a stream by scanning forward from an initial position. If new messages are found then
     * they will be delivered to the {@link ControlledFragmentHandler} up to a limited position.
     *

     * Use a {@link ControlledFragmentAssembler} to assemble messages which span multiple fragments. Scans must also
     * start at the beginning of a message so that the assembler is reset.
     *
     * @param initialPosition from which to peek forward.
     * @param handler         to which message fragments are delivered.
     * @param limitPosition   up to which can be scanned.
     * @return the resulting position after the scan terminates which is a complete message.
     * @see ControlledFragmentAssembler
     * @see ImageControlledFragmentAssembler
     */
    public long controlledPeek(
        final long initialPosition, final ControlledFragmentHandler handler, final long limitPosition)
    {
        if (isClosed)
        {
            return 0;
        }

        validatePosition(initialPosition);

        int initialOffset = (int)initialPosition & termLengthMask;
        int offset = initialOffset;
        long position = initialPosition;
        final UnsafeBuffer termBuffer = activeTermBuffer(initialPosition);
        final int capacity = termBuffer.capacity();
        final Header header = this.header;
        header.buffer(termBuffer);
        long resultingPosition = initialPosition;

        try
        {
            while (position < limitPosition && offset < capacity)
            {
                final int length = frameLengthVolatile(termBuffer, offset);
                if (length <= 0)
                {
                    break;
                }

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

                if (isPaddingFrame(termBuffer, frameOffset))
                {
                    position += (offset - initialOffset);
                    initialOffset = offset;
                    resultingPosition = position;

                    continue;
                }

                header.offset(frameOffset);

                final Action action = handler.onFragment(
                    termBuffer, frameOffset + HEADER_LENGTH, length - HEADER_LENGTH, header);

                if (action == ABORT)
                {
                    break;
                }

                position += (offset - initialOffset);
                initialOffset = offset;

                if ((header.flags() & END_FRAG_FLAG) == END_FRAG_FLAG)
                {
                    resultingPosition = position;
                }

                if (action == BREAK)
                {
                    break;
                }
            }
        }
        catch (final Throwable t)
        {
            errorHandler.onError(t);
        }

        return resultingPosition;
    }

    /**
     * Poll for new messages in a stream. If new messages are found beyond the last consumed position then they
     * will be delivered to the {@link BlockHandler} up to a limited number of bytes.
     *

     * A scan will terminate if a padding frame is encountered. If first frame in a scan is padding then a block
     * for the padding is notified. If the padding comes after the first frame in a scan then the scan terminates
     * at the offset the padding frame begins. Padding frames are delivered singularly in a block.
     *

     * Padding frames may be for a greater range than the limit offset but only the header needs to be valid so
     * relevant length of the frame is {@link io.aeron.protocol.DataHeaderFlyweight#HEADER_LENGTH}.
     *
     * @param handler          to which block is delivered.
     * @param blockLengthLimit up to which a block may be in length.
     * @return the number of bytes that have been consumed.
     */
    public int blockPoll(final BlockHandler handler, final int blockLengthLimit)
    {
        if (isClosed)
        {
            return 0;
        }

        final long position = subscriberPosition.get();
        final int termOffset = (int)position & termLengthMask;
        final UnsafeBuffer termBuffer = activeTermBuffer(position);
        final int limitOffset = Math.min(termOffset + blockLengthLimit, termBuffer.capacity());
        final int resultingOffset = TermBlockScanner.scan(termBuffer, termOffset, limitOffset);
        final int length = resultingOffset - termOffset;

        if (resultingOffset > termOffset)
        {
            try
            {
                final int termId = termBuffer.getInt(termOffset + TERM_ID_FIELD_OFFSET, LITTLE_ENDIAN);
                handler.onBlock(termBuffer, termOffset, length, sessionId, termId);
            }
            catch (final Throwable t)
            {
                errorHandler.onError(t);
            }
            finally
            {
                subscriberPosition.setOrdered(position + length);
            }
        }

        return length;
    }

    /**
     * Poll for new messages in a stream. If new messages are found beyond the last consumed position then they
     * will be delivered to the {@link RawBlockHandler} up to a limited number of bytes.
     *

     * This method is useful for operations like bulk archiving a stream to file.
     *

     * A scan will terminate if a padding frame is encountered. If first frame in a scan is padding then a block
     * for the padding is notified. If the padding comes after the first frame in a scan then the scan terminates
     * at the offset the padding frame begins. Padding frames are delivered singularly in a block.
     *

     * Padding frames may be for a greater range than the limit offset but only the header needs to be valid so
     * relevant length of the frame is {@link io.aeron.protocol.DataHeaderFlyweight#HEADER_LENGTH}.
     *
     * @param handler          to which block is delivered.
     * @param blockLengthLimit up to which a block may be in length.
     * @return the number of bytes that have been consumed.
     */
    public int rawPoll(final RawBlockHandler handler, final int blockLengthLimit)
    {
        if (isClosed)
        {
            return 0;
        }

        final long position = subscriberPosition.get();
        final int termOffset = (int)position & termLengthMask;
        final int activeIndex = indexByPosition(position, positionBitsToShift);
        final UnsafeBuffer termBuffer = termBuffers[activeIndex];
        final int capacity = termBuffer.capacity();
        final int limitOffset = Math.min(termOffset + blockLengthLimit, capacity);
        final int resultingOffset = TermBlockScanner.scan(termBuffer, termOffset, limitOffset);
        final int length = resultingOffset - termOffset;

        if (resultingOffset > termOffset)
        {
            try
            {
                final long fileOffset = ((long)capacity * activeIndex) + termOffset;
                final int termId = termBuffer.getInt(termOffset + TERM_ID_FIELD_OFFSET, LITTLE_ENDIAN);

                handler.onBlock(
                    logBuffers.fileChannel(), fileOffset, termBuffer, termOffset, length, sessionId, termId);
            }
            catch (final Throwable t)
            {
                errorHandler.onError(t);
            }
            finally
            {
                subscriberPosition.setOrdered(position + length);
            }
        }

        return length;
    }

    private UnsafeBuffer activeTermBuffer(final long position)
    {
        return termBuffers[indexByPosition(position, positionBitsToShift)];
    }

    private void validatePosition(final long newPosition)
    {
        final long currentPosition = subscriberPosition.get();
        final long limitPosition = (currentPosition - (currentPosition & termLengthMask)) + termLengthMask + 1;
        if (newPosition < currentPosition || newPosition > limitPosition)
        {
            throw new IllegalArgumentException(
                newPosition + " newPosition out of range " + currentPosition + "-" + limitPosition);
        }

        if (0 != (newPosition & (FRAME_ALIGNMENT - 1)))
        {
            throw new IllegalArgumentException(newPosition + " newPosition not aligned to FRAME_ALIGNMENT");
        }
    }

    LogBuffers logBuffers()
    {
        return logBuffers;
    }

    void close()
    {
        finalPosition = subscriberPosition.getVolatile();
        isEos = finalPosition >= endOfStreamPosition(logBuffers.metaDataBuffer());
        isClosed = true;
    }

    public String toString()
    {
        return "Image{" +
            "correlationId=" + correlationId +
            ", joinPosition=" + joinPosition +
            ", sessionId=" + sessionId +
            ", initialTermId=" + initialTermId +
            ", isEos=" + isEos +
            ", sourceIdentity='" + sourceIdentity + '\'' +
            ", subscription=" + subscription +
            ", position=" + position() +
            '}';
    }
}


/*
 * 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 io.aeron.protocol.DataHeaderFlyweight;
import org.agrona.BitUtil;
import org.agrona.DirectBuffer;

import java.nio.ByteOrder;

import static io.aeron.logbuffer.FrameDescriptor.FRAME_ALIGNMENT;
import static io.aeron.protocol.DataHeaderFlyweight.*;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static io.aeron.logbuffer.LogBufferDescriptor.computePosition;

/**
 * Represents the header of the data frame for accessing meta data fields.
 */
public class Header
{
    private final int positionBitsToShift;
    private final int initialTermId;
    private int offset = 0;
    private DirectBuffer buffer;
    private final Object context;

    /**
     * Construct a header that references a buffer for the log.
     *
     * @param initialTermId       this stream started at.
     * @param positionBitsToShift for calculating positions.
     */
    public Header(final int initialTermId, final int positionBitsToShift)
    {
        this(initialTermId, positionBitsToShift, null);
    }

    /**
     * Construct a header that references a buffer for the log.
     *
     * @param initialTermId       this stream started at.
     * @param positionBitsToShift for calculating positions.
     * @param context             for storing state when which can be accessed with {@link #context()}.
     */
    public Header(final int initialTermId, final int positionBitsToShift, final Object context)
    {
        this.initialTermId = initialTermId;
        this.positionBitsToShift = positionBitsToShift;
        this.context = context;
    }

    /**
     * Context for storing state related to the context of the callback where the header is used.
     *
     * @return context for storing state related to the context of the callback where the header is used.
     */
    public Object context()
    {
        return context;
    }

    /**
     * Get the current position to which the image has advanced on reading this message.
     *
     * @return the current position to which the image has advanced on reading this message.
     */
    public final long position()
    {
        final int resultingOffset = BitUtil.align(termOffset() + frameLength(), FRAME_ALIGNMENT);
        return computePosition(termId(), resultingOffset, positionBitsToShift, initialTermId);
    }

    /**
     * Get the initial term id this stream started at.
     *
     * @return the initial term id this stream started at.
     */
    public final int initialTermId()
    {
        return initialTermId;
    }

    /**
     * Set the offset at which the header begins in the log.
     *
     * @param offset at which the header begins in the log.
     */
    public final void offset(final int offset)
    {
        this.offset = offset;
    }

    /**
     * The offset at which the frame begins.
     *
     * @return offset at which the frame begins.
     */
    public final int offset()
    {
        return offset;
    }

    /**
     * The {@link org.agrona.DirectBuffer} containing the header.
     *
     * @return {@link org.agrona.DirectBuffer} containing the header.
     */
    public final DirectBuffer buffer()
    {
        return buffer;
    }

    /**
     * The {@link org.agrona.DirectBuffer} containing the header.
     *
     * @param buffer {@link org.agrona.DirectBuffer} containing the header.
     */
    public final void buffer(final DirectBuffer buffer)
    {
        if (buffer != this.buffer)
        {
            this.buffer = buffer;
        }
    }

    /**
     * The total length of the frame including the header.
     *
     * @return the total length of the frame including the header.
     */
    public int frameLength()
    {
        return buffer.getInt(offset, LITTLE_ENDIAN);
    }

    /**
     * The session ID to which the frame belongs.
     *
     * @return the session ID to which the frame belongs.
     */
    public final int sessionId()
    {
        return buffer.getInt(offset + SESSION_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * The stream ID to which the frame belongs.
     *
     * @return the stream ID to which the frame belongs.
     */
    public final int streamId()
    {
        return buffer.getInt(offset + STREAM_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * The term ID to which the frame belongs.
     *
     * @return the term ID to which the frame belongs.
     */
    public final int termId()
    {
        return buffer.getInt(offset + TERM_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * The offset in the term at which the frame begins. This will be the same as {@link #offset()}
     *
     * @return the offset in the term at which the frame begins.
     */
    public int termOffset()
    {
        return offset;
    }

    /**
     * The type of the the frame which should always be {@link DataHeaderFlyweight#HDR_TYPE_DATA}
     *
     * @return type of the the frame which should always be {@link DataHeaderFlyweight#HDR_TYPE_DATA}
     */
    public final int type()
    {
        return buffer.getShort(offset + TYPE_FIELD_OFFSET, LITTLE_ENDIAN) & 0xFFFF;
    }

    /**
     * The flags for this frame. Valid flags are {@link DataHeaderFlyweight#BEGIN_FLAG}
     * and {@link DataHeaderFlyweight#END_FLAG}. A convenience flag {@link DataHeaderFlyweight#BEGIN_AND_END_FLAGS}
     * can be used for both flags.
     *
     * @return the flags for this frame.
     */
    public byte flags()
    {
        return buffer.getByte(offset + FLAGS_FIELD_OFFSET);
    }

    /**
     * Get the value stored in the reserve space at the end of a data frame header.
     * <p>
     * Note: The value is in {@link ByteOrder#LITTLE_ENDIAN} format.
     *
     * @return the value stored in the reserve space at the end of a data frame header.
     * @see DataHeaderFlyweight
     */
    public long reservedValue()
    {
        return buffer.getLong(offset + RESERVED_VALUE_OFFSET, LITTLE_ENDIAN);
    }
}


/*
 * 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 subscription.
 * <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
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                    Command Correlation ID                     |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                 Registration Correlation ID                   |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                         Stream Id                             |
 *  +---------------------------------------------------------------+
 *  |                       Channel Length                          |
 *  +---------------------------------------------------------------+
 *  |                       Channel (ASCII)                        ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 * </pre>
 */
public class SubscriptionMessageFlyweight extends CorrelatedMessageFlyweight
{
    private static final int REGISTRATION_CORRELATION_ID_OFFSET = CORRELATION_ID_FIELD_OFFSET + SIZE_OF_LONG;
    private static final int STREAM_ID_OFFSET = REGISTRATION_CORRELATION_ID_OFFSET + SIZE_OF_LONG;
    private static final int CHANNEL_OFFSET = STREAM_ID_OFFSET + SIZE_OF_INT;

    private int lengthOfChannel;

    /**
     * return correlation id used in registration field
     *
     * @return correlation id field
     */
    public long registrationCorrelationId()
    {
        return buffer.getLong(offset + REGISTRATION_CORRELATION_ID_OFFSET);
    }

    /**
     * set registration correlation id field
     *
     * @param correlationId field value
     * @return flyweight
     */
    public SubscriptionMessageFlyweight registrationCorrelationId(final long correlationId)
    {
        buffer.putLong(offset + REGISTRATION_CORRELATION_ID_OFFSET, correlationId);

        return this;
    }

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

    /**
     * Set the stream id
     *
     * @param streamId the channel id
     * @return flyweight
     */
    public SubscriptionMessageFlyweight streamId(final int streamId)
    {
        buffer.putInt(offset + STREAM_ID_OFFSET, streamId);

        return this;
    }

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

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

        return this;
    }

    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.driver.buffer;

import io.aeron.exceptions.AeronException;
import io.aeron.logbuffer.LogBufferDescriptor;
import org.agrona.ErrorHandler;
import org.agrona.IoUtil;
import org.agrona.LangUtil;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;

import static io.aeron.logbuffer.LogBufferDescriptor.TERM_MAX_LENGTH;

/**
 * Factory for creating {@link RawLog}s in the source publications or publication images directories as appropriate.
 */
public class FileStoreLogFactory implements LogFactory
{
    private static final String PUBLICATIONS = "publications";
    private static final String IMAGES = "images";

    private final long lowStorageWarningThreshold;
    private final int filePageSize;
    private final boolean checkStorage;
    private final ErrorHandler errorHandler;
    private final File publicationsDir;
    private final File imagesDir;
    private final FileStore fileStore;

    public FileStoreLogFactory(
        final String dataDirectoryName,
        final int filePageSize,
        final boolean checkStorage,
        final long lowStorageWarningThreshold,
        final ErrorHandler errorHandler)
    {
        this.filePageSize = filePageSize;
        this.lowStorageWarningThreshold = lowStorageWarningThreshold;
        this.checkStorage = checkStorage;
        this.errorHandler = errorHandler;

        final File dataDir = new File(dataDirectoryName);

        publicationsDir = new File(dataDir, PUBLICATIONS);
        imagesDir = new File(dataDir, IMAGES);

        IoUtil.ensureDirectoryExists(publicationsDir, PUBLICATIONS);
        IoUtil.ensureDirectoryExists(imagesDir, IMAGES);

        FileStore fs = null;
        try
        {
            if (checkStorage)
            {
                fs = Files.getFileStore(dataDir.toPath());
            }
        }
        catch (final IOException ex)
        {
            LangUtil.rethrowUnchecked(ex);
        }

        fileStore = fs;
    }

    /**
     * Create new {@link RawLog} in the publications directory for the supplied triplet.
     *
     * @param channel          address on the media to send to.
     * @param sessionId        under which transmissions are made.
     * @param streamId         within the channel address to separate message flows.
     * @param correlationId    to use to distinguish this publication
     * @param termBufferLength length of each term
     * @param useSparseFiles   for the log buffer.
     * @return the newly allocated {@link RawLog}
     */
    public RawLog newPublication(
        final String channel,
        final int sessionId,
        final int streamId,
        final long correlationId,
        final int termBufferLength,
        final boolean useSparseFiles)
    {
        return newInstance(
            publicationsDir, channel, sessionId, streamId, correlationId, termBufferLength, useSparseFiles);
    }

    /**
     * Create new {@link RawLog} in the rebuilt publication images directory for the supplied triplet.
     *
     * @param channel          address on the media to listened to.
     * @param sessionId        under which transmissions are made.
     * @param streamId         within the channel address to separate message flows.
     * @param correlationId    to use to distinguish this connection
     * @param termBufferLength to use for the log buffer
     * @param useSparseFiles   for the log buffer.
     * @return the newly allocated {@link RawLog}
     */
    public RawLog newImage(
        final String channel,
        final int sessionId,
        final int streamId,
        final long correlationId,
        final int termBufferLength,
        final boolean useSparseFiles)
    {
        return newInstance(imagesDir, channel, sessionId, streamId, correlationId, termBufferLength, useSparseFiles);
    }

    private RawLog newInstance(
        final File rootDir,
        final String channel,
        final int sessionId,
        final int streamId,
        final long correlationId,
        final int termBufferLength,
        final boolean useSparseFiles)
    {
        validateTermBufferLength(termBufferLength);
        checkStorage(termBufferLength);

        final File location = streamLocation(rootDir, channel, sessionId, streamId, correlationId);

        return new MappedRawLog(location, useSparseFiles, termBufferLength, filePageSize, errorHandler);
    }

    private void checkStorage(final int termBufferLength)
    {
        if (checkStorage)
        {
            final long usableSpace = getUsableSpace();
            final long logLength = LogBufferDescriptor.computeLogLength(termBufferLength, filePageSize);

            if (usableSpace <= lowStorageWarningThreshold)
            {
                System.out.format("Warning: space is running low in %s threshold=%,d usable=%,d%n",
                    fileStore, lowStorageWarningThreshold, usableSpace);
            }

            if (usableSpace < logLength)
            {
                throw new AeronException(
                    "insufficient usable storage for new log of length=" + logLength + " in " + fileStore);
            }
        }
    }

    private long getUsableSpace()
    {
        long usableSpace = 0;

        try
        {
            usableSpace = fileStore.getUsableSpace();
        }
        catch (final IOException ex)
        {
            LangUtil.rethrowUnchecked(ex);
        }

        return usableSpace;
    }

    private void validateTermBufferLength(final int termBufferLength)
    {
        if (termBufferLength < 0 || termBufferLength > TERM_MAX_LENGTH)
        {
            throw new IllegalArgumentException(
                "invalid buffer length: " + termBufferLength + " max is " + TERM_MAX_LENGTH);
        }
    }

    private static File streamLocation(
        final File rootDir,
        final String channel,
        final int sessionId,
        final int streamId,
        final long correlationId)
    {
        final String fileName = channel + '-' +
            Integer.toHexString(sessionId) + '-' +
            Integer.toHexString(streamId) + '-' +
            Long.toHexString(correlationId) + ".logbuffer";

        return new File(rootDir, fileName);
    }
}


/*
 * 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 org.agrona.BitUtil;
import org.agrona.DirectBuffer;
import org.agrona.MutableDirectBuffer;
import org.agrona.concurrent.status.CountersReader;

import static org.agrona.BitUtil.SIZE_OF_INT;
import static org.agrona.concurrent.status.CountersReader.*;

/**
 * Counter representing the heartbeat timestamp from a service.
 *

 * 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
 *  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *  |                         Service ID                            |
 *  +---------------------------------------------------------------+
 *

 */
public class ServiceHeartbeat
{
    /**
     * Type id of a service heartbeat counter.
     */
    public static final int SERVICE_HEARTBEAT_TYPE_ID = 206;

    public static final int SERVICE_ID_OFFSET = 0;

    /**
     * Human readable name for the counter.
     */
    public static final String NAME = "service-heartbeat: serviceId=";

    public static final int KEY_LENGTH = SIZE_OF_INT;

    /**
     * Allocate a counter to represent the heartbeat of a clustered service.
     *
     * @param aeron           to allocate the counter.
     * @param tempBuffer      to use for building the key and label without allocation.
     * @param serviceId       of the service heartbeat.
     * @return the {@link Counter} for the commit position.
     */
    public static Counter allocate(
        final Aeron aeron,
        final MutableDirectBuffer tempBuffer,
        final int serviceId)
    {
        tempBuffer.putInt(SERVICE_ID_OFFSET, serviceId);

        final int labelOffset = BitUtil.align(KEY_LENGTH, SIZE_OF_INT);
        int labelLength = 0;
        labelLength += tempBuffer.putStringWithoutLengthAscii(labelOffset + labelLength, NAME);
        labelLength += tempBuffer.putIntAscii(labelOffset + labelLength, serviceId);

        return aeron.addCounter(
            SERVICE_HEARTBEAT_TYPE_ID, tempBuffer, 0, KEY_LENGTH, tempBuffer, labelOffset, labelLength);
    }

    /**
     * Find the active counter id for heartbeat of a given service id.
     *
     * @param counters  to search within.
     * @param serviceId to search for.
     * @return the counter id if found otherwise {@link CountersReader#NULL_COUNTER_ID}.
     */
    public static int findCounterId(final CountersReader counters, final int serviceId)
    {
        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) == SERVICE_HEARTBEAT_TYPE_ID &&
                    buffer.getInt(recordOffset + KEY_OFFSET + SERVICE_ID_OFFSET) == serviceId)
                {
                    return i;
                }
            }
        }

        return NULL_COUNTER_ID;
    }
}


/*
 * 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.samples;

import static io.aeron.driver.status.PerImageIndicator.PER_IMAGE_TYPE_ID;
import static io.aeron.driver.status.PublisherLimit.PUBLISHER_LIMIT_TYPE_ID;
import static io.aeron.driver.status.PublisherPos.PUBLISHER_POS_TYPE_ID;
import static io.aeron.driver.status.ReceiverPos.RECEIVER_POS_TYPE_ID;
import static io.aeron.driver.status.SenderLimit.SENDER_LIMIT_TYPE_ID;
import static io.aeron.driver.status.StreamCounter.CHANNEL_OFFSET;
import static io.aeron.driver.status.StreamCounter.REGISTRATION_ID_OFFSET;
import static io.aeron.driver.status.StreamCounter.SESSION_ID_OFFSET;
import static io.aeron.driver.status.StreamCounter.STREAM_ID_OFFSET;

import io.aeron.driver.status.PublisherLimit;
import io.aeron.driver.status.PublisherPos;
import io.aeron.driver.status.ReceiverHwm;
import io.aeron.driver.status.ReceiverPos;
import io.aeron.driver.status.SenderLimit;
import io.aeron.driver.status.SenderPos;
import io.aeron.driver.status.SubscriberPos;

import java.io.PrintStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;

import org.agrona.concurrent.status.CountersReader;

/**
 * Tool for taking a snapshot of Aeron streams backlog information and some explanation for the
 * {@link StreamStat} counters.
 *

 * Each stream managed by the {@link io.aeron.driver.MediaDriver} will be sampled and printed out on {@link System#out}.
 */
public class BacklogStat
{
    private final CountersReader counters;

    public static void main(final String[] args)
    {
        final CountersReader counters = SamplesUtil.mapCounters();
        final BacklogStat backlogStat = new BacklogStat(counters);

        backlogStat.print(System.out);
    }

    public BacklogStat(final CountersReader counters)
    {
        this.counters = counters;
    }

    /**
     * Take a snapshot of all the backlog information and group by stream.
     *
     * @return a snapshot of all the backlog information and group by stream.
     */
    public Map<StreamCompositeKey, StreamBacklog> snapshot()
    {
        final Map<StreamCompositeKey, StreamBacklog> streams = new HashMap<>();

        counters.forEach(
            (counterId, typeId, keyBuffer, label) ->
            {
                if ((typeId >= PUBLISHER_LIMIT_TYPE_ID && typeId <= RECEIVER_POS_TYPE_ID) ||
                    typeId == SENDER_LIMIT_TYPE_ID || typeId == PER_IMAGE_TYPE_ID || typeId == PUBLISHER_POS_TYPE_ID)
                {
                    final StreamCompositeKey key = new StreamCompositeKey(
                        keyBuffer.getInt(SESSION_ID_OFFSET),
                        keyBuffer.getInt(STREAM_ID_OFFSET),
                        keyBuffer.getStringAscii(CHANNEL_OFFSET));

                    final StreamBacklog streamBacklog = streams.computeIfAbsent(key, (ignore) -> new StreamBacklog());
                    final long registrationId = keyBuffer.getLong(REGISTRATION_ID_OFFSET);
                    final long value = counters.getCounterValue(counterId);
                    switch (typeId)
                    {
                        case PublisherLimit.PUBLISHER_LIMIT_TYPE_ID:
                            streamBacklog.createPublisherIfAbsent().registrationId(registrationId);
                            streamBacklog.createPublisherIfAbsent().limit(value);
                            break;

                        case PublisherPos.PUBLISHER_POS_TYPE_ID:
                            streamBacklog.createPublisherIfAbsent().registrationId(registrationId);
                            streamBacklog.createPublisherIfAbsent().position(value);
                            break;

                        case SenderPos.SENDER_POSITION_TYPE_ID:
                            streamBacklog.createSenderIfAbsent().registrationId(registrationId);
                            streamBacklog.createSenderIfAbsent().position(value);
                            break;

                        case SenderLimit.SENDER_LIMIT_TYPE_ID:
                            streamBacklog.createSenderIfAbsent().registrationId(registrationId);
                            streamBacklog.createSenderIfAbsent().limit(value);
                            break;

                        case ReceiverHwm.RECEIVER_HWM_TYPE_ID:
                            streamBacklog.createReceiverIfAbsent().registrationId(registrationId);
                            streamBacklog.createReceiverIfAbsent().highWaterMark(value);
                            break;

                        case ReceiverPos.RECEIVER_POS_TYPE_ID:
                            streamBacklog.createReceiverIfAbsent().registrationId(registrationId);
                            streamBacklog.createReceiverIfAbsent().position(value);
                            break;

                        case SubscriberPos.SUBSCRIBER_POSITION_TYPE_ID:
                            streamBacklog.subscriberBacklogs().put(registrationId, new Subscriber(value));
                            break;
                    }
                }
            });

        return streams;
    }

    /**
     * Print a snapshot of the stream backlog with some explanation to a {@link PrintStream}.
     *

     * Each stream will be printed in its own section.
     *
     * @param out to which the stream backlog will be written.
     */
    public void print(final PrintStream out)
    {
        final StringBuilder builder = new StringBuilder();

        for (final Map.Entry<StreamCompositeKey, StreamBacklog> entry : snapshot().entrySet())
        {
            builder.setLength(0);
            final StreamCompositeKey key = entry.getKey();

            builder
                .append("sessionId=").append(key.sessionId())
                .append(" streamId=").append(key.streamId())
                .append(" channel=").append(key.channel())
                .append(" : ");

            final StreamBacklog streamBacklog = entry.getValue();
            if (streamBacklog.publisher() != null)
            {
                builder
                    .append("\n┌─for publisher ")
                    .append(streamBacklog.publisher().registrationId())
                    .append(" the last sampled position is ")
                    .append(streamBacklog.publisher().position())
                    .append(" (~")
                    .append(streamBacklog.publisher().remainingWindow())
                    .append(" bytes before back-pressure)");

                final Sender sender = streamBacklog.sender();
                if (sender != null)
                {
                    final long senderBacklog = sender.backlog(streamBacklog.publisher().position());

                    builder.append("\n└─sender ").append(sender.registrationId());

                    if (senderBacklog >= 0)
                    {
                        builder.append(" has to send ").append(senderBacklog).append(" bytes");
                    }
                    else
                    {
                        builder.append(" is at position ").append(sender.position());
                    }

                    builder.append(" (").append(sender.window()).append(" bytes remaining in the sender window)");
                }
                else
                {
                    builder.append("\n└─no sender yet...");
                }
            }

            if (streamBacklog.receiver() != null)
            {
                builder
                    .append("\n┌─receiver ")
                    .append(streamBacklog.receiver().registrationId())
                    .append(" is at position ")
                    .append(streamBacklog.receiver().position());

                final Iterator<Map.Entry<Long, Subscriber>> subscriberIterator =
                    streamBacklog.subscriberBacklogs().entrySet().iterator();

                while (subscriberIterator.hasNext())
                {
                    final Map.Entry<Long, Subscriber> subscriber = subscriberIterator.next();
                    builder
                        .append(subscriberIterator.hasNext() ? "\n├" : "\n└")
                        .append("─subscriber ")
                        .append(subscriber.getKey())
                        .append(" has ")
                        .append(subscriber.getValue().backlog(streamBacklog.receiver().highWaterMark()))
                        .append(" backlog bytes");
                }
            }

            builder.append('\n');
            out.println(builder);
        }
    }

    /**
     * Composite key which identifies an Aeron stream of messages.
     */
    public static class StreamCompositeKey
    {
        private final int sessionId;
        private final int streamId;
        private final String channel;

        public StreamCompositeKey(final int sessionId, final int streamId, final String channel)
        {
            Objects.requireNonNull(channel, "channel cannot be null");

            this.sessionId = sessionId;
            this.streamId = streamId;
            this.channel = channel;
        }

        public int sessionId()
        {
            return sessionId;
        }

        public int streamId()
        {
            return streamId;
        }

        public String channel()
        {
            return channel;
        }

        public boolean equals(final Object o)
        {
            if (this == o)
            {
                return true;
            }

            if (!(o instanceof StreamCompositeKey))
            {
                return false;
            }

            final StreamCompositeKey that = (StreamCompositeKey)o;

            return this.sessionId == that.sessionId &&
                this.streamId == that.streamId &&
                this.channel.equals(that.channel);
        }

        public int hashCode()
        {
            int result = sessionId;
            result = 31 * result + streamId;
            result = 31 * result + channel.hashCode();

            return result;
        }

        public String toString()
        {
            return "StreamCompositeKey{" +
                "sessionId=" + sessionId +
                ", streamId=" + streamId +
                ", channel='" + channel + '\'' +
                '}';
        }
    }

    /**
     * Represents the backlog information for a particular stream of messages.
     */
    public static class StreamBacklog
    {
        private Publisher publisher;
        private Sender sender;
        private Receiver receiver;
        private final SortedMap<Long, Subscriber> subscriberBacklogs = new TreeMap<>();

        Publisher publisher()
        {
            return publisher;
        }

        Sender sender()
        {
            return sender;
        }

        Receiver receiver()
        {
            return receiver;
        }

        Map<Long, Subscriber> subscriberBacklogs()
        {
            return subscriberBacklogs;
        }

        Publisher createPublisherIfAbsent()
        {
            return publisher == null ? publisher = new Publisher() : publisher;
        }

        Sender createSenderIfAbsent()
        {
            return sender == null ? sender = new Sender() : sender;
        }

        Receiver createReceiverIfAbsent()
        {
            return receiver == null ? receiver = new Receiver() : receiver;
        }
    }

    static class AeronEntity
    {
        private long registrationId;

        long registrationId()
        {
            return registrationId;
        }

        void registrationId(final long registrationId)
        {
            this.registrationId = registrationId;
        }
    }

    static class Publisher extends AeronEntity
    {
        private long limit;
        private long position;

        void limit(final long limit)
        {
            this.limit = limit;
        }

        void position(final long position)
        {
            this.position = position;
        }

        long position()
        {
            return position;
        }

        long remainingWindow()
        {
            return limit - position;
        }
    }

    static class Sender extends AeronEntity
    {
        private long position;
        private long limit;

        void position(final long publisherPosition)
        {
            this.position = publisherPosition;
        }

        long position()
        {
            return position;
        }

        void limit(final long limit)
        {
            this.limit = limit;
        }

        long backlog(final long publisherPosition)
        {
            return publisherPosition - position;
        }

        long window()
        {
            return limit - position;
        }
    }

    static class Receiver extends AeronEntity
    {
        private long highWaterMark;
        private long position;

        void highWaterMark(final long highWaterMark)
        {
            this.highWaterMark = highWaterMark;
        }

        long highWaterMark()
        {
            return highWaterMark;
        }

        void position(final long position)
        {
            this.position = position;
        }

        long position()
        {
            return position;
        }
    }

    static class Subscriber
    {
        private final long position;

        Subscriber(final long position)
        {
            this.position = position;
        }

        long backlog(final long receiverHwm)
        {
            return receiverHwm - position;
        }
    }
}


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

import io.aeron.Aeron;
import io.aeron.logbuffer.TermGapScanner;
import org.agrona.concurrent.UnsafeBuffer;

import static io.aeron.logbuffer.TermGapScanner.scanForGap;

/**
 * Detecting and handling of gaps in a message stream.
 *

 * Each detector only notifies a single run of a gap in a message stream.
 */
public class LossDetector implements TermGapScanner.GapHandler
{
    private long deadlineNs = Aeron.NULL_VALUE;

    private int scannedTermId;
    private int scannedTermOffset = -1;
    private int scannedLength;

    private int activeTermId;
    private int activeTermOffset = -1;
    private int activeLength;

    private final FeedbackDelayGenerator delayGenerator;
    private final LossHandler lossHandler;

    /**
     * Create a loss detector for a channel.
     *
     * @param delayGenerator to use for delay determination
     * @param lossHandler    to call when signalling a gap
     */
    public LossDetector(final FeedbackDelayGenerator delayGenerator, final LossHandler lossHandler)
    {
        this.delayGenerator = delayGenerator;
        this.lossHandler = lossHandler;
    }

    /**
     * Scan for gaps and handle received data.
     *

     * The handler keeps track from scan to scan what is a gap and what must have been repaired.
     *
     * @param termBuffer          to scan
     * @param rebuildPosition     to start scanning from
     * @param hwmPosition         to scan up to
     * @param nowNs               time in nanoseconds
     * @param termLengthMask      used for offset calculation
     * @param positionBitsToShift used for position calculation
     * @param initialTermId       used by the scanner
     * @return packed outcome of the scan.
     */
    public long scan(
        final UnsafeBuffer termBuffer,
        final long rebuildPosition,
        final long hwmPosition,
        final long nowNs,
        final int termLengthMask,
        final int positionBitsToShift,
        final int initialTermId)
    {
        boolean lossFound = false;
        int rebuildOffset = (int)rebuildPosition & termLengthMask;

        if (rebuildPosition < hwmPosition)
        {
            final int rebuildTermCount = (int)(rebuildPosition >>> positionBitsToShift);
            final int hwmTermCount = (int)(hwmPosition >>> positionBitsToShift);

            final int rebuildTermId = initialTermId + rebuildTermCount;
            final int hwmTermOffset = (int)hwmPosition & termLengthMask;
            final int limitOffset = rebuildTermCount == hwmTermCount ? hwmTermOffset : termLengthMask + 1;

            rebuildOffset = scanForGap(termBuffer, rebuildTermId, rebuildOffset, limitOffset, this);
            if (rebuildOffset < limitOffset)
            {
                if (scannedTermOffset != activeTermOffset || scannedTermId != activeTermId)
                {
                    activateGap(nowNs);
                    lossFound = true;
                }

                checkTimerExpiry(nowNs);
            }
        }

        return pack(rebuildOffset, lossFound);
    }

    public void onGap(final int termId, final int offset, final int length)
    {
        scannedTermId = termId;
        scannedTermOffset = offset;
        scannedLength = length;
    }

    /**
     * Pack the values for workCount and rebuildOffset into a long for returning on the stack.
     *
     * @param rebuildOffset value to be packed.
     * @param lossFound     value to be packed.
     * @return a long with rebuildOffset and lossFound packed into it.
     */
    public static long pack(final int rebuildOffset, final boolean lossFound)
    {
        return ((long)rebuildOffset << 32) | (lossFound ? 1 : 0);
    }

    /**
     * Has loss been found in the scan?
     *
     * @param scanOutcome into which the fragments read value has been packed.
     * @return if loss has been found or not.
     */
    public static boolean lossFound(final long scanOutcome)
    {
        return ((int)scanOutcome) != 0;
    }

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

    private void activateGap(final long nowNs)
    {
        activeTermId = scannedTermId;
        activeTermOffset = scannedTermOffset;
        activeLength = scannedLength;

        if (delayGenerator.shouldFeedbackImmediately())
        {
            deadlineNs = nowNs;
        }
        else
        {
            deadlineNs = nowNs + delayGenerator.generateDelay();
        }
    }

    private void checkTimerExpiry(final long nowNs)
    {
        if (deadlineNs - nowNs <= 0)
        {
            lossHandler.onGapDetected(activeTermId, activeTermOffset, activeLength);
            deadlineNs = nowNs + delayGenerator.generateDelay();
        }
    }
}


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

import io.aeron.logbuffer.*;
import org.agrona.DirectBuffer;
import org.agrona.concurrent.UnsafeBuffer;
import org.agrona.concurrent.status.ReadablePosition;

import static io.aeron.logbuffer.LogBufferDescriptor.*;

/**
 * Aeron publisher API for sending messages to subscribers of a given channel and streamId pair. {@link Publication}s
 * are created via the {@link Aeron#addPublication(String, int)} method, and messages are sent via one of the
 * {@link #offer(DirectBuffer)} methods, or a {@link #tryClaim(int, BufferClaim)} and {@link BufferClaim#commit()}
 * method combination.
 *

 * The APIs used for try claim and offer are non-blocking and thread safe.
 *

 * Note: Instances are threadsafe and can be shared between publishing threads.
 *
 * @see Aeron#addPublication(String, int)
 * @see BufferClaim
 */
public class ConcurrentPublication extends Publication
{
    private final TermAppender[] termAppenders = new TermAppender[PARTITION_COUNT];

    ConcurrentPublication(
        final ClientConductor clientConductor,
        final String channel,
        final int streamId,
        final int sessionId,
        final ReadablePosition positionLimit,
        final int channelStatusId,
        final LogBuffers logBuffers,
        final long originalRegistrationId,
        final long registrationId)
    {
        super(
            clientConductor,
            channel,
            streamId,
            sessionId,
            positionLimit,
            channelStatusId,
            logBuffers,
            originalRegistrationId,
            registrationId);

        final UnsafeBuffer[] buffers = logBuffers.duplicateTermBuffers();

        for (int i = 0; i < PARTITION_COUNT; i++)
        {
            termAppenders[i] = new TermAppender(buffers[i], logMetaDataBuffer, i);
        }
    }

    public long availableWindow()
    {
        if (isClosed)
        {
            return CLOSED;
        }

        return positionLimit.getVolatile() - position();
    }

    /**
     * Non-blocking publish of a partial buffer containing a message.
     *
     * @param buffer                containing message.
     * @param offset                offset in the buffer at which the encoded message begins.
     * @param length                in bytes of the encoded message.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return The new stream position, otherwise a negative error value of {@link #NOT_CONNECTED},
     * {@link #BACK_PRESSURED}, {@link #ADMIN_ACTION}, {@link #CLOSED}, or {@link #MAX_POSITION_EXCEEDED}.
     */
    public long offer(
        final DirectBuffer buffer,
        final int offset,
        final int length,
        final ReservedValueSupplier reservedValueSupplier)
    {
        long newPosition = CLOSED;
        if (!isClosed)
        {
            final long limit = positionLimit.getVolatile();
            final int termCount = activeTermCount(logMetaDataBuffer);
            final TermAppender termAppender = termAppenders[indexByTermCount(termCount)];
            final long rawTail = termAppender.rawTailVolatile();
            final long termOffset = rawTail & 0xFFFF_FFFFL;
            final int termId = termId(rawTail);
            final long position = computeTermBeginPosition(termId, positionBitsToShift, initialTermId) + termOffset;

            if (termCount != (termId - initialTermId))
            {
                return ADMIN_ACTION;
            }

            if (position < limit)
            {
                final int resultingOffset;
                if (length <= maxPayloadLength)
                {
                    checkPositiveLength(length);
                    resultingOffset = termAppender.appendUnfragmentedMessage(
                        headerWriter, buffer, offset, length, reservedValueSupplier, termId);
                }
                else
                {
                    checkMaxMessageLength(length);
                    resultingOffset = termAppender.appendFragmentedMessage(
                        headerWriter, buffer, offset, length, maxPayloadLength, reservedValueSupplier, termId);
                }

                newPosition = newPosition(termCount, (int)termOffset, termId, position, resultingOffset);
            }
            else
            {
                newPosition = backPressureStatus(position, length);
            }
        }

        return newPosition;
    }

    /**
     * Non-blocking publish of a message composed of two parts, e.g. a header and encapsulated payload.
     *
     * @param bufferOne             containing the first part of the message.
     * @param offsetOne             at which the first part of the message begins.
     * @param lengthOne             of the first part of the message.
     * @param bufferTwo             containing the second part of the message.
     * @param offsetTwo             at which the second part of the message begins.
     * @param lengthTwo             of the second part of the message.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return The new stream position, otherwise a negative error value of {@link #NOT_CONNECTED},
     * {@link #BACK_PRESSURED}, {@link #ADMIN_ACTION}, {@link #CLOSED}, or {@link #MAX_POSITION_EXCEEDED}.
     */
    public long offer(
        final DirectBuffer bufferOne,
        final int offsetOne,
        final int lengthOne,
        final DirectBuffer bufferTwo,
        final int offsetTwo,
        final int lengthTwo,
        final ReservedValueSupplier reservedValueSupplier)
    {
        long newPosition = CLOSED;
        if (!isClosed)
        {
            final long limit = positionLimit.getVolatile();
            final int termCount = activeTermCount(logMetaDataBuffer);
            final TermAppender termAppender = termAppenders[indexByTermCount(termCount)];
            final long rawTail = termAppender.rawTailVolatile();
            final long termOffset = rawTail & 0xFFFF_FFFFL;
            final int termId = termId(rawTail);
            final long position = computeTermBeginPosition(termId, positionBitsToShift, initialTermId) + termOffset;

            if (termCount != (termId - initialTermId))
            {
                return ADMIN_ACTION;
            }

            final int length = validateAndComputeLength(lengthOne, lengthTwo);
            if (position < limit)
            {
                final int resultingOffset;
                if (length <= maxPayloadLength)
                {
                    resultingOffset = termAppender.appendUnfragmentedMessage(
                        headerWriter,
                        bufferOne, offsetOne, lengthOne,
                        bufferTwo, offsetTwo, lengthTwo,
                        reservedValueSupplier,
                        termId);
                }
                else
                {
                    checkMaxMessageLength(length);
                    resultingOffset = termAppender.appendFragmentedMessage(
                        headerWriter,
                        bufferOne, offsetOne, lengthOne,
                        bufferTwo, offsetTwo, lengthTwo,
                        maxPayloadLength,
                        reservedValueSupplier,
                        termId);
                }

                newPosition = newPosition(termCount, (int)termOffset, termId, position, resultingOffset);
            }
            else
            {
                newPosition = backPressureStatus(position, length);
            }
        }

        return newPosition;
    }

    /**
     * Non-blocking publish by gathering buffer vectors into a message.
     *
     * @param vectors               which make up the message.
     * @param reservedValueSupplier {@link ReservedValueSupplier} for the frame.
     * @return The new stream position, otherwise a negative error value of {@link #NOT_CONNECTED},
     * {@link #BACK_PRESSURED}, {@link #ADMIN_ACTION}, {@link #CLOSED}, or {@link #MAX_POSITION_EXCEEDED}.
     */
    public long offer(final DirectBufferVector[] vectors, final ReservedValueSupplier reservedValueSupplier)
    {
        final int length = DirectBufferVector.validateAndComputeLength(vectors);
        long newPosition = CLOSED;

        if (!isClosed)
        {
            final long limit = positionLimit.getVolatile();
            final int termCount = activeTermCount(logMetaDataBuffer);
            final TermAppender termAppender = termAppenders[indexByTermCount(termCount)];
            final long rawTail = termAppender.rawTailVolatile();
            final long termOffset = rawTail & 0xFFFF_FFFFL;
            final int termId = termId(rawTail);
            final long position = computeTermBeginPosition(termId, positionBitsToShift, initialTermId) + termOffset;

            if (termCount != (termId - initialTermId))
            {
                return ADMIN_ACTION;
            }

            if (position < limit)
            {
                final int resultingOffset;
                if (length <= maxPayloadLength)
                {
                    resultingOffset = termAppender.appendUnfragmentedMessage(
                        headerWriter, vectors, length, reservedValueSupplier, termId);
                }
                else
                {
                    checkMaxMessageLength(length);
                    resultingOffset = termAppender.appendFragmentedMessage(
                        headerWriter, vectors, length, maxPayloadLength, reservedValueSupplier, termId);
                }

                newPosition = newPosition(termCount, (int)termOffset, termId, position, resultingOffset);
            }
            else
            {
                newPosition = backPressureStatus(position, length);
            }
        }

        return newPosition;
    }

    /**
     * Try to claim a range in the publication log into which a message can be written with zero copy semantics.
     * Once the message has been written then {@link BufferClaim#commit()} should be called thus making it available.
     *

     * Note: This method can only be used for message lengths less than MTU length minus header.
     * If the claim is held for more than the aeron.publication.unblock.timeout system property then the driver will
     * assume the publication thread is dead and will unblock the claim thus allowing other threads to make progress or
     * to reach end-of-stream (EOS).
     *
{@code
     *     final BufferClaim bufferClaim = new BufferClaim(); // Can be stored and reused to avoid allocation
     *
     *     if (publication.tryClaim(messageLength, bufferClaim) > 0L)
     *     {
     *         try
     *         {
     *              final MutableDirectBuffer buffer = bufferClaim.buffer();
     *              final int offset = bufferClaim.offset();
     *
     *              // Work with buffer directly or wrap with a flyweight
     *         }
     *         finally
     *         {
     *             bufferClaim.commit();
     *         }
     *     }
     * }

     *
     * @param length      of the range to claim, in bytes..
     * @param bufferClaim to be populated if the claim succeeds.
     * @return The new stream position, otherwise a negative error value of {@link #NOT_CONNECTED},
     * {@link #BACK_PRESSURED}, {@link #ADMIN_ACTION}, {@link #CLOSED}, or {@link #MAX_POSITION_EXCEEDED}.
     * @throws IllegalArgumentException if the length is greater than {@link #maxPayloadLength()} within an MTU.
     * @see BufferClaim#commit()
     * @see BufferClaim#abort()
     */
    public long tryClaim(final int length, final BufferClaim bufferClaim)
    {
        checkPayloadLength(length);
        long newPosition = CLOSED;

        if (!isClosed)
        {
            final long limit = positionLimit.getVolatile();
            final int termCount = activeTermCount(logMetaDataBuffer);
            final TermAppender termAppender = termAppenders[indexByTermCount(termCount)];
            final long rawTail = termAppender.rawTailVolatile();
            final long termOffset = rawTail & 0xFFFF_FFFFL;
            final int termId = termId(rawTail);
            final long position = computeTermBeginPosition(termId, positionBitsToShift, initialTermId) + termOffset;

            if (termCount != (termId - initialTermId))
            {
                return ADMIN_ACTION;
            }

            if (position < limit)
            {
                final int resultingOffset = termAppender.claim(headerWriter, length, bufferClaim, termId);
                newPosition = newPosition(termCount, (int)termOffset, termId, position, resultingOffset);
            }
            else
            {
                newPosition = backPressureStatus(position, length);
            }
        }

        return newPosition;
    }

    private long newPosition(
        final int termCount, final int termOffset, final int termId, final long position, final int resultingOffset)
    {
        if (resultingOffset > 0)
        {
            return (position - termOffset) + resultingOffset;
        }

        if ((position + termOffset) > maxPossiblePosition)
        {
            return MAX_POSITION_EXCEEDED;
        }

        rotateLog(logMetaDataBuffer, termCount, termId);

        return ADMIN_ACTION;
    }
}


/*
 * 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.FRAME_ALIGNMENT;
import static io.aeron.logbuffer.FrameDescriptor.frameLengthVolatile;
import static io.aeron.logbuffer.FrameDescriptor.isPaddingFrame;
import static org.agrona.BitUtil.align;

/**
 * Scan a term buffer for a block of message fragments including padding. The block must include complete fragments.
 */
public class TermBlockScanner
{
    /**
     * Scan a term buffer for a block of message fragments from an offset up to a limitOffset.
     *

     * A scan will terminate if a padding frame is encountered. If first frame in a scan is padding then a block
     * for the padding is notified. If the padding comes after the first frame in a scan then the scan terminates
     * at the offset the padding frame begins. Padding frames are delivered singularly in a block.
     *

     * Padding frames may be for a greater range than the limit offset but only the header needs to be valid so
     * relevant length of the frame is {@link io.aeron.protocol.DataHeaderFlyweight#HEADER_LENGTH}.
     *
     * @param termBuffer  to scan for message fragments.
     * @param termOffset  at which the scan should begin.
     * @param limitOffset at which the scan should stop.
     * @return the offset at which the scan terminated.
     */
    public static int scan(final UnsafeBuffer termBuffer, final int termOffset, final int limitOffset)
    {
        int offset = termOffset;

        while (offset < limitOffset)
        {
            final int frameLength = frameLengthVolatile(termBuffer, offset);
            if (frameLength <= 0)
            {
                break;
            }

            final int alignedFrameLength = align(frameLength, FRAME_ALIGNMENT);

            if (isPaddingFrame(termBuffer, offset))
            {
                if (termOffset == offset)
                {
                    offset += alignedFrameLength;
                }

                break;
            }

            if (offset + alignedFrameLength > limitOffset)
            {
                break;
            }

            offset += alignedFrameLength;
        }

        return offset;
    }
}