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

import io.aeron.Aeron;
import io.aeron.ControlledFragmentAssembler;
import io.aeron.Subscription;
import io.aeron.archive.codecs.*;
import io.aeron.logbuffer.ControlledFragmentHandler;
import io.aeron.logbuffer.Header;
import org.agrona.DirectBuffer;

/**
 * Encapsulate the polling and decoding of archive control protocol response messages.
 */
public class ControlResponsePoller implements ControlledFragmentHandler
{
    /**
     * Limit to apply when polling response messages.
     */
    public static final int FRAGMENT_LIMIT = 10;

    private final MessageHeaderDecoder messageHeaderDecoder = new MessageHeaderDecoder();
    private final ControlResponseDecoder controlResponseDecoder = new ControlResponseDecoder();

    private final Subscription subscription;
    private final ControlledFragmentAssembler fragmentAssembler = new ControlledFragmentAssembler(this);
    private long controlSessionId = Aeron.NULL_VALUE;
    private long correlationId = Aeron.NULL_VALUE;
    private long relevantId = Aeron.NULL_VALUE;
    private int templateId = Aeron.NULL_VALUE;
    private final int fragmentLimit;
    private ControlResponseCode code;
    private String errorMessage;
    private boolean pollComplete = false;

    /**
     * Create a poller for a given subscription to an archive for control response messages.
     *
     * @param subscription  to poll for new events.
     * @param fragmentLimit to apply when polling.
     */
    private ControlResponsePoller(final Subscription subscription, final int fragmentLimit)
    {
        this.subscription = subscription;
        this.fragmentLimit = fragmentLimit;
    }

    /**
     * Create a poller for a given subscription to an archive for control response messages with a default
     * fragment limit for polling as {@link #FRAGMENT_LIMIT}.
     *
     * @param subscription  to poll for new events.
     */
    public ControlResponsePoller(final Subscription subscription)
    {
        this(subscription, FRAGMENT_LIMIT);
    }

    /**
     * Get the {@link Subscription} used for polling responses.
     *
     * @return the {@link Subscription} used for polling responses.
     */
    public Subscription subscription()
    {
        return subscription;
    }

    /**
     * Poll for control response events.
     *
     * @return the number of fragments read during the operation. Zero if no events are available.
     */
    public int poll()
    {
        controlSessionId = -1;
        correlationId = -1;
        relevantId = -1;
        templateId = -1;
        errorMessage = null;
        pollComplete = false;

        return subscription.controlledPoll(fragmentAssembler, fragmentLimit);
    }

    /**
     * Control session id of the last polled message or {@link Aeron#NULL_VALUE} if poll returned nothing.
     *
     * @return control session id of the last polled message or {@link Aeron#NULL_VALUE} if poll returned nothing.
     */
    public long controlSessionId()
    {
        return controlSessionId;
    }

    /**
     * Correlation id of the last polled message or {@link Aeron#NULL_VALUE} if poll returned nothing.
     *
     * @return correlation id of the last polled message or {@link Aeron#NULL_VALUE} if poll returned nothing.
     */
    public long correlationId()
    {
        return correlationId;
    }

    /**
     * Get the relevant id returned with the response, e.g. replay session id.
     *
     * @return the relevant id returned with the response.
     */
    public long relevantId()
    {
        return relevantId;
    }

    /**
     * Has the last polling action received a complete message?
     *
     * @return true if the last polling action received a complete message?
     */
    public boolean isPollComplete()
    {
        return pollComplete;
    }

    /**
     * Get the template id of the last received message.
     *
     * @return the template id of the last received message.
     */
    public int templateId()
    {
        return templateId;
    }

    /**
     * Get the response code of the last response.
     *
     * @return the response code of the last response.
     */
    public ControlResponseCode code()
    {
        return code;
    }

    /**
     * Get the error message of the last response.
     *
     * @return the error message of the last response.
     */
    public String errorMessage()
    {
        return errorMessage;
    }

    public ControlledFragmentAssembler.Action onFragment(
        final DirectBuffer buffer, final int offset, final int length, final Header header)
    {
        if (pollComplete)
        {
            return Action.ABORT;
        }

        messageHeaderDecoder.wrap(buffer, offset);

        final int schemaId = messageHeaderDecoder.schemaId();
        if (schemaId != MessageHeaderDecoder.SCHEMA_ID)
        {
            throw new ArchiveException("expected schemaId=" + MessageHeaderDecoder.SCHEMA_ID + ", actual=" + schemaId);
        }

        templateId = messageHeaderDecoder.templateId();
        if (templateId == ControlResponseDecoder.TEMPLATE_ID)
        {
            controlResponseDecoder.wrap(
                buffer,
                offset + MessageHeaderEncoder.ENCODED_LENGTH,
                messageHeaderDecoder.blockLength(),
                messageHeaderDecoder.version());

            controlSessionId = controlResponseDecoder.controlSessionId();
            correlationId = controlResponseDecoder.correlationId();
            relevantId = controlResponseDecoder.relevantId();
            code = controlResponseDecoder.code();
            errorMessage = controlResponseDecoder.errorMessage();
            pollComplete = true;

            return Action.BREAK;
        }

        return Action.CONTINUE;
    }

    public String toString()
    {
        return "ControlResponsePoller{" +
            "controlSessionId=" + controlSessionId +
            ", correlationId=" + correlationId +
            ", relevantId=" + relevantId +
            ", templateId=" + templateId +
            ", code=" + code +
            ", errorMessage='" + errorMessage + '\'' +
            ", pollComplete=" + pollComplete +
            '}';
    }
}


/*
 * 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 java.io.File;
import java.nio.MappedByteBuffer;

import static org.agrona.IoUtil.mapNewFile;

public class LossReportUtil
{
    /**
     * Name of the error report file in the Aeron directory.
     */
    public static final String LOSS_REPORT_FILE_NAME = "loss-report.dat";

    /**
     * Create a new {@link File} object for the loss report.
     *
     * @param aeronDirectoryName in which the loss report should exist.
     * @return the new {@link File} for the loss report.
     */
    public static File file(final String aeronDirectoryName)
    {
        return new File(aeronDirectoryName, LOSS_REPORT_FILE_NAME);
    }

    /**
     * Map a new loss report in the Aeron directory for a given length.
     *
     * @param aeronDirectoryName in which to create the file.
     * @param reportFileLength   for the file.
     * @return the newly mapped buffer for the file.
     */
    public static MappedByteBuffer mapLossReport(final String aeronDirectoryName, final int reportFileLength)
    {
        return mapNewFile(file(aeronDirectoryName), reportFileLength, false);
    }
}


