/* * Copyright 2014-2017 Real Logic Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.aeron; 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.indexByPosition; import static io.aeron.logbuffer.TermReader.read; 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. * * <b>Note:</b>Images are not threadsafe and should not be shared between subscribers. */ public class Image { private final long correlationId; private final long joiningPosition; private final int sessionId; private final int initialTermId; private final int termLengthMask; private final int positionBitsToShift; 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.joiningPosition = subscriberPosition.get(); termBuffers = logBuffers.termBuffers(); final int termLength = logBuffers.termLength(); this.termLengthMask = termLength - 1; this.positionBitsToShift = Integer.numberOfTrailingZeros(termLength); this.initialTermId = LogBufferDescriptor.initialTermId(logBuffers.metaDataBuffer()); header = new Header(initialTermId, positionBitsToShift, this); } /** * 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. * * @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 joiningPosition() { return joiningPosition; } /** * 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 0; } 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) { throw new IllegalStateException("Image is closed"); } validatePosition(newPosition); subscriberPosition.setOrdered(newPosition); } /** * 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 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 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 ControlledFragmentAssembler * @see ImageControlledFragmentAssembler */ public int controlledPoll(final ControlledFragmentHandler fragmentHandler, 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(); header.buffer(termBuffer); try { do { 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 = fragmentHandler.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); } } while (fragmentsRead < fragmentLimit && resultingOffset < capacity); } 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 fragmentHandler 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 fragmentHandler, 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(); header.buffer(termBuffer); long resultingPosition = initialPosition; try { do { 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)) { continue; } header.offset(frameOffset); final Action action = fragmentHandler.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; } } while (position < limitPosition && offset < capacity); } 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. * * @param blockHandler 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 blockHandler, 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 limit = Math.min(termOffset + blockLengthLimit, termBuffer.capacity()); final int resultingOffset = TermBlockScanner.scan(termBuffer, termOffset, limit); final int bytesConsumed = resultingOffset - termOffset; if (resultingOffset > termOffset) { try { final int termId = termBuffer.getInt(termOffset + TERM_ID_FIELD_OFFSET, LITTLE_ENDIAN); blockHandler.onBlock(termBuffer, termOffset, bytesConsumed, sessionId, termId); } catch (final Throwable t) { errorHandler.onError(t); } finally { subscriberPosition.setOrdered(position + bytesConsumed); } } return bytesConsumed; } /** * 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. * * @param rawBlockHandler 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 rawBlockHandler, 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 limit = Math.min(termOffset + blockLengthLimit, capacity); final int resultingOffset = TermBlockScanner.scan(termBuffer, termOffset, limit); 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); rawBlockHandler.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 + termBufferLength(); if (newPosition < currentPosition || newPosition > limitPosition) { throw new IllegalArgumentException( "newPosition of " + newPosition + " out of range " + currentPosition + "-" + limitPosition); } if (0 != (newPosition & (FRAME_ALIGNMENT - 1))) { throw new IllegalArgumentException("newPosition of " + newPosition + " not aligned to FRAME_ALIGNMENT"); } } ManagedResource managedResource() { isClosed = true; return new ImageManagedResource(); } private class ImageManagedResource implements ManagedResource { private long timeOfLastStateChange = 0; public void timeOfLastStateChange(final long time) { this.timeOfLastStateChange = time; } public long timeOfLastStateChange() { return timeOfLastStateChange; } public void delete() { logBuffers.close(); } } }