/*
 * 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.ControlledFragmentHandler;
import io.aeron.logbuffer.Header;
import org.agrona.DirectBuffer;

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

/**
 * A {@link ControlledFragmentHandler} that sits in a chain-of-responsibility pattern that reassembles fragmented
 * messages so that the next handler in the chain only sees whole messages. This is for a single session on an
 * {@link Image} and not for multiple session {@link Image}s in a {@link Subscription}.
 *

 * Unfragmented messages are delegated without copy. Fragmented messages are copied to a temporary
 * buffer for reassembly before delegation.
 *

 * The {@link Header} passed to the delegate on assembling a message will be that of the last fragment.
 *
 * @see Image#controlledPoll(ControlledFragmentHandler, int)
 * @see Image#controlledPeek(long, ControlledFragmentHandler, long)
 */
public class ImageControlledFragmentAssembler implements ControlledFragmentHandler
{
    private final ControlledFragmentHandler delegate;
    private final BufferBuilder builder;

    /**
     * Construct an adapter to reassemble message fragments and delegate on whole messages.
     *
     * @param delegate onto which whole messages are forwarded.
     */
    public ImageControlledFragmentAssembler(final ControlledFragmentHandler delegate)
    {
        this(delegate, 0, false);
    }

    /**
     * Construct an adapter to reassemble message fragments and delegate on whole messages.
     *
     * @param delegate            onto which whole messages are forwarded.
     * @param initialBufferLength to be used for the session.
     */
    public ImageControlledFragmentAssembler(final ControlledFragmentHandler delegate, final int initialBufferLength)
    {
        this(delegate, initialBufferLength, false);
    }

    /**
     * Construct an adapter to reassemble message fragments and delegate on whole messages.
     *
     * @param delegate            onto which whole messages are forwarded.
     * @param initialBufferLength to be used for the session.
     * @param isDirectByteBuffer  is the underlying buffer to be a direct {@link java.nio.ByteBuffer}?
     */
    public ImageControlledFragmentAssembler(
        final ControlledFragmentHandler delegate, final int initialBufferLength, final boolean isDirectByteBuffer)
    {
        this.delegate = delegate;
        this.builder = new BufferBuilder(initialBufferLength, isDirectByteBuffer);
    }

    /**
     * Get the delegate unto which assembled messages are delegated.
     *
     * @return the delegate unto which assembled messages are delegated.
     */
    public ControlledFragmentHandler delegate()
    {
        return delegate;
    }

    /**
     * Get the {@link BufferBuilder} for resetting this assembler.
     *
     * @return the {@link BufferBuilder} for resetting this assembler.
     */
    BufferBuilder bufferBuilder()
    {
        return builder;
    }

    /**
     * The implementation of {@link ControlledFragmentHandler} that reassembles and forwards whole messages.
     *
     * @param buffer containing the data.
     * @param offset at which the data begins.
     * @param length of the data in bytes.
     * @param header representing the meta data for the data.
     * @return {@link io.aeron.logbuffer.ControlledFragmentHandler.Action} to be taken after processing fragment.
     */
    public Action onFragment(final DirectBuffer buffer, final int offset, final int length, final Header header)
    {
        final byte flags = header.flags();

        Action action = Action.CONTINUE;

        if ((flags & UNFRAGMENTED) == UNFRAGMENTED)
        {
            action = delegate.onFragment(buffer, offset, length, header);
        }
        else
        {
            if ((flags & BEGIN_FRAG_FLAG) == BEGIN_FRAG_FLAG)
            {
                builder.reset().append(buffer, offset, length);
            }
            else
            {
                final int limit = builder.limit();
                builder.append(buffer, offset, length);

                if ((flags & END_FRAG_FLAG) == END_FRAG_FLAG)
                {
                    final int msgLength = builder.limit();
                    action = delegate.onFragment(builder.buffer(), 0, msgLength, header);

                    if (Action.ABORT == action)
                    {
                        builder.limit(limit);
                    }
                    else
                    {
                        builder.reset();
                    }
                }
            }
        }

        return 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.samples;

import org.agrona.concurrent.status.CountersReader;

import java.io.PrintStream;
import java.util.*;

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

/**
 * Tool for taking a snapshot of Aeron streams and relevant position counters.
 *

 * Each stream managed by the {@link io.aeron.driver.MediaDriver} will be sampled and a line of text
 * output per stream with each of the position counters for that stream.
 *

 * Each counter has the format:
 * {@code :<registration-id>:<position value>}
 */
public class StreamStat
{
    private final CountersReader counters;

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

        streamStat.print(System.out);
    }

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

    /**
     * Take a snapshot of all the counters and group them by streams.
     *
     * @return a snapshot of all the counters and group them by streams.
     */
    public Map<StreamCompositeKey, List<StreamPosition>> snapshot()
    {
        final Map<StreamCompositeKey, List<StreamPosition>> 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 StreamPosition position = new StreamPosition(
                        keyBuffer.getLong(REGISTRATION_ID_OFFSET),
                        counters.getCounterValue(counterId),
                        typeId);

                    streams
                        .computeIfAbsent(key, (ignore) -> new ArrayList<>())
                        .add(position);
                }
            });

        return streams;
    }

    /**
     * Print a snapshot of the stream positions to a {@link PrintStream}.
     *

     * Each stream will be printed on its own line.
     *
     * @param out to which the stream snapshot will be written.
     * @return the number of streams printed.
     */
    public int print(final PrintStream out)
    {
        final Map<StreamCompositeKey, List<StreamPosition>> streams = snapshot();
        final StringBuilder builder = new StringBuilder();

        for (final Map.Entry<StreamCompositeKey, List<StreamPosition>> entry : streams.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(" :");

            for (final StreamPosition streamPosition : entry.getValue())
            {
                builder
                    .append(' ')
                    .append(labelName(streamPosition.typeId()))
                    .append(':').append(streamPosition.id())
                    .append(':').append(streamPosition.value());
            }

            out.println(builder);
        }

        return streams.size();
    }

    /**
     * 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 a position within a particular stream of messages.
     */
    public static class StreamPosition
    {
        private final long id;
        private final long value;
        private final int typeId;

        public StreamPosition(final long id, final long value, final int typeId)
        {
            this.id = id;
            this.value = value;
            this.typeId = typeId;
        }

        /**
         * The identifier for the registered entity, such as publication or subscription, to which the counter relates.
         *
         * @return the identifier for the registered entity to which the counter relates.
         */
        public long id()
        {
            return id;
        }

        /**
         * The value of the counter.
         *
         * @return the value of the counter.
         */
        public long value()
        {
            return value;
        }

        /**
         * The type category of the counter for the stream position.
         *
         * @return the type category of the counter for the stream position.
         */
        public int typeId()
        {
            return typeId;
        }

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

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

            final StreamPosition that = (StreamPosition)o;

            return this.id == that.id && this.value == that.value && this.typeId == that.typeId;

        }

        public int hashCode()
        {
            int result = (int)(id ^ (id >>> 32));
            result = 31 * result + (int)(value ^ (value >>> 32));
            result = 31 * result + typeId;

            return result;
        }

        public String toString()
        {
            return "StreamPosition{" +
                "id=" + id +
                ", value=" + value +
                ", typeId=" + typeId +
                '}';
        }
    }

}


/*
 * 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 java.util.concurrent.ThreadLocalRandom;

/**
 * Feedback delay used for NAKs as well as for some retransmission use cases.
 *

 * Generates delay based on Optimal Multicast Feedback

 * http://tools.ietf.org/html/rfc5401#page-13
 *

 * {@code maxBackoffT} is max interval for delay
 *

 * C version of the code:
 *
{@code
 * double RandomBackoff(double maxBackoffT, double groupSize)
 * {
 *     double lambda = log(groupSize) + 1;
 *     double x = UniformRand(lambda / maxBackoffT) + lambda / (maxBackoffT * (exp(lambda) - 1));
 *     return ((maxBackoffT / lambda) * log(x * (exp(lambda) - 1) * (maxBackoffT / lambda)));
 * }
 * }

 * where {@code UniformRand(x)} is uniform distribution from {@code 0..max}
 *

 * In this implementation's calculation:
 *

 *
the {@code groupSize} is a constant (could be configurable as system property)

 *
{@code maxBackoffT} is a constant (could be configurable as system property)

 *
{@code GRTT} is a constant (could be configurable as a system property)

 *

 *

 * {@code N} (the expected number of feedback messages per RTT) is:

 * {@code N = exp(1.2 * L / (2 * maxBackoffT / GRTT))}
 *

 * Assumptions:
 *

 * {@code maxBackoffT = K * GRTT (K >= 1)}
 *

 * Recommended {@code K}:
 *

 *
K = 4 for situations where responses come from multiple places (i.e. for NAKs, multiple retransmitters)

 *
K = 6 for situations where responses come from single places (i.e. for NAKs, source only retransmit)

 *

 */
public class OptimalMulticastDelayGenerator implements FeedbackDelayGenerator
{
    private final double randMax;
    private final double baseX;
    private final double constantT;
    private final double factorT;

    /**
     * Create new feedback delay generator based on estimates. Pre-calculating some parameters upfront.
     *

     *
     * @param maxBackoffT of the delay interval
     * @param groupSize   estimate
     */
    public OptimalMulticastDelayGenerator(final double maxBackoffT, final double groupSize)
    {
        final double lambda = Math.log(groupSize) + 1;

        this.randMax = lambda / maxBackoffT;
        this.baseX = lambda / (maxBackoffT * (Math.exp(lambda) - 1));
        this.constantT = maxBackoffT / lambda;
        this.factorT = (Math.exp(lambda) - 1) * (maxBackoffT / lambda);
    }

    /**
     * {@inheritDoc}
     */
    public long generateDelay()
    {
        return (long)generateNewOptimalDelay();
    }

    /**
     * {@inheritDoc}
     */
    public boolean shouldFeedbackImmediately()
    {
        return false;
    }

    /**
     * Generate a new randomized delay value in the units of {@code maxBackoffT}}.
     *
     * @return delay in units of {@code maxBackoffT}.
     */
    public double generateNewOptimalDelay()
    {
        final double x = uniformRandom(randMax) + baseX;

        return constantT * Math.log(x * factorT);
    }

    /**
     * Return uniform random value in the range 0..max
     *
     * @param max of the random range
     * @return random value
     */
    public static double uniformRandom(final double max)
    {
        return ThreadLocalRandom.current().nextDouble() * max;
    }

    public String toString()
    {
        return "OptimalMulticastDelayGenerator{" +
            "randMax=" + randMax +
            ", baseX=" + baseX +
            ", constantT=" + constantT +
            ", factorT=" + factorT +
            ", shouldFeedbackImmediately=" + shouldFeedbackImmediately() +
            '}';
    }
}


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

import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;

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

/**
 * Default unicast sender flow control strategy.
 * <p>
 * Max of right edges.
 * No tracking of receivers.
 */
public class UnicastFlowControl implements FlowControl
{
    /**
     * Timeout, in nanoseconds, until a receiver is no longer tracked and considered for linger purposes.
     */
    private static final long RECEIVER_TIMEOUT_NS = TimeUnit.SECONDS.toNanos(2);

    private long lastPosition = 0;
    private long timeOfLastStatusMessage = 0;

    private volatile boolean shouldLinger = true;

    /**
     * {@inheritDoc}
     */
    public long onStatusMessage(
        final StatusMessageFlyweight flyweight,
        final InetSocketAddress receiverAddress,
        final long senderLimit,
        final int initialTermId,
        final int positionBitsToShift,
        final long timeNs)
    {
        final long position = computePosition(
            flyweight.consumptionTermId(),
            flyweight.consumptionTermOffset(),
            positionBitsToShift,
            initialTermId);

        lastPosition = Math.max(lastPosition, position);
        timeOfLastStatusMessage = timeNs;

        return Math.max(senderLimit, position + flyweight.receiverWindowLength());
    }

    /**
     * {@inheritDoc}
     */
    public void initialize(final int initialTermId, final int termBufferLength)
    {
    }

    /**
     * {@inheritDoc}
     */
    public long onIdle(final long timeNs, final long senderLimit, final long senderPosition, final boolean isEos)
    {
        if (isEos && shouldLinger)
        {
            if (lastPosition >= senderPosition || ((timeOfLastStatusMessage + RECEIVER_TIMEOUT_NS) - timeNs < 0))
            {
                shouldLinger = false;
            }
        }

        return senderLimit;
    }

    /**
     * {@inheritDoc}
     */
    public boolean shouldLinger(final long timeNs)
    {
        return shouldLinger;
    }
}


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

import io.aeron.ControlledFragmentAssembler;
import io.aeron.Subscription;
import io.aeron.archive.codecs.*;
import io.aeron.logbuffer.ControlledFragmentHandler;
import io.aeron.logbuffer.Header;
import org.agrona.DirectBuffer;
import org.agrona.ErrorHandler;

/**
 * Encapsulate the polling, decoding, dispatching of recording descriptors from an archive.
 */
public class RecordingDescriptorPoller implements ControlledFragmentHandler
{
    private final MessageHeaderDecoder messageHeaderDecoder = new MessageHeaderDecoder();
    private final ControlResponseDecoder controlResponseDecoder = new ControlResponseDecoder();
    private final RecordingDescriptorDecoder recordingDescriptorDecoder = new RecordingDescriptorDecoder();

    private final long controlSessionId;
    private final int fragmentLimit;
    private final Subscription subscription;
    private final ControlledFragmentAssembler fragmentAssembler = new ControlledFragmentAssembler(this);
    private final ErrorHandler errorHandler;

    private long correlationId;
    private int remainingRecordCount;
    private boolean isDispatchComplete = false;
    private RecordingDescriptorConsumer consumer;

    /**
     * Create a poller for a given subscription to an archive for control response messages.
     *
     * @param subscription     to poll for new events.
     * @param errorHandler     to call for asynchronous errors.
     * @param controlSessionId to filter the responses.
     * @param fragmentLimit    to apply for each polling operation.
     */
    public RecordingDescriptorPoller(
        final Subscription subscription,
        final ErrorHandler errorHandler,
        final long controlSessionId,
        final int fragmentLimit)
    {
        this.subscription = subscription;
        this.errorHandler = errorHandler;
        this.fragmentLimit = fragmentLimit;
        this.controlSessionId = controlSessionId;
    }

    /**
     * Get the {@link Subscription} used for polling responses.
     *
     * @return the {@link Subscription} used for polling responses.
     */
    public Subscription subscription()
    {
        return subscription;
    }

    /**
     * Poll for recording events.
     *
     * @return the number of fragments read during the operation. Zero if no events are available.
     */
    public int poll()
    {
        isDispatchComplete = false;

        return subscription.controlledPoll(fragmentAssembler, fragmentLimit);
    }

    /**
     * Control session id for filtering responses.
     *
     * @return control session id for filtering responses.
     */
    public long controlSessionId()
    {
        return controlSessionId;
    }

    /**
     * Is the dispatch of descriptors complete?
     *
     * @return true if the dispatch of descriptors complete?
     */
    public boolean isDispatchComplete()
    {
        return isDispatchComplete;
    }

    /**
     * Get the number of remaining records are expected.
     *
     * @return the number of remaining records are expected.
     */
    public int remainingRecordCount()
    {
        return remainingRecordCount;
    }

    /**
     * Reset the poller to dispatch the descriptors returned from a query.
     *
     * @param correlationId for the response.
     * @param recordCount   of descriptors to expect.
     * @param consumer      to which the recording descriptors are to be dispatched.
     */
    public void reset(final long correlationId, final int recordCount, final RecordingDescriptorConsumer consumer)
    {
        this.correlationId = correlationId;
        this.consumer = consumer;
        this.remainingRecordCount = recordCount;
        isDispatchComplete = false;
    }

    public Action onFragment(final DirectBuffer buffer, final int offset, final int length, final Header header)
    {
        messageHeaderDecoder.wrap(buffer, offset);

        final int schemaId = messageHeaderDecoder.schemaId();
        if (schemaId != MessageHeaderDecoder.SCHEMA_ID)
        {
            throw new ArchiveException("expected schemaId=" + MessageHeaderDecoder.SCHEMA_ID + ", actual=" + schemaId);
        }

        final int templateId = messageHeaderDecoder.templateId();
        switch (templateId)
        {
            case ControlResponseDecoder.TEMPLATE_ID:
                controlResponseDecoder.wrap(
                    buffer,
                    offset + MessageHeaderEncoder.ENCODED_LENGTH,
                    messageHeaderDecoder.blockLength(),
                    messageHeaderDecoder.version());

                if (controlResponseDecoder.controlSessionId() == controlSessionId)
                {
                    final ControlResponseCode code = controlResponseDecoder.code();
                    final long correlationId = controlResponseDecoder.correlationId();

                    if (ControlResponseCode.RECORDING_UNKNOWN == code && correlationId == this.correlationId)
                    {
                        isDispatchComplete = true;
                        return Action.BREAK;
                    }

                    if (ControlResponseCode.ERROR == code)
                    {
                        final ArchiveException ex = new ArchiveException(
                            "response for correlationId=" + this.correlationId +
                            ", error: " + controlResponseDecoder.errorMessage(),
                            (int)controlResponseDecoder.relevantId());

                        if (correlationId == this.correlationId)
                        {
                            throw ex;
                        }
                        else if (null != errorHandler)
                        {
                            errorHandler.onError(ex);
                        }
                    }
                }
                break;

            case RecordingDescriptorDecoder.TEMPLATE_ID:
                recordingDescriptorDecoder.wrap(
                    buffer,
                    offset + MessageHeaderEncoder.ENCODED_LENGTH,
                    messageHeaderDecoder.blockLength(),
                    messageHeaderDecoder.version());

                final long correlationId = recordingDescriptorDecoder.correlationId();
                if (recordingDescriptorDecoder.controlSessionId() == controlSessionId &&
                    correlationId == this.correlationId)
                {
                    consumer.onRecordingDescriptor(
                        controlSessionId,
                        correlationId,
                        recordingDescriptorDecoder.recordingId(),
                        recordingDescriptorDecoder.startTimestamp(),
                        recordingDescriptorDecoder.stopTimestamp(),
                        recordingDescriptorDecoder.startPosition(),
                        recordingDescriptorDecoder.stopPosition(),
                        recordingDescriptorDecoder.initialTermId(),
                        recordingDescriptorDecoder.segmentFileLength(),
                        recordingDescriptorDecoder.termBufferLength(),
                        recordingDescriptorDecoder.mtuLength(),
                        recordingDescriptorDecoder.sessionId(),
                        recordingDescriptorDecoder.streamId(),
                        recordingDescriptorDecoder.strippedChannel(),
                        recordingDescriptorDecoder.originalChannel(),
                        recordingDescriptorDecoder.sourceIdentity());

                    if (0 == --remainingRecordCount)
                    {
                        isDispatchComplete = true;
                        return Action.BREAK;
                    }
                }
                break;
        }

        return Action.CONTINUE;
    }
}


/*
 * 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 io.aeron.driver.reports.LossReport.*;
import static org.agrona.BitUtil.SIZE_OF_INT;

/**
 * Reader that provides the function to read entries from a {@link LossReport}.
 */
public class LossReportReader
{
    /**
     * Read a {@link LossReport} contained in the buffer. This can be done concurrently.
     *
     * @param buffer        containing the loss report.
     * @param entryConsumer to be called to accept each entry in the report.
     * @return the number of entries read.
     */
    public static int read(final AtomicBuffer buffer, final EntryConsumer entryConsumer)
    {
        final int capacity = buffer.capacity();

        int recordsRead = 0;
        int offset = 0;

        while (offset < capacity)
        {
            final long observationCount = buffer.getLongVolatile(offset + OBSERVATION_COUNT_OFFSET);
            if (observationCount <= 0)
            {
                break;
            }

            ++recordsRead;

            final String channel = buffer.getStringAscii(offset + CHANNEL_OFFSET);
            final String source = buffer.getStringAscii(offset + CHANNEL_OFFSET + SIZE_OF_INT + channel.length());

            entryConsumer.accept(
                observationCount,
                buffer.getLong(offset + TOTAL_BYTES_LOST_OFFSET),
                buffer.getLong(offset + FIRST_OBSERVATION_OFFSET),
                buffer.getLong(offset + LAST_OBSERVATION_OFFSET),
                buffer.getInt(offset + SESSION_ID_OFFSET),
                buffer.getInt(offset + STREAM_ID_OFFSET),
                channel,
                source);

            final int recordLength = CHANNEL_OFFSET + (SIZE_OF_INT * 2) + channel.length() + source.length();
            offset += BitUtil.align(recordLength, ENTRY_ALIGNMENT);
        }

        return recordsRead;
    }

    /**
     * Consumer function to be implemented by caller of the read method.
     */
    @FunctionalInterface
    public interface EntryConsumer
    {
        void accept(
            long observationCount,
            long totalBytesLost,
            long firstObservationTimestamp,
            long lastObservationTimestamp,
            int sessionId,
            int streamId,
            String channel,
            String source);
    }
}


/*
 * 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.BufferUtil;
import org.agrona.concurrent.UnsafeBuffer;

import java.nio.ByteBuffer;

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

/**
 * Flyweight for Data Frame header of a message fragment.
 *

 * Data Frame
 * wiki page.
 */
public class DataHeaderFlyweight extends HeaderFlyweight
{
    /**
     * Length of the Data Header.
     */
    public static final int HEADER_LENGTH = 32;

    /**
     * (B) - Fragment that Begins a message Flag.
     */
    public static final short BEGIN_FLAG = 0x80;

    /**
     * (E) - Fragment that Ends a message Flag.
     */
    public static final short END_FLAG = 0x40;

    /**
     * Begin and End Flags.
     */
    public static final short BEGIN_AND_END_FLAGS = BEGIN_FLAG | END_FLAG;

    /**
     * (S) - End of Stream (EOS) Flag for heartbeats after the publication is closed.
     */
    public static final short EOS_FLAG = 0x20;

    /**
     * Begin, End, and EOS Flags.
     */
    public static final short BEGIN_END_AND_EOS_FLAGS = BEGIN_FLAG | END_FLAG | EOS_FLAG;

    public static final long DEFAULT_RESERVE_VALUE = 0L;

    public static final int TERM_OFFSET_FIELD_OFFSET = 8;
    public static final int SESSION_ID_FIELD_OFFSET = 12;
    public static final int STREAM_ID_FIELD_OFFSET = 16;
    public static final int TERM_ID_FIELD_OFFSET = 20;
    public static final int RESERVED_VALUE_OFFSET = 24;
    public static final int DATA_OFFSET = HEADER_LENGTH;

    public DataHeaderFlyweight()
    {
    }

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

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

    /**
     * Is the frame at data frame at the beginning of packet a heartbeat message?
     *
     * @param packet containing the data frame.
     * @param length of the data frame.
     * @return true if a heartbeat otherwise false.
     */
    public static boolean isHeartbeat(final UnsafeBuffer packet, final int length)
    {
        return length == HEADER_LENGTH && packet.getInt(0) == 0;
    }

    /**
     * Does the data frame in the packet have the EOS flag set?
     *
     * @param packet containing the data frame.
     * @return true if the EOS flag is set otherwise false.
     */
    public static boolean isEndOfStream(final UnsafeBuffer packet)
    {
        return BEGIN_END_AND_EOS_FLAGS == (packet.getByte(FLAGS_FIELD_OFFSET) & 0xFF);
    }

    /**
     * return session id field
     *
     * @return session id field
     */
    public int sessionId()
    {
        return getInt(SESSION_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    public static int sessionId(final UnsafeBuffer termBuffer, final int frameOffset)
    {
        return termBuffer.getInt(frameOffset + SESSION_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * set session id field
     *
     * @param sessionId field value
     * @return flyweight
     */
    public DataHeaderFlyweight sessionId(final int sessionId)
    {
        putInt(SESSION_ID_FIELD_OFFSET, sessionId, LITTLE_ENDIAN);

        return this;
    }

    /**
     * return stream id field
     *
     * @return stream id field
     */
    public int streamId()
    {
        return getInt(STREAM_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    public static int streamId(final UnsafeBuffer termBuffer, final int frameOffset)
    {
        return termBuffer.getInt(frameOffset + STREAM_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * set stream id field
     *
     * @param streamId field value
     * @return flyweight
     */
    public DataHeaderFlyweight streamId(final int streamId)
    {
        putInt(STREAM_ID_FIELD_OFFSET, streamId, LITTLE_ENDIAN);

        return this;
    }

    /**
     * return term id field
     *
     * @return term id field
     */
    public int termId()
    {
        return getInt(TERM_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    public static int termId(final UnsafeBuffer termBuffer, final int frameOffset)
    {
        return termBuffer.getInt(frameOffset + TERM_ID_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * set term id field
     *
     * @param termId field value
     * @return flyweight
     */
    public DataHeaderFlyweight termId(final int termId)
    {
        putInt(TERM_ID_FIELD_OFFSET, termId, LITTLE_ENDIAN);

        return this;
    }

    /**
     * return term offset field
     *
     * @return term offset field
     */
    public int termOffset()
    {
        return getInt(TERM_OFFSET_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    public static int termOffset(final UnsafeBuffer termBuffer, final int frameOffset)
    {
        return termBuffer.getInt(frameOffset + TERM_OFFSET_FIELD_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * set term offset field
     *
     * @param termOffset field value
     * @return flyweight
     */
    public DataHeaderFlyweight termOffset(final int termOffset)
    {
        putInt(TERM_OFFSET_FIELD_OFFSET, termOffset, LITTLE_ENDIAN);

        return this;
    }

    /**
     * Get the reserved value in LITTLE_ENDIAN format.
     *
     * @return value of the reserved value.
     */
    public long reservedValue()
    {
        return getLong(RESERVED_VALUE_OFFSET, LITTLE_ENDIAN);
    }

    /**
     * Set the reserved value in LITTLE_ENDIAN format.
     *
     * @param reservedValue to be stored
     * @return flyweight
     */
    public DataHeaderFlyweight reservedValue(final long reservedValue)
    {
        putLong(RESERVED_VALUE_OFFSET, reservedValue, LITTLE_ENDIAN);

        return this;
    }

    /**
     * Return offset in buffer for data
     *
     * @return offset of data in the buffer
     */
    public int dataOffset()
    {
        return DATA_OFFSET;
    }

    /**
     * Return an initialised default Data Frame Header.
     *
     * @param sessionId for the header
     * @param streamId  for the header
     * @param termId    for the header
     * @return byte array containing the header
     */
    public static UnsafeBuffer createDefaultHeader(final int sessionId, final int streamId, final int termId)
    {
        final UnsafeBuffer buffer = new UnsafeBuffer(
            BufferUtil.allocateDirectAligned(HEADER_LENGTH, CACHE_LINE_LENGTH));

        buffer.putByte(VERSION_FIELD_OFFSET, CURRENT_VERSION);
        buffer.putByte(FLAGS_FIELD_OFFSET, (byte)BEGIN_AND_END_FLAGS);
        buffer.putShort(TYPE_FIELD_OFFSET, (short)HDR_TYPE_DATA, LITTLE_ENDIAN);
        buffer.putInt(SESSION_ID_FIELD_OFFSET, sessionId, LITTLE_ENDIAN);
        buffer.putInt(STREAM_ID_FIELD_OFFSET, streamId, LITTLE_ENDIAN);
        buffer.putInt(TERM_ID_FIELD_OFFSET, termId, LITTLE_ENDIAN);
        buffer.putLong(RESERVED_VALUE_OFFSET, DEFAULT_RESERVE_VALUE);

        return buffer;
    }

    public String toString()
    {
        return "DATA Header{" +
            "frame-length=" + frameLength() +
            " version=" + version() +
            " flags=" + String.valueOf(flagsToChars(flags())) +
            " type=" + headerType() +
            " term-offset=" + termOffset() +
            " session-id=" + sessionId() +
            " stream-id=" + streamId() +
            " term-id=" + termId() +
            " reserved-value=" + reservedValue() +
            "}";
    }
}


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

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

/**
 * Message to denote a new counter.
 *

 * Note: Layout should be SBE 2.0 compliant so that the label length is aligned.
 *
 * @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                        |
 *  |                                                               |
 *  +---------------------------------------------------------------+
 *  |                        Counter Type ID                        |
 *  +---------------------------------------------------------------+
 *  |                           Key Length                          |
 *  +---------------------------------------------------------------+
 *  |                           Key Buffer                         ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *  |                          Label Length                         |
 *  +---------------------------------------------------------------+
 *  |                          Label (ASCII)                       ...
 * ...                                                              |
 *  +---------------------------------------------------------------+
 *

 */
public class CounterMessageFlyweight extends CorrelatedMessageFlyweight
{
    private static final int COUNTER_TYPE_ID_FIELD_OFFSET = CORRELATION_ID_FIELD_OFFSET + SIZE_OF_LONG;
    private static final int KEY_LENGTH_OFFSET = COUNTER_TYPE_ID_FIELD_OFFSET + SIZE_OF_INT;

    /**
     * return type id field
     *
     * @return type id field
     */
    public int typeId()
    {
        return buffer.getInt(offset + COUNTER_TYPE_ID_FIELD_OFFSET);
    }

    /**
     * set counter type id field
     *
     * @param typeId field value
     * @return flyweight
     */
    public CounterMessageFlyweight typeId(final long typeId)
    {
        buffer.putLong(offset + COUNTER_TYPE_ID_FIELD_OFFSET, typeId);

        return this;
    }

    /**
     * Offset of the key buffer
     *
     * @return offset of the key buffer
     */
    public int keyBufferOffset()
    {
        return KEY_LENGTH_OFFSET + SIZE_OF_INT;
    }

    /**
     * Length of the key buffer in bytes
     *
     * @return length of key buffer in bytes
     */
    public int keyBufferLength()
    {
        return buffer.getInt(offset + KEY_LENGTH_OFFSET);
    }

    /**
     * Fill the key buffer.
     *
     * @param keyBuffer containing the optional key for the counter.
     * @param keyOffset within the keyBuffer at which the key begins.
     * @param keyLength of the key in the keyBuffer.
     * @return flyweight
     */
    public CounterMessageFlyweight keyBuffer(final DirectBuffer keyBuffer, final int keyOffset, final int keyLength)
    {
        buffer.putInt(KEY_LENGTH_OFFSET, keyLength);
        if (null != keyBuffer && keyLength > 0)
        {
            buffer.putBytes(keyBufferOffset(), keyBuffer, keyOffset, keyLength);
        }

        return this;
    }

    /**
     * Offset of label buffer.
     *
     * @return offset of label buffer
     */
    public int labelBufferOffset()
    {
        return labelOffset() + SIZE_OF_INT;
    }

    /**
     * Length of label buffer in bytes.
     *
     * @return length of label buffer in bytes
     */
    public int labelBufferLength()
    {
        return buffer.getInt(offset + labelOffset());
    }

    /**
     * Fill the label buffer.
     *
     * @param labelBuffer containing the mandatory label for the counter.
     * @param labelOffset within the labelBuffer at which the label begins.
     * @param labelLength of the label in the labelBuffer.
     * @return flyweight
     */
    public CounterMessageFlyweight labelBuffer(
        final DirectBuffer labelBuffer, final int labelOffset, final int labelLength)
    {
        buffer.putInt(labelOffset(), labelLength);
        buffer.putBytes(labelBufferOffset(), labelBuffer, labelOffset, labelLength);

        return this;
    }

    /**
     * Fill the label.
     *
     * @param label for the counter
     * @return flyweight
     */
    public CounterMessageFlyweight label(final String label)
    {
        buffer.putStringAscii(labelOffset(), label);

        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()
    {
        final int labelOffset = labelOffset();
        return labelOffset + buffer.getInt(offset + labelOffset) + SIZE_OF_INT;
    }

    private int labelOffset()
    {
        return KEY_LENGTH_OFFSET + SIZE_OF_INT + BitUtil.align(buffer.getInt(offset + KEY_LENGTH_OFFSET), SIZE_OF_INT);
    }
}


/*
 * 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.exceptions.AeronException;
import io.aeron.logbuffer.*;
import io.aeron.status.ChannelEndpointStatus;
import org.agrona.collections.ArrayUtil;

import java.util.*;
import java.util.function.Consumer;

class SubscriptionLhsPadding
{
    @SuppressWarnings("unused")
    protected long p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15;
}

class SubscriptionFields extends SubscriptionLhsPadding
{
    protected static final Image[] EMPTY_ARRAY = new Image[0];

    protected final long registrationId;
    protected int roundRobinIndex = 0;
    protected final int streamId;
    protected volatile boolean isClosed = false;
    protected volatile Image[] images = EMPTY_ARRAY;
    protected final ClientConductor conductor;
    protected final String channel;
    protected final AvailableImageHandler availableImageHandler;
    protected final UnavailableImageHandler unavailableImageHandler;
    protected int channelStatusId = 0;

    protected SubscriptionFields(
        final long registrationId,
        final int streamId,
        final ClientConductor clientConductor,
        final String channel,
        final AvailableImageHandler availableImageHandler,
        final UnavailableImageHandler unavailableImageHandler)
    {
        this.registrationId = registrationId;
        this.streamId = streamId;
        this.conductor = clientConductor;
        this.channel = channel;
        this.availableImageHandler = availableImageHandler;
        this.unavailableImageHandler = unavailableImageHandler;
    }
}

/**
 * Aeron Subscriber API for receiving a reconstructed {@link Image} for a stream of messages from publishers on
 * a given channel and streamId pair. {@link Image}s are aggregated under a {@link Subscription}.
 *

 * {@link Subscription}s are created via an {@link Aeron} object, and received messages are delivered
 * to the {@link FragmentHandler}.
 *

 * 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 Subscription} for new messages.
 *

 * Note:Subscriptions are not threadsafe and should not be shared between subscribers.
 *
 * @see FragmentAssembler
 * @see ControlledFragmentHandler
 * @see Aeron#addSubscription(String, int)
 * @see Aeron#addSubscription(String, int, AvailableImageHandler, UnavailableImageHandler)
 */
public class Subscription extends SubscriptionFields implements AutoCloseable
{
    @SuppressWarnings("unused")
    protected long p16, p17, p18, p19, p20, p21, p22, p23, p24, p25, p26, p27, p28, p29, p30;

    Subscription(
        final ClientConductor conductor,
        final String channel,
        final int streamId,
        final long registrationId,
        final AvailableImageHandler availableImageHandler,
        final UnavailableImageHandler unavailableImageHandler)
    {
        super(
            registrationId,
            streamId,
            conductor,
            channel,
            availableImageHandler,
            unavailableImageHandler);
    }

    /**
     * Media address for delivery to the channel.
     *
     * @return Media address for delivery to the channel.
     */
    public String channel()
    {
        return channel;
    }

    /**
     * Stream identity for scoping within the channel media address.
     *
     * @return Stream identity for scoping within the channel media address.
     */
    public int streamId()
    {
        return streamId;
    }

    /**
     * Return the registration id used to register this Subscription with the media driver.
     *
     * @return registration id
     */
    public long registrationId()
    {
        return registrationId;
    }

    /**
     * Callback used to indicate when an {@link Image} becomes available under this {@link Subscription}.
     *
     * @return callback used to indicate when an {@link Image} becomes available under this {@link Subscription}.
     */
    public AvailableImageHandler availableImageHandler()
    {
        return availableImageHandler;
    }

    /**
     * Callback used to indicate when an {@link Image} goes unavailable under this {@link Subscription}.
     *
     * @return Callback used to indicate when an {@link Image} goes unavailable under this {@link Subscription}.
     */
    public UnavailableImageHandler unavailableImageHandler()
    {
        return unavailableImageHandler;
    }

    /**
     * Poll the {@link Image}s under the subscription for available message fragments.
     *

     * Each fragment read will be a whole message if it is under MTU length. If larger than MTU then it will come
     * as a series of fragments ordered within a session.
     *

     * To assemble messages that span multiple fragments then use {@link FragmentAssembler}.
     *
     * @param fragmentHandler callback for handling each message fragment as it is read.
     * @param fragmentLimit   number of message fragments to limit when polling across multiple {@link Image}s.
     * @return the number of fragments received
     */
    public int poll(final FragmentHandler fragmentHandler, final int fragmentLimit)
    {
        final Image[] images = this.images;
        final int length = images.length;
        int fragmentsRead = 0;

        int startingIndex = roundRobinIndex++;
        if (startingIndex >= length)
        {
            roundRobinIndex = startingIndex = 0;
        }

        for (int i = startingIndex; i < length && fragmentsRead < fragmentLimit; i++)
        {
            fragmentsRead += images[i].poll(fragmentHandler, fragmentLimit - fragmentsRead);
        }

        for (int i = 0; i < startingIndex && fragmentsRead < fragmentLimit; i++)
        {
            fragmentsRead += images[i].poll(fragmentHandler, fragmentLimit - fragmentsRead);
        }

        return fragmentsRead;
    }

    /**
     * Poll in a controlled manner the {@link Image}s under the subscription for available message fragments.
     * Control is applied to fragments in the stream. If more fragments can be read on another stream
     * they will even if BREAK or ABORT is returned from the fragment handler.
     *

     * Each fragment read will be a whole message if it is under MTU length. If larger than MTU then it will come
     * as a series of fragments ordered within a session.
     *

     * To assemble messages that span multiple fragments then use {@link ControlledFragmentAssembler}.
     *
     * @param fragmentHandler callback for handling each message fragment as it is read.
     * @param fragmentLimit   number of message fragments to limit when polling across multiple {@link Image}s.
     * @return the number of fragments received
     * @see ControlledFragmentHandler
     */
    public int controlledPoll(final ControlledFragmentHandler fragmentHandler, final int fragmentLimit)
    {
        final Image[] images = this.images;
        final int length = images.length;
        int fragmentsRead = 0;

        int startingIndex = roundRobinIndex++;
        if (startingIndex >= length)
        {
            roundRobinIndex = startingIndex = 0;
        }

        for (int i = startingIndex; i < length && fragmentsRead < fragmentLimit; i++)
        {
            fragmentsRead += images[i].controlledPoll(fragmentHandler, fragmentLimit - fragmentsRead);
        }

        for (int i = 0; i < startingIndex && fragmentsRead < fragmentLimit; i++)
        {
            fragmentsRead += images[i].controlledPoll(fragmentHandler, fragmentLimit - fragmentsRead);
        }

        return fragmentsRead;
    }

    /**
     * Poll the {@link Image}s under the subscription for available message fragments in blocks.
     *

     * This method is useful for operations like bulk archiving and messaging indexing.
     *
     * @param blockHandler     to receive a block of fragments from each {@link Image}.
     * @param blockLengthLimit for each {@link Image} polled.
     * @return the number of bytes consumed.
     */
    public long blockPoll(final BlockHandler blockHandler, final int blockLengthLimit)
    {
        long bytesConsumed = 0;
        for (final Image image : images)
        {
            bytesConsumed += image.blockPoll(blockHandler, blockLengthLimit);
        }

        return bytesConsumed;
    }

    /**
     * Poll the {@link Image}s under the subscription for available message fragments in blocks.
     *

     * This method is useful for operations like bulk archiving a stream to file.
     *
     * @param rawBlockHandler  to receive a block of fragments from each {@link Image}.
     * @param blockLengthLimit for each {@link Image} polled.
     * @return the number of bytes consumed.
     */
    public long rawPoll(final RawBlockHandler rawBlockHandler, final int blockLengthLimit)
    {
        long bytesConsumed = 0;
        for (final Image image : images)
        {
            bytesConsumed += image.rawPoll(rawBlockHandler, blockLengthLimit);
        }

        return bytesConsumed;
    }

    /**
     * Is this subscription connected by having at least one open publication {@link Image}.
     *
     * @return true if this subscription connected by having at least one open publication {@link Image}.
     */
    public boolean isConnected()
    {
        for (final Image image : images)
        {
            if (!image.isClosed())
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Has this subscription currently no {@link Image}s?
     *
     * @return has subscription currently no {@link Image}s?
     */
    public boolean hasNoImages()
    {
        return images.length == 0;
    }

    /**
     * Count of {@link Image}s associated to this subscription.
     *
     * @return count of {@link Image}s associated to this subscription.
     */
    public int imageCount()
    {
        return images.length;
    }

    /**
     * Return the {@link Image} associated with the given sessionId.
     *
     * @param sessionId associated with the {@link Image}.
     * @return Image associated with the given sessionId or null if no Image exist.
     */
    public Image imageBySessionId(final int sessionId)
    {
        Image result = null;

        for (final Image image : images)
        {
            if (sessionId == image.sessionId())
            {
                result = image;
                break;
            }
        }

        return result;
    }

    /**
     * Get the {@link Image} at the given index from the images array.
     *
     * @param index in the array
     * @return image at given index
     * @throws ArrayIndexOutOfBoundsException if the index is not valid.
     */
    public Image imageAtIndex(final int index)
    {
        return images[index];
    }

    /**
     * Get a {@link List} of active {@link Image}s that match this subscription.
     *
     * @return an unmodifiable {@link List} of active {@link Image}s that match this subscription.
     */
    public List<Image> images()
    {
        return Collections.unmodifiableList(Arrays.asList(images));
    }

    /**
     * Iterate over the {@link Image}s for this subscription.
     *
     * @param consumer to handle each {@link Image}.
     */
    public void forEachImage(final Consumer<Image> consumer)
    {
        for (final Image image : images)
        {
            consumer.accept(image);
        }
    }

    /**
     * Close the Subscription so that associated {@link Image}s can be released.
     *

     * This method is idempotent.
     */
    public void close()
    {
        if (!isClosed)
        {
            conductor.releaseSubscription(this);
        }
    }

    /**
     * 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 status of the media channel for this Subscription.
     *

     * The status will be {@link io.aeron.status.ChannelEndpointStatus#ERRORED} if a socket exception occurs on setup
     * and {@link io.aeron.status.ChannelEndpointStatus#ACTIVE} if all is well.
     *
     * @return status for the channel as one of the constants from {@link ChannelEndpointStatus} with it being
     * {@link ChannelEndpointStatus#NO_ID_ALLOCATED} if the subscription is closed.
     * @see io.aeron.status.ChannelEndpointStatus
     */
    public long channelStatus()
    {
        if (isClosed)
        {
            return ChannelEndpointStatus.NO_ID_ALLOCATED;
        }

        return conductor.channelStatus(channelStatusId);
    }

    /**
     * Add a destination manually to a multi-destination Subscription.
     *
     * @param endpointChannel for the destination to add
     */
    public void addDestination(final String endpointChannel)
    {
        if (isClosed)
        {
            throw new AeronException("Subscription is closed");
        }

        conductor.addRcvDestination(registrationId, endpointChannel);
    }

    /**
     * Remove a previously added destination from a multi-destination Subscription.
     *
     * @param endpointChannel for the destination to remove
     */
    public void removeDestination(final String endpointChannel)
    {
        if (isClosed)
        {
            throw new AeronException("Subscription is closed");
        }

        conductor.removeRcvDestination(registrationId, endpointChannel);
    }

    void channelStatusId(final int id)
    {
        channelStatusId = id;
    }

    int channelStatusId()
    {
        return channelStatusId;
    }

    void internalClose()
    {
        isClosed = true;
        closeImages();
    }

    void addImage(final Image image)
    {
        images = ArrayUtil.add(images, image);
    }

    Image removeImage(final long correlationId)
    {
        final Image[] oldArray = images;
        Image removedImage = null;

        int i = 0;
        for (final Image image : oldArray)
        {
            if (image.correlationId() == correlationId)
            {
                image.close();
                removedImage = image;
                break;
            }

            i++;
        }

        if (null != removedImage)
        {
            images = ArrayUtil.remove(oldArray, i);
            conductor.releaseLogBuffers(removedImage.logBuffers(), correlationId);
        }

        return removedImage;
    }

    private void closeImages()
    {
        final Image[] images = this.images;
        this.images = EMPTY_ARRAY;

        for (final Image image : images)
        {
            image.close();
        }

        for (final Image image : images)
        {
            conductor.releaseLogBuffers(image.logBuffers(), image.correlationId());

            try
            {
                if (null != unavailableImageHandler)
                {
                    unavailableImageHandler.onUnavailableImage(image);
                }
            }
            catch (final Throwable ex)
            {
                conductor.handleError(ex);
            }
        }
    }

    public String toString()
    {
        return "Subscription{" +
            "registrationId=" + registrationId +
            ", isClosed=" + isClosed +
            ", streamId=" + streamId +
            ", channel='" + channel + '\'' +
            ", imageCount=" + imageCount() +
            '}';
    }
}


/*
 * 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.driver.MediaDriver;
import io.aeron.driver.status.SystemCounterDescriptor;
import org.agrona.CloseHelper;
import org.agrona.concurrent.ShutdownSignalBarrier;

import static org.agrona.SystemUtil.loadPropertiesFiles;

/**
 * Archiving media driver which is an aggregate of a {@link MediaDriver} and an {@link Archive}.
 */
public class ArchivingMediaDriver implements AutoCloseable
{
    private final MediaDriver driver;
    private final Archive archive;

    ArchivingMediaDriver(final MediaDriver driver, final Archive archive)
    {
        this.driver = driver;
        this.archive = archive;
    }

    /**
     * Launch an {@link Archive} with an embedded {@link 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);

        final ShutdownSignalBarrier barrier = new ShutdownSignalBarrier();
        final MediaDriver.Context ctx = new MediaDriver.Context();

        ctx.terminationHook(barrier::signal);

        try (ArchivingMediaDriver ignore = launch(ctx, new Archive.Context()))
        {
            barrier.await();

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

    /**
     * Launch a new {@link ArchivingMediaDriver} with defaults for {@link io.aeron.driver.MediaDriver.Context} and
     * {@link io.aeron.archive.Archive.Context}.
     *
     * @return a new {@link ArchivingMediaDriver} with default contexts.
     */
    public static ArchivingMediaDriver launch()
    {
        return launch(new MediaDriver.Context(), new Archive.Context());
    }

    /**
     * Launch a new {@link ArchivingMediaDriver} with provided contexts.
     *
     * @param driverCtx  for configuring the {@link MediaDriver}.
     * @param archiveCtx for configuring the {@link Archive}.
     * @return a new {@link ArchivingMediaDriver} with the provided contexts.
     */
    public static ArchivingMediaDriver launch(final MediaDriver.Context driverCtx, final Archive.Context archiveCtx)
    {
        final MediaDriver driver = MediaDriver.launch(driverCtx);

        final Archive archive = Archive.launch(archiveCtx
            .mediaDriverAgentInvoker(driver.sharedAgentInvoker())
            .errorHandler(driverCtx.errorHandler())
            .errorCounter(driverCtx.systemCounters().get(SystemCounterDescriptor.ERRORS)));

        return new ArchivingMediaDriver(driver, archive);
    }

    /**
     * Get the launched {@link Archive}.
     *
     * @return the launched {@link Archive}.
     */
    public Archive archive()
    {
        return archive;
    }

    /**
     * Get the launched {@link MediaDriver}.
     *
     * @return the launched {@link MediaDriver}.
     */
    public MediaDriver mediaDriver()
    {
        return driver;
    }

    public void close()
    {
        CloseHelper.close(archive);
        CloseHelper.close(driver);
    }
}