(clientd);
+ aeron::status::PublicationErrorFrame errorFrame{error_frame};
+ handler(errorFrame);
+ }
};
}
diff --git a/aeron-client/src/main/cpp_wrapper/Image.h b/aeron-client/src/main/cpp_wrapper/Image.h
index c28d6db433..b622e2d53a 100644
--- a/aeron-client/src/main/cpp_wrapper/Image.h
+++ b/aeron-client/src/main/cpp_wrapper/Image.h
@@ -487,6 +487,16 @@ class Image
return numFragments;
}
+ /**
+ * Force the driver to disconnect this image from the remote publication.
+ *
+ * @param reason an error message to be forwarded back to the publication.
+ */
+ void reject(std::string reason)
+ {
+ aeron_image_reject(m_image, reason.c_str());
+ }
+
private:
aeron_subscription_t *m_subscription = nullptr;
aeron_image_t *m_image = nullptr;
diff --git a/aeron-client/src/main/cpp_wrapper/status/PublicationErrorFrame.h b/aeron-client/src/main/cpp_wrapper/status/PublicationErrorFrame.h
new file mode 100644
index 0000000000..82c4bebdeb
--- /dev/null
+++ b/aeron-client/src/main/cpp_wrapper/status/PublicationErrorFrame.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+#ifndef AERON_PUBLICATIONERRORFRAME_H
+#define AERON_PUBLICATIONERRORFRAME_H
+
+#include "aeronc.h"
+
+namespace aeron { namespace status {
+
+class PublicationErrorFrame
+{
+public:
+ /**
+ * Constructs from a supplied C pointer to the aeron_publication_error_values_t and wraps over the top of it.
+ * By default it won't manage the underlying memory of the C structure.
+ *
+ * @param errorValues C structure holding the actual data.
+ * @param ownsErrorValuesPtr to indicate if the destructor of this class should free the underlying C memory.
+ */
+ PublicationErrorFrame(aeron_publication_error_values_t *errorValues, bool ownsErrorValuesPtr = false) :
+ m_errorValues(errorValues), m_ownsErrorValuesPtr(ownsErrorValuesPtr)
+ {
+ }
+
+ PublicationErrorFrame(PublicationErrorFrame &other)
+ {
+ if (aeron_publication_error_values_copy(&this->m_errorValues, other.m_errorValues) < 0)
+ {
+ AERON_MAP_ERRNO_TO_SOURCED_EXCEPTION_AND_THROW;
+ }
+ }
+
+ PublicationErrorFrame& operator=(PublicationErrorFrame& other)
+ {
+ if (aeron_publication_error_values_copy(&this->m_errorValues, other.m_errorValues) < 0)
+ {
+ AERON_MAP_ERRNO_TO_SOURCED_EXCEPTION_AND_THROW;
+ }
+
+ m_ownsErrorValuesPtr = true;
+ return *this;
+ }
+
+ PublicationErrorFrame(PublicationErrorFrame &&other)
+ : m_errorValues(other.m_errorValues), m_ownsErrorValuesPtr(other.m_ownsErrorValuesPtr)
+ {
+ other.m_errorValues = nullptr;
+ other.m_ownsErrorValuesPtr = false;
+ }
+
+ ~PublicationErrorFrame()
+ {
+ if (m_ownsErrorValuesPtr)
+ {
+ aeron_publication_error_values_delete(this->m_errorValues);
+ }
+ }
+
+ std::int64_t registrationId()
+ {
+ return m_errorValues->registration_id;
+ }
+
+ std::int32_t sessionId()
+ {
+ return m_errorValues->session_id;
+ }
+
+ std::int32_t streamId()
+ {
+ return m_errorValues->stream_id;
+ }
+
+ std::int64_t groupTag()
+ {
+ return m_errorValues->group_tag;
+ }
+
+ std::uint16_t sourcePort()
+ {
+ return m_errorValues->source_port;
+ }
+
+ std::uint8_t* sourceAddress()
+ {
+ return m_errorValues->source_address;
+ }
+
+ std::int16_t sourceAddressType()
+ {
+ return m_errorValues->address_type;
+ }
+
+ bool isValid()
+ {
+ return nullptr != m_errorValues;
+ }
+
+private:
+ aeron_publication_error_values_t *m_errorValues;
+ bool m_ownsErrorValuesPtr;
+};
+
+}}
+
+#endif //AERON_PUBLICATIONERRORFRAME_H
diff --git a/aeron-client/src/main/java/io/aeron/Aeron.java b/aeron-client/src/main/java/io/aeron/Aeron.java
index 57281589c6..2320f41a87 100644
--- a/aeron-client/src/main/java/io/aeron/Aeron.java
+++ b/aeron-client/src/main/java/io/aeron/Aeron.java
@@ -942,6 +942,7 @@ public static class Context extends CommonContext
private UnavailableImageHandler unavailableImageHandler;
private AvailableCounterHandler availableCounterHandler;
private UnavailableCounterHandler unavailableCounterHandler;
+ private PublicationErrorFrameHandler publicationErrorFrameHandler = PublicationErrorFrameHandler.NO_OP;
private Runnable closeHandler;
private long keepAliveIntervalNs = Configuration.KEEPALIVE_INTERVAL_NS;
private long interServiceTimeoutNs = 0;
@@ -1729,6 +1730,33 @@ public ThreadFactory threadFactory()
return threadFactory;
}
+ /**
+ * Set the handler to receive error frames that have been received by the local driver for publications added by
+ * this client.
+ *
+ * @param publicationErrorFrameHandler to be called back when an error frame is received.
+ * @return this for a fluent API.
+ * @since 1.47.0
+ */
+ public Context publicationErrorFrameHandler(
+ final PublicationErrorFrameHandler publicationErrorFrameHandler)
+ {
+ this.publicationErrorFrameHandler = publicationErrorFrameHandler;
+ return this;
+ }
+
+ /**
+ * Get the handler to receive error frames that have been received by the local driver for publications added by
+ * this client.
+ *
+ * @return the {@link PublicationErrorFrameHandler} to call back on to.
+ * @since 1.47.0
+ */
+ public PublicationErrorFrameHandler publicationErrorFrameHandler()
+ {
+ return this.publicationErrorFrameHandler;
+ }
+
/**
* Clean up all resources that the client uses to communicate with the Media Driver.
*/
diff --git a/aeron-client/src/main/java/io/aeron/ClientConductor.java b/aeron-client/src/main/java/io/aeron/ClientConductor.java
index 24c5fd8063..4a9cdefeed 100644
--- a/aeron-client/src/main/java/io/aeron/ClientConductor.java
+++ b/aeron-client/src/main/java/io/aeron/ClientConductor.java
@@ -15,9 +15,11 @@
*/
package io.aeron;
+import io.aeron.command.PublicationErrorFrameFlyweight;
import io.aeron.exceptions.*;
import io.aeron.status.ChannelEndpointStatus;
import io.aeron.status.HeartbeatTimestamp;
+import io.aeron.status.PublicationErrorFrame;
import org.agrona.*;
import org.agrona.collections.ArrayListUtil;
import org.agrona.collections.Long2ObjectHashMap;
@@ -84,6 +86,7 @@ final class ClientConductor implements Agent
private final AgentInvoker driverAgentInvoker;
private final UnsafeBuffer counterValuesBuffer;
private final CountersReader countersReader;
+ private final PublicationErrorFrame publicationErrorFrame = new PublicationErrorFrame();
private AtomicCounter heartbeatTimestamp;
ClientConductor(final Aeron.Context ctx, final Aeron aeron)
@@ -269,6 +272,22 @@ else if (resource instanceof Publication)
}
}
+ void onPublicationError(final PublicationErrorFrameFlyweight errorFrameFlyweight)
+ {
+ for (final Object resource : resourceByRegIdMap.values())
+ {
+ if (resource instanceof Publication)
+ {
+ final Publication publication = (Publication)resource;
+ if (publication.originalRegistrationId() == errorFrameFlyweight.registrationId())
+ {
+ publicationErrorFrame.set(errorFrameFlyweight);
+ ctx.publicationErrorFrameHandler().onPublicationError(publicationErrorFrame);
+ }
+ }
+ }
+ }
+
void onNewPublication(
final long correlationId,
final long registrationId,
@@ -1420,6 +1439,25 @@ void onStaticCounter(final long correlationId, final int counterId)
resourceByRegIdMap.put(correlationId, (Integer)counterId);
}
+ void rejectImage(final long correlationId, final long position, final String reason)
+ {
+ clientLock.lock();
+ try
+ {
+ ensureActive();
+ ensureNotReentrant();
+
+ // TODO, check reason length??
+
+ final long registrationId = driverProxy.rejectImage(correlationId, position, reason);
+ awaitResponse(registrationId);
+ }
+ finally
+ {
+ clientLock.unlock();
+ }
+ }
+
private void ensureActive()
{
if (isClosed)
diff --git a/aeron-client/src/main/java/io/aeron/DriverEventsAdapter.java b/aeron-client/src/main/java/io/aeron/DriverEventsAdapter.java
index a80a9437b9..c0e12b14f0 100644
--- a/aeron-client/src/main/java/io/aeron/DriverEventsAdapter.java
+++ b/aeron-client/src/main/java/io/aeron/DriverEventsAdapter.java
@@ -30,6 +30,7 @@
class DriverEventsAdapter implements MessageHandler
{
private final ErrorResponseFlyweight errorResponse = new ErrorResponseFlyweight();
+ private final PublicationErrorFrameFlyweight publicationErrorFrame = new PublicationErrorFrameFlyweight();
private final PublicationBuffersReadyFlyweight publicationReady = new PublicationBuffersReadyFlyweight();
private final SubscriptionReadyFlyweight subscriptionReady = new SubscriptionReadyFlyweight();
private final ImageBuffersReadyFlyweight imageReady = new ImageBuffersReadyFlyweight();
@@ -263,6 +264,14 @@ else if (correlationId == activeCorrelationId)
}
break;
}
+
+ case ON_PUBLICATION_ERROR:
+ {
+ publicationErrorFrame.wrap(buffer, index);
+
+ conductor.onPublicationError(publicationErrorFrame);
+ break;
+ }
}
}
}
diff --git a/aeron-client/src/main/java/io/aeron/DriverProxy.java b/aeron-client/src/main/java/io/aeron/DriverProxy.java
index 989fa6c955..d4f8358745 100644
--- a/aeron-client/src/main/java/io/aeron/DriverProxy.java
+++ b/aeron-client/src/main/java/io/aeron/DriverProxy.java
@@ -39,6 +39,7 @@ public final class DriverProxy
private final DestinationByIdMessageFlyweight destinationByIdMessage = new DestinationByIdMessageFlyweight();
private final CounterMessageFlyweight counterMessage = new CounterMessageFlyweight();
private final StaticCounterMessageFlyweight staticCounterMessageFlyweight = new StaticCounterMessageFlyweight();
+ private final RejectImageFlyweight rejectImage = new RejectImageFlyweight();
private final RingBuffer toDriverCommandBuffer;
/**
@@ -491,6 +492,43 @@ public boolean terminateDriver(final DirectBuffer tokenBuffer, final int tokenOf
return false;
}
+ /**
+ * Reject a specific image.
+ *
+ * @param imageCorrelationId of the image to be invalidated
+ * @param position of the image when invalidation occurred
+ * @param reason user supplied reason for invalidation, reported back to publication
+ * @return the correlationId of the request for invalidation.
+ */
+ public long rejectImage(
+ final long imageCorrelationId,
+ final long position,
+ final String reason)
+ {
+ final int length = RejectImageFlyweight.computeLength(reason);
+ final int index = toDriverCommandBuffer.tryClaim(REJECT_IMAGE, length);
+
+ if (index < 0)
+ {
+ throw new AeronException("could not write reject image command");
+ }
+
+ final long correlationId = toDriverCommandBuffer.nextCorrelationId();
+
+ rejectImage
+ .wrap(toDriverCommandBuffer.buffer(), index)
+ .clientId(clientId)
+ .correlationId(correlationId)
+ .imageCorrelationId(imageCorrelationId)
+ .position(position)
+ .reason(reason);
+
+ toDriverCommandBuffer.commit(index);
+
+ return correlationId;
+ }
+
+
/**
* {@inheritDoc}
*/
diff --git a/aeron-client/src/main/java/io/aeron/Image.java b/aeron-client/src/main/java/io/aeron/Image.java
index 4c5f5a8573..14366f3fe0 100644
--- a/aeron-client/src/main/java/io/aeron/Image.java
+++ b/aeron-client/src/main/java/io/aeron/Image.java
@@ -788,6 +788,16 @@ public int rawPoll(final RawBlockHandler handler, final int blockLengthLimit)
return length;
}
+ /**
+ * Force the driver to disconnect this image from the remote publication.
+ *
+ * @param reason an error message to be forwarded back to the publication.
+ */
+ public void reject(final String reason)
+ {
+ subscription.rejectImage(correlationId, position(), reason);
+ }
+
private UnsafeBuffer activeTermBuffer(final long position)
{
return termBuffers[LogBufferDescriptor.indexByPosition(position, positionBitsToShift)];
diff --git a/aeron-client/src/main/java/io/aeron/PublicationErrorFrameHandler.java b/aeron-client/src/main/java/io/aeron/PublicationErrorFrameHandler.java
new file mode 100644
index 0000000000..31fe6a425e
--- /dev/null
+++ b/aeron-client/src/main/java/io/aeron/PublicationErrorFrameHandler.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://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.status.PublicationErrorFrame;
+
+/**
+ * Interface for handling various error frame messages for publications.
+ * @since 1.47.0
+ */
+public interface PublicationErrorFrameHandler
+{
+ /**
+ * Default no-op error frame handler.
+ */
+ PublicationErrorFrameHandler NO_OP = errorFrame -> {};
+
+ /**
+ * Called when an error frame for a publication is received by the local driver and needs to be propagated to the
+ * appropriate clients. E.g. when an image is invalidated. This callback will reuse the {@link
+ * PublicationErrorFrame} instance, so data is only valid for the lifetime of the callback. If the user needs to
+ * pass the data onto another thread or hold in another location for use later, then the user needs to make use of
+ * the {@link PublicationErrorFrame#clone()} method to create a copy for their own use.
+ *
+ * This callback will be executed on the client conductor thread, similar to image availability notifications.
+ *
+ * This notification will only be propagated to clients that have added an instance of the Publication that received
+ * the error frame (i.e. the originalRegistrationId matches the registrationId on the error frame).
+ *
+ * @param errorFrame containing the relevant information about the publication and the error message.
+ */
+ void onPublicationError(PublicationErrorFrame errorFrame);
+}
diff --git a/aeron-client/src/main/java/io/aeron/Subscription.java b/aeron-client/src/main/java/io/aeron/Subscription.java
index cea1b5b583..80f8849b3b 100644
--- a/aeron-client/src/main/java/io/aeron/Subscription.java
+++ b/aeron-client/src/main/java/io/aeron/Subscription.java
@@ -620,6 +620,11 @@ Image removeImage(final long correlationId)
return removedImage;
}
+ void rejectImage(final long correlationId, final long position, final String reason)
+ {
+ conductor.rejectImage(correlationId, position, reason);
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/aeron-client/src/main/java/io/aeron/command/ControlProtocolEvents.java b/aeron-client/src/main/java/io/aeron/command/ControlProtocolEvents.java
index 759d02392c..616309887d 100644
--- a/aeron-client/src/main/java/io/aeron/command/ControlProtocolEvents.java
+++ b/aeron-client/src/main/java/io/aeron/command/ControlProtocolEvents.java
@@ -100,10 +100,15 @@ public class ControlProtocolEvents
*/
public static final int ADD_STATIC_COUNTER = 0x0F;
+ /**
+ * Invalidate an image.
+ */
+ public static final int REJECT_IMAGE = 0x10;
+
/**
* Remove a destination by registration id.
*/
- public static final int REMOVE_DESTINATION_BY_ID = 0x10;
+ public static final int REMOVE_DESTINATION_BY_ID = 0x11;
// Media Driver to Clients
@@ -163,4 +168,10 @@ public class ControlProtocolEvents
* @since 1.45.0
*/
public static final int ON_STATIC_COUNTER = 0x0F0B;
+
+ /**
+ * Inform clients of error frame received by publication
+ * @since 1.47.0
+ */
+ public static final int ON_PUBLICATION_ERROR = 0x0F0C;
}
diff --git a/aeron-client/src/main/java/io/aeron/command/PublicationErrorFrameFlyweight.java b/aeron-client/src/main/java/io/aeron/command/PublicationErrorFrameFlyweight.java
new file mode 100644
index 0000000000..d72c187978
--- /dev/null
+++ b/aeron-client/src/main/java/io/aeron/command/PublicationErrorFrameFlyweight.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://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 io.aeron.ErrorCode;
+import org.agrona.BitUtil;
+import org.agrona.MutableDirectBuffer;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Control message flyweight error frames received by a publication to be reported to the client.
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Publication Registration Id |
+ * | |
+ * +---------------------------------------------------------------+
+ * | Destination Registration Id |
+ * | |
+ * +---------------------------------------------------------------+
+ * | Session ID |
+ * +---------------------------------------------------------------+
+ * | Stream ID |
+ * +---------------------------------------------------------------+
+ * | Receiver ID |
+ * | |
+ * +---------------------------------------------------------------+
+ * | Group Tag |
+ * | |
+ * +-------------------------------+-------------------------------+
+ * | Address Type | UDP Port |
+ * +-------------------------------+-------------------------------+
+ * | IPv4 or IPv6 Address padded out to 16 bytes |
+ * | |
+ * | |
+ * | |
+ * +---------------------------------------------------------------+
+ * | Error Code |
+ * +---------------------------------------------------------------+
+ * | Error Message Length |
+ * +---------------------------------------------------------------+
+ * | Error Message ...
+ * ... |
+ * +---------------------------------------------------------------+
+ *
+ * @since 1.47.0
+ */
+public class PublicationErrorFrameFlyweight
+{
+ private static final int REGISTRATION_ID_OFFSET = 0;
+ private static final int IPV6_ADDRESS_LENGTH = 16;
+ private static final int IPV4_ADDRESS_LENGTH = BitUtil.SIZE_OF_INT;
+ private static final int DESTINATION_REGISTRATION_ID_OFFSET = REGISTRATION_ID_OFFSET + BitUtil.SIZE_OF_LONG;
+ private static final int SESSION_ID_OFFSET = DESTINATION_REGISTRATION_ID_OFFSET + BitUtil.SIZE_OF_LONG;
+ private static final int STREAM_ID_OFFSET = SESSION_ID_OFFSET + BitUtil.SIZE_OF_INT;
+ private static final int RECEIVER_ID_OFFSET = STREAM_ID_OFFSET + BitUtil.SIZE_OF_INT;
+ private static final int GROUP_TAG_OFFSET = RECEIVER_ID_OFFSET + BitUtil.SIZE_OF_LONG;
+ private static final int ADDRESS_TYPE_OFFSET = GROUP_TAG_OFFSET + BitUtil.SIZE_OF_LONG;
+ private static final int ADDRESS_PORT_OFFSET = ADDRESS_TYPE_OFFSET + BitUtil.SIZE_OF_SHORT;
+ private static final int ADDRESS_OFFSET = ADDRESS_PORT_OFFSET + BitUtil.SIZE_OF_SHORT;
+ private static final int ERROR_CODE_OFFSET = ADDRESS_OFFSET + IPV6_ADDRESS_LENGTH;
+ private static final int ERROR_MESSAGE_OFFSET = ERROR_CODE_OFFSET + BitUtil.SIZE_OF_INT;
+ private static final short ADDRESS_TYPE_IPV4 = 1;
+ private static final short ADDRESS_TYPE_IPV6 = 2;
+
+ private MutableDirectBuffer buffer;
+ private int offset;
+
+ /**
+ * Wrap the buffer at a given offset for updates.
+ *
+ * @param buffer to wrap.
+ * @param offset at which the message begins.
+ * @return this for a fluent API.
+ */
+ public final PublicationErrorFrameFlyweight wrap(final MutableDirectBuffer buffer, final int offset)
+ {
+ this.buffer = buffer;
+ this.offset = offset;
+
+ return this;
+ }
+
+ /**
+ * Return registration ID of the publication that received the error frame.
+ *
+ * @return registration ID of the publication
+ */
+ public long registrationId()
+ {
+ return buffer.getLong(offset + REGISTRATION_ID_OFFSET);
+ }
+
+ /**
+ * Set the registration ID of the publication that received the error frame.
+ *
+ * @param registrationId of the publication.
+ * @return this for a fluent API.
+ */
+ public PublicationErrorFrameFlyweight registrationId(final long registrationId)
+ {
+ buffer.putLong(offset + REGISTRATION_ID_OFFSET, registrationId);
+ return this;
+ }
+
+ /**
+ * Return registration id of the destination that received the error frame. This will only be set if the publication
+ * is using manual MDC.
+ *
+ * @return registration ID of the publication or {@link io.aeron.Aeron#NULL_VALUE}
+ */
+ public long destinationRegistrationId()
+ {
+ return buffer.getLong(offset + DESTINATION_REGISTRATION_ID_OFFSET);
+ }
+
+ /**
+ * Set the registration ID of the destination that received the error frame. Use {@link io.aeron.Aeron#NULL_VALUE}
+ * if not set.
+ *
+ * @param registrationId of the destination.
+ * @return this for a fluent API.
+ */
+ public PublicationErrorFrameFlyweight destinationRegistrationId(final long registrationId)
+ {
+ buffer.putLong(offset + DESTINATION_REGISTRATION_ID_OFFSET, registrationId);
+ return this;
+ }
+
+ /**
+ * Get the stream id field.
+ *
+ * @return stream id field.
+ */
+ public int streamId()
+ {
+ return buffer.getInt(offset + STREAM_ID_OFFSET);
+ }
+
+ /**
+ * Set the stream id field.
+ *
+ * @param streamId field value.
+ * @return this for a fluent API.
+ */
+ public PublicationErrorFrameFlyweight streamId(final int streamId)
+ {
+ buffer.putInt(offset + STREAM_ID_OFFSET, streamId);
+
+ return this;
+ }
+
+ /**
+ * Get the session id field.
+ *
+ * @return session id field.
+ */
+ public int sessionId()
+ {
+ return buffer.getInt(offset + SESSION_ID_OFFSET);
+ }
+
+ /**
+ * Set session id field.
+ *
+ * @param sessionId field value.
+ * @return this for a fluent API.
+ */
+ public PublicationErrorFrameFlyweight sessionId(final int sessionId)
+ {
+ buffer.putInt(offset + SESSION_ID_OFFSET, sessionId);
+
+ return this;
+ }
+
+ /**
+ * Get the receiver id field
+ *
+ * @return get the receiver id field.
+ */
+ public long receiverId()
+ {
+ return buffer.getLong(offset + RECEIVER_ID_OFFSET);
+ }
+
+ /**
+ * Set receiver id field
+ *
+ * @param receiverId field value.
+ * @return this for a fluent API.
+ */
+ public PublicationErrorFrameFlyweight receiverId(final long receiverId)
+ {
+ buffer.putLong(offset + RECEIVER_ID_OFFSET, receiverId);
+
+ return this;
+ }
+
+ /**
+ * Get the group tag field.
+ *
+ * @return the group tag field.
+ */
+ public long groupTag()
+ {
+ return buffer.getLong(offset + GROUP_TAG_OFFSET);
+ }
+
+ /**
+ * Set the group tag field.
+ *
+ * @param groupTag the group tag value.
+ * @return this for a fluent API.
+ */
+ public PublicationErrorFrameFlyweight groupTag(final long groupTag)
+ {
+ buffer.putLong(offset + GROUP_TAG_OFFSET, groupTag);
+
+ return this;
+ }
+
+ /**
+ * Set the source address for the error frame. Store the type (IPv4 or IPv6), port and address as bytes.
+ *
+ * @param sourceAddress of the error frame.
+ * @return this for a fluent API
+ */
+ public PublicationErrorFrameFlyweight sourceAddress(final InetSocketAddress sourceAddress)
+ {
+ final short sourcePort = (short)(sourceAddress.getPort() & 0xFFFF);
+ final InetAddress address = sourceAddress.getAddress();
+
+ buffer.putShort(offset + ADDRESS_PORT_OFFSET, sourcePort);
+ buffer.putBytes(offset + ADDRESS_OFFSET, address.getAddress());
+ if (address instanceof Inet4Address)
+ {
+ buffer.putShort(offset + ADDRESS_TYPE_OFFSET, ADDRESS_TYPE_IPV4);
+ buffer.setMemory(
+ offset + ADDRESS_OFFSET + IPV4_ADDRESS_LENGTH, IPV6_ADDRESS_LENGTH - IPV4_ADDRESS_LENGTH, (byte)0);
+ }
+ else if (address instanceof Inet6Address)
+ {
+ buffer.putShort(offset + ADDRESS_TYPE_OFFSET, ADDRESS_TYPE_IPV6);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Unknown address type:" + address.getClass().getSimpleName());
+ }
+
+ return this;
+ }
+
+ /**
+ * Get the source address of this error frame.
+ *
+ * @return source address of the error frame.
+ */
+ public InetSocketAddress sourceAddress()
+ {
+ final short addressType = buffer.getShort(offset + ADDRESS_TYPE_OFFSET);
+ final int port = buffer.getShort(offset + ADDRESS_PORT_OFFSET) & 0xFFFF;
+
+ final byte[] address;
+ if (ADDRESS_TYPE_IPV4 == addressType)
+ {
+ address = new byte[IPV4_ADDRESS_LENGTH];
+ }
+ else if (ADDRESS_TYPE_IPV6 == addressType)
+ {
+ address = new byte[IPV6_ADDRESS_LENGTH];
+ }
+ else
+ {
+ throw new IllegalArgumentException("Unknown address type:" + addressType);
+ }
+
+ buffer.getBytes(offset + ADDRESS_OFFSET, address);
+ try
+ {
+ return new InetSocketAddress(Inet4Address.getByAddress(address), port);
+ }
+ catch (final UnknownHostException ex)
+ {
+ throw new IllegalArgumentException("Unknown address type:" + addressType, ex);
+ }
+
+ }
+
+ /**
+ * Error code for the command.
+ *
+ * @return error code for the command.
+ */
+ public ErrorCode errorCode()
+ {
+ return ErrorCode.get(buffer.getInt(offset + ERROR_CODE_OFFSET));
+ }
+
+ /**
+ * Error code value for the command.
+ *
+ * @return error code value for the command.
+ */
+ public int errorCodeValue()
+ {
+ return buffer.getInt(offset + ERROR_CODE_OFFSET);
+ }
+
+ /**
+ * Set the error code for the command.
+ *
+ * @param code for the error.
+ * @return this for a fluent API.
+ */
+ public PublicationErrorFrameFlyweight errorCode(final ErrorCode code)
+ {
+ buffer.putInt(offset + ERROR_CODE_OFFSET, code.value());
+ return this;
+ }
+
+ /**
+ * Error message associated with the error.
+ *
+ * @return error message
+ */
+ public String errorMessage()
+ {
+ return buffer.getStringAscii(offset + ERROR_MESSAGE_OFFSET);
+ }
+
+ /**
+ * Append the error message to an appendable without allocation.
+ *
+ * @param appendable to append error message to.
+ * @return number bytes copied.
+ */
+ public int appendMessage(final Appendable appendable)
+ {
+ return buffer.getStringAscii(offset + ERROR_MESSAGE_OFFSET, appendable);
+ }
+
+ /**
+ * Set the error message.
+ *
+ * @param message to associate with the error.
+ * @return this for a fluent API.
+ */
+ public PublicationErrorFrameFlyweight errorMessage(final String message)
+ {
+ buffer.putStringAscii(offset + ERROR_MESSAGE_OFFSET, message);
+ return this;
+ }
+
+ /**
+ * Length of the error response in bytes.
+ *
+ * @return length of the error response in bytes.
+ */
+ public int length()
+ {
+ return ERROR_MESSAGE_OFFSET + BitUtil.SIZE_OF_INT + buffer.getInt(offset + ERROR_MESSAGE_OFFSET);
+ }
+}
diff --git a/aeron-client/src/main/java/io/aeron/command/RejectImageFlyweight.java b/aeron-client/src/main/java/io/aeron/command/RejectImageFlyweight.java
new file mode 100644
index 0000000000..adf99c2ddc
--- /dev/null
+++ b/aeron-client/src/main/java/io/aeron/command/RejectImageFlyweight.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://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 io.aeron.exceptions.ControlProtocolException;
+import org.agrona.MutableDirectBuffer;
+
+import static io.aeron.ErrorCode.MALFORMED_COMMAND;
+import static org.agrona.BitUtil.SIZE_OF_INT;
+import static org.agrona.BitUtil.SIZE_OF_LONG;
+
+/**
+ * Control message to reject an image for a subscription.
+ *
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Client ID |
+ * | |
+ * +---------------------------------------------------------------+
+ * | Correlation ID |
+ * | |
+ * +---------------------------------------------------------------+
+ * | Image Correlation ID |
+ * | |
+ * +---------------------------------------------------------------+
+ * | Position |
+ * | |
+ * +---------------------------------------------------------------+
+ * | Reason Length |
+ * +---------------------------------------------------------------+
+ * | Reason (ASCII) ...
+ * ... |
+ * +---------------------------------------------------------------+
+ *
+ */
+public class RejectImageFlyweight extends CorrelatedMessageFlyweight
+{
+ private static final int IMAGE_CORRELATION_ID_FIELD_OFFSET = CORRELATION_ID_FIELD_OFFSET + SIZE_OF_LONG;
+ private static final int POSITION_FIELD_OFFSET = IMAGE_CORRELATION_ID_FIELD_OFFSET + SIZE_OF_LONG;
+ private static final int REASON_FIELD_OFFSET = POSITION_FIELD_OFFSET + SIZE_OF_LONG;
+ private static final int MINIMUM_SIZE = REASON_FIELD_OFFSET + SIZE_OF_INT;
+
+ /**
+ * Wrap the buffer at a given offset for updates.
+ *
+ * @param buffer to wrap.
+ * @param offset at which the message begins.
+ * @return this for a fluent API.
+ */
+ public RejectImageFlyweight wrap(final MutableDirectBuffer buffer, final int offset)
+ {
+ super.wrap(buffer, offset);
+ return this;
+ }
+
+ /**
+ * Get image correlation id field.
+ *
+ * @return image correlation id field.
+ */
+ public long imageCorrelationId()
+ {
+ return buffer.getLong(offset + IMAGE_CORRELATION_ID_FIELD_OFFSET);
+ }
+
+ /**
+ * Put image correlation id field.
+ *
+ * @param position new image correlation id value.
+ * @return this for a fluent API.
+ */
+ public RejectImageFlyweight imageCorrelationId(final long position)
+ {
+ buffer.putLong(offset + IMAGE_CORRELATION_ID_FIELD_OFFSET, position);
+ return this;
+ }
+
+ /**
+ * Get position field.
+ *
+ * @return position field.
+ */
+ public long position()
+ {
+ return buffer.getLong(offset + POSITION_FIELD_OFFSET);
+ }
+
+ /**
+ * Put position field.
+ *
+ * @param position new position value.
+ * @return this for a fluent API.
+ */
+ public RejectImageFlyweight position(final long position)
+ {
+ buffer.putLong(offset + POSITION_FIELD_OFFSET, position);
+ return this;
+ }
+
+ /**
+ * Put reason field as ASCII. Include the reason length in the message.
+ *
+ * @param reason for invalidating the image.
+ * @return this for a fluent API.
+ */
+ public RejectImageFlyweight reason(final String reason)
+ {
+ buffer.putStringAscii(offset + REASON_FIELD_OFFSET, reason);
+ return this;
+ }
+
+
+ /**
+ * Get reason field as ASCII.
+ *
+ * @return reason for invalidating the image.
+ */
+ public String reason()
+ {
+ return buffer.getStringAscii(offset + REASON_FIELD_OFFSET);
+ }
+
+ /**
+ * Length of the reason text.
+ *
+ * @return length of the reason text.
+ */
+ public int reasonBufferLength()
+ {
+ // This does make the assumption that the string is stored with the leading 4 bytes representing the length.
+ return buffer.getInt(offset + REASON_FIELD_OFFSET);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public RejectImageFlyweight clientId(final long clientId)
+ {
+ super.clientId(clientId);
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public RejectImageFlyweight correlationId(final long correlationId)
+ {
+ super.correlationId(correlationId);
+ return this;
+ }
+
+ /**
+ * Compute the length of the message based on the reason supplied.
+ *
+ * @param reason message to be return to originator.
+ * @return length of the message.
+ */
+ public static int computeLength(final String reason)
+ {
+ return MINIMUM_SIZE + reason.length();
+ }
+
+ /**
+ * Validate buffer length is long enough for message.
+ *
+ * @param msgTypeId type of message.
+ * @param length of message in bytes to validate.
+ */
+ public void validateLength(final int msgTypeId, final int length)
+ {
+ if (length < MINIMUM_SIZE)
+ {
+ throw new ControlProtocolException(
+ MALFORMED_COMMAND, "command=" + msgTypeId + " too short: length=" + length);
+ }
+
+ if (length < MINIMUM_SIZE + reasonBufferLength())
+ {
+ throw new ControlProtocolException(
+ MALFORMED_COMMAND, "command=" + msgTypeId + " too short: length=" + length);
+ }
+ }
+}
diff --git a/aeron-client/src/main/java/io/aeron/protocol/ErrorFlyweight.java b/aeron-client/src/main/java/io/aeron/protocol/ErrorFlyweight.java
new file mode 100644
index 0000000000..46dc9eae2a
--- /dev/null
+++ b/aeron-client/src/main/java/io/aeron/protocol/ErrorFlyweight.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.aeron.protocol;
+
+import org.agrona.concurrent.UnsafeBuffer;
+
+import java.nio.ByteBuffer;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+
+/**
+ * Flyweight for general Aeron network protocol error frame
+ *
+ * 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
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * 0 |R| Frame Length (varies) |
+ * +---------------+---------------+-------------------------------+
+ * 4 | Version | Flags | Type (=0x04) |
+ * +---------------+---------------+-------------------------------+
+ * 8 | Session ID |
+ * +---------------------------------------------------------------+
+ * 12 | Stream ID |
+ * +---------------------------------------------------------------+
+ * 16 | Receiver ID |
+ * | |
+ * +---------------------------------------------------------------+
+ * 24 | Group Tag |
+ * | |
+ * +---------------------------------------------------------------+
+ * 32 | Error Code |
+ * +---------------------------------------------------------------+
+ * 36 | Error String Length |
+ * +---------------------------------------------------------------+
+ * 40 | Error String ...
+ * +---------------------------------------------------------------+
+ * ... |
+ * +---------------------------------------------------------------+
+ *
+ * @since 1.47.0
+ */
+public class ErrorFlyweight extends HeaderFlyweight
+{
+ /**
+ * Length of the Error Header.
+ */
+ public static final int HEADER_LENGTH = 40;
+
+ /**
+ * Offset in the frame at which the session-id field begins.
+ */
+ public static final int SESSION_ID_FIELD_OFFSET = 8;
+
+ /**
+ * Offset in the frame at which the stream-id field begins.
+ */
+ public static final int STREAM_ID_FIELD_OFFSET = 12;
+
+ /**
+ * Offset in the frame at which the receiver-id field begins.
+ */
+ public static final int RECEIVER_ID_FIELD_OFFSET = 16;
+
+ /**
+ * Offset in the frame at which the group-tag field begins.
+ */
+ public static final int GROUP_TAG_FIELD_OFFSET = 24;
+
+ /**
+ * Offset in the frame at which the error code field begins.
+ */
+ public static final int ERROR_CODE_FIELD_OFFSET = 32;
+
+ /**
+ * Offset in the frame at which the error string field begins. Specifically this will be the length of the string
+ * using the Agrona buffer standard of using 4 bytes for the length. Followed by the variable bytes for the string.
+ */
+ public static final int ERROR_STRING_FIELD_OFFSET = 36;
+
+ /**
+ * Maximum length that an error message can be. Can be short that this if configuration options have made the MTU
+ * smaller. The error message should be truncated to fit within a single MTU.
+ */
+ public static final int MAX_ERROR_MESSAGE_LENGTH = 1023;
+
+ /**
+ * Maximum length of an error frame. Captures the maximum message length and the header length.
+ */
+ public static final int MAX_ERROR_FRAME_LENGTH = HEADER_LENGTH + MAX_ERROR_MESSAGE_LENGTH;
+
+ /**
+ * Flag to indicate that the group tag field is relevant, if not set the value should be ignored.
+ */
+ public static final int HAS_GROUP_ID_FLAG = 0x08;
+
+ /**
+ * Default constructor for the ErrorFlyweight so that it can be wrapped over a buffer later.
+ */
+ public ErrorFlyweight()
+ {
+ }
+
+ /**
+ * Construct the ErrorFlyweight over an NIO ByteBuffer frame.
+ *
+ * @param buffer containing the frame.
+ */
+ public ErrorFlyweight(final ByteBuffer buffer)
+ {
+ super(buffer);
+ }
+
+ /**
+ * Construct the ErrorFlyweight over an UnsafeBuffer frame.
+ *
+ * @param buffer containing the frame.
+ */
+ public ErrorFlyweight(final UnsafeBuffer buffer)
+ {
+ super(buffer);
+ }
+
+
+ /**
+ * The session-id for the stream.
+ *
+ * @return session-id for the stream.
+ */
+ public int sessionId()
+ {
+ return getInt(SESSION_ID_FIELD_OFFSET, LITTLE_ENDIAN);
+ }
+
+ /**
+ * Set session-id for the stream.
+ *
+ * @param sessionId session-id for the stream.
+ * @return this for a fluent API.
+ */
+ public ErrorFlyweight sessionId(final int sessionId)
+ {
+ putInt(SESSION_ID_FIELD_OFFSET, sessionId, LITTLE_ENDIAN);
+
+ return this;
+ }
+
+ /**
+ * The stream-id for the stream.
+ *
+ * @return stream-id for the stream.
+ */
+ public int streamId()
+ {
+ return getInt(STREAM_ID_FIELD_OFFSET, LITTLE_ENDIAN);
+ }
+
+ /**
+ * Set stream-id for the stream.
+ *
+ * @param streamId stream-id for the stream.
+ * @return this for a fluent API.
+ */
+ public ErrorFlyweight streamId(final int streamId)
+ {
+ putInt(STREAM_ID_FIELD_OFFSET, streamId, LITTLE_ENDIAN);
+
+ return this;
+ }
+
+ /**
+ * The receiver-id for the stream.
+ *
+ * @return receiver-id for the stream.
+ */
+ public long receiverId()
+ {
+ return getLong(RECEIVER_ID_FIELD_OFFSET, LITTLE_ENDIAN);
+ }
+
+ /**
+ * Set receiver-id for the stream.
+ *
+ * @param receiverId receiver-id for the stream.
+ * @return this for a fluent API.
+ */
+ public ErrorFlyweight receiverId(final long receiverId)
+ {
+ putLong(RECEIVER_ID_FIELD_OFFSET, receiverId, LITTLE_ENDIAN);
+
+ return this;
+ }
+
+ /**
+ * Get the group tag for the message.
+ *
+ * @return group tag for the message.
+ */
+ public long groupTag()
+ {
+ return getLong(GROUP_TAG_FIELD_OFFSET, LITTLE_ENDIAN);
+ }
+
+ /**
+ * Determines if this message has the group tag flag set.
+ *
+ * @return true
if the flag is set false otherwise.
+ */
+ public boolean hasGroupTag()
+ {
+ return HAS_GROUP_ID_FLAG == (HAS_GROUP_ID_FLAG & flags());
+ }
+
+ /**
+ * Set an optional group tag, null indicates the value should not be set. If non-null will set HAS_GROUP_TAG flag
+ * on the header. A null value will clear this flag and use a value of 0.
+ *
+ * @param groupTag optional group tag to be applied to this message.
+ * @return this for a fluent API.
+ */
+ public ErrorFlyweight groupTag(final Long groupTag)
+ {
+ if (null == groupTag)
+ {
+ flags((short)(~HAS_GROUP_ID_FLAG & flags()));
+ }
+ else
+ {
+ putLong(GROUP_TAG_FIELD_OFFSET, groupTag);
+ flags((short)(HAS_GROUP_ID_FLAG | flags()));
+ }
+
+ return this;
+ }
+
+ /**
+ * The error-code for the message.
+ *
+ * @return error-code for the message.
+ */
+ public int errorCode()
+ {
+ return getInt(ERROR_CODE_FIELD_OFFSET, LITTLE_ENDIAN);
+ }
+
+ /**
+ * Set error-code for the message.
+ *
+ * @param errorCode for the message.
+ * @return this for a fluent API.
+ */
+ public ErrorFlyweight errorCode(final int errorCode)
+ {
+ putInt(ERROR_CODE_FIELD_OFFSET, errorCode, LITTLE_ENDIAN);
+
+ return this;
+ }
+
+ /**
+ * Get the error string for the message.
+ *
+ * @return the error string for the message.
+ */
+ public String errorMessage()
+ {
+ return getStringAscii(ERROR_STRING_FIELD_OFFSET);
+ }
+
+ /**
+ * Set the error string for the message.
+ *
+ * @param errorMessage the error string in UTF-8.
+ * @return this for a fluent API.
+ */
+ public ErrorFlyweight errorMessage(final String errorMessage)
+ {
+ final int headerAndMessageLength = putStringAscii(ERROR_STRING_FIELD_OFFSET, errorMessage, LITTLE_ENDIAN);
+ frameLength(HEADER_LENGTH + (headerAndMessageLength - STR_HEADER_LEN));
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String toString()
+ {
+ return "ERROR{" +
+ "frame-length=" + frameLength() +
+ " version=" + version() +
+ " flags=" + String.valueOf(flagsToChars(flags())) +
+ " type=" + headerType() +
+ " session-id=" + sessionId() +
+ " stream-id=" + streamId() +
+ " error-code=" + errorCode() +
+ " error-message=" + errorMessage() +
+ "}";
+ }
+}
diff --git a/aeron-client/src/main/java/io/aeron/status/PublicationErrorFrame.java b/aeron-client/src/main/java/io/aeron/status/PublicationErrorFrame.java
new file mode 100644
index 0000000000..7b4ba93277
--- /dev/null
+++ b/aeron-client/src/main/java/io/aeron/status/PublicationErrorFrame.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://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.status;
+
+import io.aeron.command.PublicationErrorFrameFlyweight;
+
+import java.net.InetSocketAddress;
+
+/**
+ * Encapsulates the data received when a publication receives an error frame.
+ */
+public class PublicationErrorFrame implements Cloneable
+{
+ private long registrationId;
+ private int sessionId;
+ private int streamId;
+ private long receiverId;
+ private long destinationRegistrationId;
+ private Long groupTag;
+ private int errorCode;
+ private String errorMessage;
+ private InetSocketAddress sourceAddress;
+
+ /**
+ * Registration id of the publication that received the error frame.
+ *
+ * @return registration id of the publication.
+ */
+ public long registrationId()
+ {
+ return registrationId;
+ }
+
+ /**
+ * Session id of the publication that received the error frame.
+ *
+ * @return session id of the publication.
+ */
+ public int sessionId()
+ {
+ return sessionId;
+ }
+
+ /**
+ * Stream id of the publication that received the error frame.
+ *
+ * @return stream id of the publication.
+ */
+ public int streamId()
+ {
+ return streamId;
+ }
+
+ /**
+ * Receiver id of the source that send the error frame.
+ *
+ * @return receiver id of the source that send the error frame.
+ */
+ public long receiverId()
+ {
+ return receiverId;
+ }
+
+ /**
+ * Group tag of the source that sent the error frame.
+ *
+ * @return group tag of the source that sent the error frame, null
if the source did not have a group
+ * tag set.
+ */
+ public Long groupTag()
+ {
+ return groupTag;
+ }
+
+ /**
+ * The error code of the error frame received.
+ *
+ * @return the error code.
+ */
+ public int errorCode()
+ {
+ return errorCode;
+ }
+
+ /**
+ * The error message of the error frame received.
+ *
+ * @return the error message.
+ */
+ public String errorMessage()
+ {
+ return errorMessage;
+ }
+
+ /**
+ * The address of the remote source that sent the error frame.
+ *
+ * @return address of the remote source.
+ */
+ public InetSocketAddress sourceAddress()
+ {
+ return sourceAddress;
+ }
+
+ /**
+ * The registrationId of the destination. Only used with manual MDC publications. Will be
+ * {@link io.aeron.Aeron#NULL_VALUE} otherwise.
+ *
+ * @return registrationId of the destination or {@link io.aeron.Aeron#NULL_VALUE}.
+ */
+ public long destinationRegistrationId()
+ {
+ return destinationRegistrationId;
+ }
+
+ /**
+ * Set the fields of the publication error frame from the flyweight.
+ *
+ * @param frameFlyweight that was received from the client message buffer.
+ * @return this for fluent API.
+ */
+ public PublicationErrorFrame set(final PublicationErrorFrameFlyweight frameFlyweight)
+ {
+ registrationId = frameFlyweight.registrationId();
+ sessionId = frameFlyweight.sessionId();
+ streamId = frameFlyweight.streamId();
+ receiverId = frameFlyweight.receiverId();
+ groupTag = frameFlyweight.groupTag();
+ sourceAddress = frameFlyweight.sourceAddress();
+ errorCode = frameFlyweight.errorCode().value();
+ errorMessage = frameFlyweight.errorMessage();
+ destinationRegistrationId = frameFlyweight.destinationRegistrationId();
+
+ return this;
+ }
+
+ /**
+ * Return a copy of this message. Useful if a callback is reusing an instance of this class to avoid unnecessary
+ * allocation.
+ *
+ * @return a copy of this instance's data.
+ */
+ public PublicationErrorFrame clone()
+ {
+ try
+ {
+ return (PublicationErrorFrame)super.clone();
+ }
+ catch (final CloneNotSupportedException ex)
+ {
+ throw new RuntimeException(ex);
+ }
+ }
+}
diff --git a/aeron-client/src/test/cpp_wrapper/CMakeLists.txt b/aeron-client/src/test/cpp_wrapper/CMakeLists.txt
index 11a6cb89e5..6647185821 100644
--- a/aeron-client/src/test/cpp_wrapper/CMakeLists.txt
+++ b/aeron-client/src/test/cpp_wrapper/CMakeLists.txt
@@ -74,5 +74,6 @@ if (AERON_UNIT_TESTS)
aeron_client_wrapper_test(imageFragmentAssemblerTest ImageFragmentAssemblerTest.cpp)
aeron_client_wrapper_test(controlledFragmentAssemblerTest ControlledFragmentAssemblerTest.cpp)
aeron_client_wrapper_test(imageControlledFragmentAssemblerTest ImageControlledFragmentAssemblerTest.cpp)
+ aeron_client_wrapper_test(rejectImageTest RejectImageTest.cpp)
endif ()
\ No newline at end of file
diff --git a/aeron-client/src/test/cpp_wrapper/RejectImageTest.cpp b/aeron-client/src/test/cpp_wrapper/RejectImageTest.cpp
new file mode 100644
index 0000000000..250cb2b4cf
--- /dev/null
+++ b/aeron-client/src/test/cpp_wrapper/RejectImageTest.cpp
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+#include
+
+#include
+
+#include "EmbeddedMediaDriver.h"
+#include "Aeron.h"
+#include "TestUtil.h"
+#include "aeron_socket.h"
+
+using namespace aeron;
+
+class RejectImageTest : public testing::TestWithParam
+{
+public:
+ RejectImageTest()
+ {
+ m_driver.start();
+ }
+
+ ~RejectImageTest() override
+ {
+ m_driver.stop();
+ }
+
+ static std::int32_t typeId(CountersReader &reader, std::int32_t counterId)
+ {
+ const index_t offset = aeron::concurrent::CountersReader::metadataOffset(counterId);
+ return reader.metaDataBuffer().getInt32(offset + CountersReader::TYPE_ID_OFFSET);
+ }
+
+ static std::string addressAsString(int16_t addressType, uint8_t *address)
+ {
+ if (AERON_RESPONSE_ADDRESS_TYPE_IPV4 == addressType)
+ {
+ char buffer[INET_ADDRSTRLEN] = {};
+ inet_ntop(AF_INET, address, buffer, sizeof(buffer));
+ return std::string{buffer};
+ }
+ else if (AERON_RESPONSE_ADDRESS_TYPE_IPV6 == addressType)
+ {
+ char buffer[INET6_ADDRSTRLEN] = {};
+ inet_ntop(AF_INET6, address, buffer, sizeof(buffer));
+ return "[" + std::string{buffer} + "]";
+ }
+
+ return "";
+ }
+
+ static std::shared_ptr connectClient(std::atomic &counter)
+ {
+ Context ctx;
+ ctx.useConductorAgentInvoker(true);
+
+ on_publication_error_frame_t errorFrameHandler =
+ [&](aeron::status::PublicationErrorFrame &errorFrame)
+ {
+ std::atomic_fetch_add(&counter, 1);
+ };
+
+ ctx.errorFrameHandler(errorFrameHandler);
+ return Aeron::connect(ctx);
+ }
+
+protected:
+ EmbeddedMediaDriver m_driver;
+};
+
+INSTANTIATE_TEST_SUITE_P(
+ RejectImageTestWithParam, RejectImageTest, testing::Values("127.0.0.1", "[::1]"));
+
+TEST_P(RejectImageTest, shouldRejectImage)
+{
+ Context ctx;
+ ctx.useConductorAgentInvoker(true);
+ std::string address = GetParam();
+ std::uint16_t port = 10000;
+ std::string control = address + ":" + std::to_string(10001);
+ std::string endpoint = address + ":" + std::to_string(port);
+
+ aeron::status::PublicationErrorFrame error{ nullptr };
+
+ on_publication_error_frame_t errorFrameHandler =
+ [&](aeron::status::PublicationErrorFrame &errorFrame)
+ {
+ error = errorFrame;
+ return;
+ };
+
+ ctx.errorFrameHandler(errorFrameHandler);
+ std::shared_ptr aeron = Aeron::connect(ctx);
+ AgentInvoker &invoker = aeron->conductorAgentInvoker();
+ invoker.start();
+
+ std::int64_t groupTag = 99999;
+ std::string mdc = "aeron:udp?control-mode=dynamic|control=" + control + "|fc=tagged,g:" + std::to_string(groupTag);
+ std::string channel = "aeron:udp?endpoint=" + endpoint + "|control=" + control + "|gtag=" + std::to_string(groupTag);
+
+ std::int64_t pubId = aeron->addPublication(mdc, 10000);
+ std::int64_t subId = aeron->addSubscription(channel, 10000);
+ invoker.invoke();
+
+ POLL_FOR_NON_NULL(pub, aeron->findPublication(pubId), invoker);
+ POLL_FOR_NON_NULL(sub, aeron->findSubscription(subId), invoker);
+ POLL_FOR(pub->isConnected() && sub->isConnected(), invoker);
+
+ std::string message = "Hello World!";
+
+ auto *data = reinterpret_cast(message.c_str());
+ POLL_FOR(0 < pub->offer(data, message.length()), invoker);
+ POLL_FOR(0 < sub->poll(
+ [&](concurrent::AtomicBuffer &buffer, util::index_t offset, util::index_t length, Header &header)
+ {
+ EXPECT_EQ(message, buffer.getStringWithoutLength(offset, length));
+ },
+ 1), invoker);
+
+ POLL_FOR(1 == sub->imageCount(), invoker);
+
+ const std::shared_ptr image = sub->imageByIndex(0);
+ image->reject("No Longer Valid");
+
+ POLL_FOR(error.isValid(), invoker);
+ ASSERT_EQ(pubId, error.registrationId());
+ ASSERT_EQ(pub->sessionId(), error.sessionId());
+ ASSERT_EQ(pub->streamId(), error.streamId());
+ ASSERT_EQ(groupTag, error.groupTag());
+ ASSERT_EQ(port, error.sourcePort());
+ ASSERT_EQ(address, addressAsString(error.sourceAddressType(), error.sourceAddress()));
+}
+
+TEST_P(RejectImageTest, shouldRejectImageForExclusive)
+{
+ std::string address = GetParam();
+
+ Context ctx;
+ ctx.useConductorAgentInvoker(true);
+
+ std::atomic errorFrameCount{0};
+
+ on_publication_error_frame_t errorFrameHandler =
+ [&](aeron::status::PublicationErrorFrame &errorFrame)
+ {
+ std::atomic_fetch_add(&errorFrameCount, 1);
+ return;
+ };
+
+ ctx.errorFrameHandler(errorFrameHandler);
+ std::shared_ptr aeron = Aeron::connect(ctx);
+ AgentInvoker &invoker = aeron->conductorAgentInvoker();
+ invoker.start();
+
+ std::int64_t pubId = aeron->addExclusivePublication("aeron:udp?endpoint=" + address + ":10000", 10000);
+ std::int64_t subId = aeron->addSubscription("aeron:udp?endpoint=" + address + ":10000", 10000);
+ invoker.invoke();
+
+ POLL_FOR_NON_NULL(pub, aeron->findExclusivePublication(pubId), invoker);
+ POLL_FOR_NON_NULL(sub, aeron->findSubscription(subId), invoker);
+ POLL_FOR(pub->isConnected() && sub->isConnected(), invoker);
+
+ std::string message = "Hello World!";
+
+ auto *data = reinterpret_cast(message.c_str());
+ POLL_FOR(0 < pub->offer(data, message.length()), invoker);
+ POLL_FOR(0 < sub->poll(
+ [&](concurrent::AtomicBuffer &buffer, util::index_t offset, util::index_t length, Header &header)
+ {
+ EXPECT_EQ(message, buffer.getStringWithoutLength(offset, length));
+ },
+ 1), invoker);
+
+ POLL_FOR(1 == sub->imageCount(), invoker);
+
+ const std::shared_ptr image = sub->imageByIndex(0);
+ image->reject("No Longer Valid");
+
+ POLL_FOR(0 < errorFrameCount, invoker);
+}
+
+TEST_P(RejectImageTest, shouldOnlySeePublicationErrorFramesForPublicationsAddedToTheClient)
+{
+ const std::string address = GetParam();
+ const std::string channel = "aeron:udp?endpoint=" + address + ":10000";
+ const int streamId = 10000;
+
+ std::atomic errorFrameCount0{0};
+ std::shared_ptr aeron0 = connectClient(errorFrameCount0);
+ AgentInvoker &invoker0 = aeron0->conductorAgentInvoker();
+
+ std::atomic errorFrameCount1{0};
+ std::shared_ptr aeron1 = connectClient(errorFrameCount1);
+ AgentInvoker &invoker1 = aeron1->conductorAgentInvoker();
+
+ std::atomic errorFrameCount2{0};
+ std::shared_ptr aeron2 = connectClient(errorFrameCount2);
+ AgentInvoker &invoker2 = aeron2->conductorAgentInvoker();
+
+ invoker0.start();
+ invoker1.start();
+ invoker2.start();
+
+ std::int64_t pub0Id = aeron0->addPublication(channel, streamId);
+ std::int64_t subId = aeron0->addSubscription(channel, streamId);
+ invoker0.invoke();
+
+ POLL_FOR_NON_NULL(pub0, aeron0->findPublication(pub0Id), invoker0);
+ POLL_FOR_NON_NULL(sub, aeron0->findSubscription(subId), invoker0);
+ POLL_FOR(pub0->isConnected() && sub->isConnected(), invoker0);
+
+ std::int64_t pub1Id = aeron1->addPublication(channel, streamId);
+ invoker1.invoke();
+ POLL_FOR_NON_NULL(pub1, aeron1->findPublication(pub1Id), invoker1);
+
+ std::string message = "Hello World!";
+
+ auto *data = reinterpret_cast(message.c_str());
+ POLL_FOR(0 < pub0->offer(data, message.length()), invoker0);
+ POLL_FOR(0 < sub->poll(
+ [&](concurrent::AtomicBuffer &buffer, util::index_t offset, util::index_t length, Header &header)
+ {
+ EXPECT_EQ(message, buffer.getStringWithoutLength(offset, length));
+ },
+ 1), invoker0);
+
+ POLL_FOR(1 == sub->imageCount(), invoker0);
+
+ const std::shared_ptr image = sub->imageByIndex(0);
+ image->reject("No Longer Valid");
+
+ POLL_FOR(0 < errorFrameCount0, invoker0);
+ POLL_FOR(0 < errorFrameCount1, invoker1);
+
+ int64_t timeout_ms = aeron_epoch_clock() + 500;
+ while (aeron_epoch_clock() < timeout_ms)
+ {
+ invoker2.invoke();
+ ASSERT_EQ(0, errorFrameCount2);
+ std::this_thread::sleep_for(std::chrono::duration(1));
+ }
+}
diff --git a/aeron-client/src/test/cpp_wrapper/WrapperSystemTest.cpp b/aeron-client/src/test/cpp_wrapper/WrapperSystemTest.cpp
index ff462cd758..b194c890dd 100644
--- a/aeron-client/src/test/cpp_wrapper/WrapperSystemTest.cpp
+++ b/aeron-client/src/test/cpp_wrapper/WrapperSystemTest.cpp
@@ -123,4 +123,4 @@ TEST_F(WrapperSystemTest, shouldRejectClientNameThatIsTooLong)
const char *string = strstr(ex.what(), "client_name length must <= 100");
ASSERT_NE(nullptr, string) << ex.what();
}
-}
\ No newline at end of file
+}
diff --git a/aeron-client/src/test/java/io/aeron/ImageTest.java b/aeron-client/src/test/java/io/aeron/ImageTest.java
index 1eb3b276c9..110605c950 100644
--- a/aeron-client/src/test/java/io/aeron/ImageTest.java
+++ b/aeron-client/src/test/java/io/aeron/ImageTest.java
@@ -35,6 +35,7 @@
import static org.agrona.BitUtil.align;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
@@ -609,6 +610,27 @@ void shouldPollFragmentsToBoundedFragmentHandlerWithMaxPositionAboveIntMaxValue(
inOrder.verify(position).setOrdered(TERM_BUFFER_LENGTH);
}
+ @Test
+ void shouldRejectFragment()
+ {
+ final int initialOffset = TERM_BUFFER_LENGTH - (ALIGNED_FRAME_LENGTH * 2);
+ final long initialPosition = computePosition(
+ INITIAL_TERM_ID, initialOffset, POSITION_BITS_TO_SHIFT, INITIAL_TERM_ID);
+ position.setOrdered(initialPosition);
+ final Image image = createImage();
+
+ insertDataFrame(INITIAL_TERM_ID, initialOffset);
+ insertPaddingFrame(INITIAL_TERM_ID, initialOffset + ALIGNED_FRAME_LENGTH);
+
+ assertEquals(initialPosition, image.position());
+
+ final String reason = "this is frame is to be rejected";
+ image.reject(reason);
+
+ verify(subscription).rejectImage(image.correlationId(), image.position(), reason);
+ }
+
+
private Image createImage()
{
return new Image(subscription, SESSION_ID, position, logBuffers, errorHandler, SOURCE_IDENTITY, CORRELATION_ID);
diff --git a/aeron-client/src/test/java/io/aeron/protocol/ErrorFlyweightTest.java b/aeron-client/src/test/java/io/aeron/protocol/ErrorFlyweightTest.java
new file mode 100644
index 0000000000..7331f28887
--- /dev/null
+++ b/aeron-client/src/test/java/io/aeron/protocol/ErrorFlyweightTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://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.junit.jupiter.api.Test;
+
+import static io.aeron.protocol.ErrorFlyweight.HAS_GROUP_ID_FLAG;
+import static io.aeron.protocol.ErrorFlyweight.MAX_ERROR_FRAME_LENGTH;
+import static java.nio.ByteBuffer.allocate;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+class ErrorFlyweightTest
+{
+ @Test
+ void shouldCorrectlySetFlagsForGroupTag()
+ {
+ final ErrorFlyweight errorFlyweight = new ErrorFlyweight(allocate(MAX_ERROR_FRAME_LENGTH));
+
+ assertEquals(0, errorFlyweight.flags());
+ errorFlyweight.groupTag(10L);
+ assertEquals(HAS_GROUP_ID_FLAG, errorFlyweight.flags());
+
+ errorFlyweight.groupTag(null);
+ assertNotEquals(HAS_GROUP_ID_FLAG, errorFlyweight.flags());
+ }
+
+ @Test
+ void shouldCorrectlyUpdateExistingFlagsForGroupTag()
+ {
+ final ErrorFlyweight errorFlyweight = new ErrorFlyweight(allocate(MAX_ERROR_FRAME_LENGTH));
+
+ final short initialFlags = (short)0x3;
+
+ errorFlyweight.flags(initialFlags);
+ assertEquals(initialFlags, errorFlyweight.flags());
+ errorFlyweight.groupTag(10L);
+ assertEquals(HAS_GROUP_ID_FLAG, HAS_GROUP_ID_FLAG & errorFlyweight.flags());
+ assertEquals(initialFlags, initialFlags & errorFlyweight.flags());
+
+ errorFlyweight.groupTag(null);
+ assertEquals(0, HAS_GROUP_ID_FLAG & errorFlyweight.flags());
+ assertEquals(initialFlags, errorFlyweight.flags());
+ }
+}
\ No newline at end of file
diff --git a/aeron-driver/src/main/c/aeron_driver_conductor.c b/aeron-driver/src/main/c/aeron_driver_conductor.c
index 255f5a37c0..0fd906f665 100644
--- a/aeron-driver/src/main/c/aeron_driver_conductor.c
+++ b/aeron-driver/src/main/c/aeron_driver_conductor.c
@@ -1875,6 +1875,22 @@ static int aeron_driver_conductor_find_response_publication_image(
return -1;
}
+static aeron_publication_image_t * aeron_driver_conductor_find_publication_image(
+ aeron_driver_conductor_t *conductor,
+ int64_t correlation_id)
+{
+ for (size_t i = 0; i < conductor->publication_images.length; i++)
+ {
+ aeron_publication_image_t *image_entry = conductor->publication_images.array[i].image;
+ if (aeron_publication_image_registration_id(image_entry) == correlation_id)
+ {
+ return image_entry;
+ }
+ }
+
+ return NULL;
+}
+
aeron_network_publication_t *aeron_driver_conductor_get_or_add_network_publication(
aeron_driver_conductor_t *conductor,
aeron_client_t *client,
@@ -2552,6 +2568,50 @@ void on_error(
aeron_driver_conductor_client_transmit(conductor, AERON_RESPONSE_ON_ERROR, response, response_length);
}
+void aeron_driver_conductor_on_publication_error(void *clientd, void *item)
+{
+ uint8_t buffer[sizeof(aeron_publication_error_t) + (AERON_ERROR_MAX_TEXT_LENGTH - 1)];
+ aeron_driver_conductor_t *conductor = clientd;
+ aeron_command_publication_error_t *error = item;
+ aeron_driver_conductor_log_explicit_error(conductor, error->error_code, (const char *)error->error_text);
+
+ aeron_publication_error_t *response = (aeron_publication_error_t *)buffer;
+ response->error_code = error->error_code;
+ response->registration_id = error->registration_id;
+ response->destination_registration_id = error->destination_registration_id;
+ response->session_id = error->session_id;
+ response->stream_id = error->stream_id;
+ response->receiver_id = error->receiver_id;
+ response->group_tag = error->group_tag;
+
+ memset(&response->source_address[0], 0, sizeof(response->source_address));
+ if (AF_INET == error->src_address.ss_family)
+ {
+ struct sockaddr_in *src_addr_in = (struct sockaddr_in *)&error->src_address;
+ response->address_type = AERON_RESPONSE_ADDRESS_TYPE_IPV4;
+ response->source_port = ntohs(src_addr_in->sin_port);
+ memcpy(&response->source_address[0], &src_addr_in->sin_addr, sizeof(src_addr_in->sin_addr));
+ }
+ else if (AF_INET6 == error->src_address.ss_family)
+ {
+ struct sockaddr_in6 *src_addr_in6 = (struct sockaddr_in6 *)&error->src_address;
+ response->address_type = AERON_RESPONSE_ADDRESS_TYPE_IPV6;
+ response->source_port = ntohs(src_addr_in6->sin6_port);
+ memcpy(&response->source_address[0], &src_addr_in6->sin6_addr, sizeof(src_addr_in6->sin6_addr));
+ }
+ else
+ {
+ response->address_type = 0;
+ response->source_port = 0;
+ }
+
+ response->error_message_length = error->error_length;
+ memcpy(response->error_message, error->error_text, error->error_length);
+ size_t response_length = offsetof(aeron_publication_error_values_t, error_message) + response->error_message_length;
+
+ aeron_driver_conductor_client_transmit(conductor, AERON_RESPONSE_ON_PUBLICATION_ERROR, response, response_length);
+}
+
void aeron_driver_conductor_on_error(
aeron_driver_conductor_t *conductor,
int32_t errcode,
@@ -3317,6 +3377,26 @@ aeron_rb_read_action_t aeron_driver_conductor_on_command(
break;
}
+ case AERON_COMMAND_REJECT_IMAGE:
+ {
+ aeron_reject_image_command_t *command = (aeron_reject_image_command_t *)message;
+ correlation_id = command->correlated.correlation_id;
+
+ if (length < sizeof (aeron_reject_image_command_t))
+ {
+ goto malformed_command;
+ }
+
+ if (length < offsetof(aeron_reject_image_command_t, reason_text) + command->reason_length)
+ {
+ goto malformed_command;
+ }
+
+ result = aeron_driver_conductor_on_invalidate_image(conductor, command);
+
+ break;
+ }
+
case AERON_COMMAND_REMOVE_DESTINATION_BY_ID:
{
aeron_destination_by_id_command_t *command = (aeron_destination_by_id_command_t *)message;
@@ -5634,6 +5714,37 @@ int aeron_driver_conductor_on_terminate_driver(
return 0;
}
+int aeron_driver_conductor_on_invalidate_image(
+ aeron_driver_conductor_t *conductor, aeron_reject_image_command_t *command)
+{
+ const int64_t image_correlation_id = command->image_correlation_id;
+ aeron_publication_image_t *image = aeron_driver_conductor_find_publication_image(
+ conductor, image_correlation_id);
+
+ if (AERON_ERROR_MAX_TEXT_LENGTH < command->reason_length)
+ {
+ AERON_SET_ERR(AERON_ERROR_CODE_GENERIC_ERROR, "%s", "Invalidation reason_text must be 1023 bytes or less");
+ return -1;
+ }
+
+ if (NULL == image)
+ {
+ AERON_SET_ERR(
+ AERON_ERROR_CODE_GENERIC_ERROR,
+ "Unable to resolve image for correlationId=%" PRId64, image_correlation_id);
+ return -1;
+ }
+
+ const char *reason_text = (const char *)command->reason_text;
+ aeron_driver_receiver_proxy_on_invalidate_image(
+ conductor->context->receiver_proxy, image_correlation_id, command->position, command->reason_length, reason_text);
+
+ aeron_driver_conductor_on_operation_succeeded(conductor, command->correlated.correlation_id);
+
+ return 0;
+}
+
+
void aeron_driver_conductor_on_create_publication_image(void *clientd, void *item)
{
aeron_driver_conductor_t *conductor = (aeron_driver_conductor_t *)clientd;
diff --git a/aeron-driver/src/main/c/aeron_driver_conductor.h b/aeron-driver/src/main/c/aeron_driver_conductor.h
index 67ad3ed0bf..c09b3d409b 100644
--- a/aeron-driver/src/main/c/aeron_driver_conductor.h
+++ b/aeron-driver/src/main/c/aeron_driver_conductor.h
@@ -522,6 +522,9 @@ int aeron_driver_conductor_on_client_close(aeron_driver_conductor_t *conductor,
int aeron_driver_conductor_on_terminate_driver(
aeron_driver_conductor_t *conductor, aeron_terminate_driver_command_t *command);
+int aeron_driver_conductor_on_invalidate_image(
+ aeron_driver_conductor_t *conductor, aeron_reject_image_command_t *command);
+
void aeron_driver_conductor_on_create_publication_image(void *clientd, void *item);
void aeron_driver_conductor_on_re_resolve_endpoint(void *clientd, void *item);
@@ -534,6 +537,8 @@ void aeron_driver_conductor_on_response_setup(void *clientd, void *item);
void aeron_driver_conductor_on_response_connected(void *clientd, void *item);
+void aeron_driver_conductor_on_publication_error(void *clientd, void *item);
+
void aeron_driver_conductor_on_release_resource(void *clientd, void *item);
aeron_send_channel_endpoint_t *aeron_driver_conductor_find_send_channel_endpoint_by_tag(
diff --git a/aeron-driver/src/main/c/aeron_driver_conductor_proxy.c b/aeron-driver/src/main/c/aeron_driver_conductor_proxy.c
index b6bbae5605..a96a7a2387 100644
--- a/aeron-driver/src/main/c/aeron_driver_conductor_proxy.c
+++ b/aeron-driver/src/main/c/aeron_driver_conductor_proxy.c
@@ -19,6 +19,8 @@
#include "aeron_alloc.h"
#include "aeron_driver_conductor.h"
+#define AERON_COMMAND_PUBLICATION_ERROR_MAX_LENGTH (sizeof(aeron_command_publication_error_t) + AERON_ERROR_MAX_TEXT_LENGTH)
+
void aeron_driver_conductor_proxy_offer(aeron_driver_conductor_proxy_t *conductor_proxy, void *cmd, size_t length)
{
aeron_rb_write_result_t result;
@@ -240,7 +242,7 @@ void aeron_driver_conductor_proxy_on_release_resource(
void *managed_resource,
aeron_driver_conductor_resource_type_t resource_type)
{
- aeron_command_release_resource_t cmd =
+ aeron_command_release_resource_t cmd =
{
.base =
{
@@ -259,3 +261,48 @@ void aeron_driver_conductor_proxy_on_release_resource(
aeron_driver_conductor_proxy_offer(conductor_proxy, &cmd, sizeof(cmd));
}
}
+
+void aeron_driver_conductor_proxy_on_publication_error(
+ aeron_driver_conductor_proxy_t *conductor_proxy,
+ const int64_t registration_id,
+ const int64_t destination_registration_id,
+ int32_t session_id,
+ int32_t stream_id,
+ int64_t receiver_id,
+ int64_t group_tag,
+ struct sockaddr_storage *src_address,
+ int32_t error_code,
+ int32_t error_length,
+ const uint8_t *error_text)
+{
+ uint8_t buffer[AERON_COMMAND_PUBLICATION_ERROR_MAX_LENGTH];
+ aeron_command_publication_error_t *error = (aeron_command_publication_error_t *)buffer;
+ error_length = error_length <= AERON_ERROR_MAX_TEXT_LENGTH ? error_length : AERON_ERROR_MAX_TEXT_LENGTH;
+
+ error->base.func = aeron_driver_conductor_on_publication_error;
+ error->base.item = NULL;
+ error->registration_id = registration_id;
+ error->destination_registration_id = destination_registration_id;
+ error->session_id = session_id;
+ error->stream_id = stream_id;
+ error->receiver_id = receiver_id;
+ error->group_tag = group_tag;
+ memset(&error->src_address, 0, sizeof(struct sockaddr_storage));
+ memcpy(&error->src_address, src_address, AERON_ADDR_LEN(src_address));
+ error->error_code = error_code;
+ error->error_length = error_length;
+ memcpy(error->error_text, error_text, (size_t)error_length);
+ aeron_str_null_terminate(error->error_text, error_length);
+
+ size_t cmd_length = sizeof(aeron_command_publication_error_t) + error_length + 1;
+
+ if (AERON_THREADING_MODE_IS_SHARED_OR_INVOKER(conductor_proxy->threading_mode))
+ {
+ aeron_driver_conductor_on_publication_error(conductor_proxy->conductor, error);
+ }
+ else
+ {
+ aeron_driver_conductor_proxy_offer(conductor_proxy, (void *)error, cmd_length);
+ }
+}
+
diff --git a/aeron-driver/src/main/c/aeron_driver_conductor_proxy.h b/aeron-driver/src/main/c/aeron_driver_conductor_proxy.h
index 9a08a7249c..10228dbb48 100644
--- a/aeron-driver/src/main/c/aeron_driver_conductor_proxy.h
+++ b/aeron-driver/src/main/c/aeron_driver_conductor_proxy.h
@@ -100,6 +100,22 @@ struct aeron_command_release_resource_stct
};
typedef struct aeron_command_release_resource_stct aeron_command_release_resource_t;
+struct aeron_command_publication_error_stct
+{
+ aeron_command_base_t base;
+ int64_t registration_id;
+ int64_t destination_registration_id;
+ int32_t session_id;
+ int32_t stream_id;
+ int64_t receiver_id;
+ int64_t group_tag;
+ struct sockaddr_storage src_address;
+ int32_t error_code;
+ int32_t error_length;
+ uint8_t error_text[1];
+};
+typedef struct aeron_command_publication_error_stct aeron_command_publication_error_t;
+
void aeron_driver_conductor_proxy_on_create_publication_image_cmd(
aeron_driver_conductor_proxy_t *conductor_proxy,
int32_t session_id,
@@ -156,5 +172,17 @@ void aeron_driver_conductor_proxy_on_release_resource(
void *managed_resource,
aeron_driver_conductor_resource_type_t resource_type);
+void aeron_driver_conductor_proxy_on_publication_error(
+ aeron_driver_conductor_proxy_t *conductor_proxy,
+ const int64_t registration_id,
+ const int64_t destination_registration_id,
+ int32_t session_id,
+ int32_t stream_id,
+ int64_t receiver_id,
+ int64_t group_tag,
+ struct sockaddr_storage *src_address,
+ int32_t error_code,
+ int32_t error_length,
+ const uint8_t *error_text);
#endif //AERON_DRIVER_CONDUCTOR_PROXY_H
diff --git a/aeron-driver/src/main/c/aeron_driver_receiver.c b/aeron-driver/src/main/c/aeron_driver_receiver.c
index d0cb3fa853..f6ba44f8f0 100644
--- a/aeron-driver/src/main/c/aeron_driver_receiver.c
+++ b/aeron-driver/src/main/c/aeron_driver_receiver.c
@@ -582,6 +582,26 @@ void aeron_driver_receiver_on_resolution_change(void *clientd, void *item)
aeron_receive_channel_endpoint_update_control_address(endpoint, destination, &cmd->new_addr);
}
+void aeron_driver_receiver_on_invalidate_image(void *clientd, void *item)
+{
+ aeron_driver_receiver_t *receiver = clientd;
+ aeron_command_receiver_invalidate_image_t *cmd = item;
+ const int64_t correlation_id = cmd->image_correlation_id;
+ const int32_t reason_length = cmd->reason_length;
+ const char *reason = (const char *)cmd->reason_text;
+
+ for (size_t i = 0, size = receiver->images.length; i < size; i++)
+ {
+ aeron_publication_image_t *image = receiver->images.array[i].image;
+ // TODO: Should we pass the pointer to the image here instead of the correlation_id.
+ if (correlation_id == aeron_publication_image_registration_id(image))
+ {
+ aeron_publication_image_invalidate(image, reason_length, reason);
+ break;
+ }
+ }
+}
+
int aeron_driver_receiver_add_pending_setup(
aeron_driver_receiver_t *receiver,
aeron_receive_channel_endpoint_t *endpoint,
diff --git a/aeron-driver/src/main/c/aeron_driver_receiver.h b/aeron-driver/src/main/c/aeron_driver_receiver.h
index 6ab7b85040..77874ca5de 100644
--- a/aeron-driver/src/main/c/aeron_driver_receiver.h
+++ b/aeron-driver/src/main/c/aeron_driver_receiver.h
@@ -123,6 +123,8 @@ void aeron_driver_receiver_on_remove_matching_state(void *clientd, void *item);
void aeron_driver_receiver_on_resolution_change(void *clientd, void *item);
+void aeron_driver_receiver_on_invalidate_image(void *clientd, void *item);
+
int aeron_driver_receiver_add_pending_setup(
aeron_driver_receiver_t *receiver,
aeron_receive_channel_endpoint_t *endpoint,
diff --git a/aeron-driver/src/main/c/aeron_driver_receiver_proxy.c b/aeron-driver/src/main/c/aeron_driver_receiver_proxy.c
index 83a1426232..ffdb66958d 100644
--- a/aeron-driver/src/main/c/aeron_driver_receiver_proxy.c
+++ b/aeron-driver/src/main/c/aeron_driver_receiver_proxy.c
@@ -351,3 +351,32 @@ void aeron_driver_receiver_proxy_on_resolution_change(
aeron_driver_receiver_proxy_offer(receiver_proxy, &cmd, sizeof(cmd));
}
}
+
+void aeron_driver_receiver_proxy_on_invalidate_image(
+ aeron_driver_receiver_proxy_t *receiver_proxy,
+ int64_t image_correlation_id,
+ int64_t position,
+ int32_t reason_length,
+ const char *reason)
+{
+ reason_length = reason_length <= AERON_ERROR_MAX_TEXT_LENGTH ? reason_length : AERON_ERROR_MAX_TEXT_LENGTH;
+ uint8_t message_buffer[sizeof(aeron_command_base_t) + AERON_ERROR_MAX_TEXT_LENGTH + 1];
+ aeron_command_receiver_invalidate_image_t *cmd = (aeron_command_receiver_invalidate_image_t *)message_buffer;
+
+ cmd->base.func = aeron_driver_receiver_on_invalidate_image;
+ cmd->base.item = NULL;
+ cmd->image_correlation_id = image_correlation_id;
+ cmd->position = position;
+ cmd->reason_length = reason_length;
+ memcpy(cmd->reason_text, reason, reason_length);
+ aeron_str_null_terminate(cmd->reason_text, reason_length);
+
+ if (AERON_THREADING_MODE_IS_SHARED_OR_INVOKER(receiver_proxy->threading_mode))
+ {
+ aeron_driver_receiver_on_invalidate_image(receiver_proxy->receiver, cmd);
+ }
+ else
+ {
+ aeron_driver_receiver_proxy_offer(receiver_proxy, cmd, sizeof(*cmd) + reason_length);
+ }
+}
diff --git a/aeron-driver/src/main/c/aeron_driver_receiver_proxy.h b/aeron-driver/src/main/c/aeron_driver_receiver_proxy.h
index 0747f4f040..399e0ebc17 100644
--- a/aeron-driver/src/main/c/aeron_driver_receiver_proxy.h
+++ b/aeron-driver/src/main/c/aeron_driver_receiver_proxy.h
@@ -123,6 +123,27 @@ typedef struct aeron_command_on_remove_matching_state_stct
}
aeron_command_on_remove_matching_state_t;
+typedef struct aeron_command_receiver_resolution_change_stct
+{
+ aeron_command_base_t base;
+ const char *endpoint_name;
+ void *endpoint;
+ void *destination;
+ struct sockaddr_storage new_addr;
+}
+aeron_command_receiver_resolution_change_t;
+
+typedef struct aeron_command_receiver_invalidate_image_stct
+{
+ aeron_command_base_t base;
+ int64_t image_correlation_id;
+ int64_t position;
+ int32_t reason_length;
+ uint8_t reason_text[1];
+}
+aeron_command_receiver_invalidate_image_t;
+
+
void aeron_driver_receiver_proxy_on_add_publication_image(
aeron_driver_receiver_proxy_t *receiver_proxy,
aeron_receive_channel_endpoint_t *endpoint,
@@ -140,17 +161,12 @@ void aeron_driver_receiver_proxy_on_remove_init_in_progress(
aeron_receive_channel_endpoint_t *endpoint,
int32_t session_id,
int32_t stream_id);
-
-typedef struct aeron_command_receiver_resolution_change_stct
-{
- aeron_command_base_t base;
- const char *endpoint_name;
- void *endpoint;
- void *destination;
- struct sockaddr_storage new_addr;
-}
-aeron_command_receiver_resolution_change_t;
-
+void aeron_driver_receiver_proxy_on_invalidate_image(
+ aeron_driver_receiver_proxy_t *receiver_proxy,
+ int64_t image_correlation_id,
+ int64_t position,
+ int32_t reason_length,
+ const char *reason);
void aeron_driver_receiver_proxy_on_resolution_change(
aeron_driver_receiver_proxy_t *receiver_proxy,
const char *endpoint_name,
diff --git a/aeron-driver/src/main/c/aeron_driver_sender.c b/aeron-driver/src/main/c/aeron_driver_sender.c
index dd64e321c2..84a5088b20 100644
--- a/aeron-driver/src/main/c/aeron_driver_sender.c
+++ b/aeron-driver/src/main/c/aeron_driver_sender.c
@@ -104,6 +104,8 @@ int aeron_driver_sender_init(
system_counters, AERON_SYSTEM_COUNTER_STATUS_MESSAGES_RECEIVED);
sender->nak_messages_received_counter = aeron_system_counter_addr(
system_counters, AERON_SYSTEM_COUNTER_NAK_MESSAGES_RECEIVED);
+ sender->error_messages_received_counter = aeron_system_counter_addr(
+ system_counters, AERON_SYSTEM_COUNTER_ERROR_FRAMES_RECEIVED);
sender->resolution_changes_counter = aeron_system_counter_addr(
system_counters, AERON_SYSTEM_COUNTER_RESOLUTION_CHANGES);
sender->short_sends_counter = aeron_system_counter_addr(system_counters, AERON_SYSTEM_COUNTER_SHORT_SENDS);
diff --git a/aeron-driver/src/main/c/aeron_driver_sender.h b/aeron-driver/src/main/c/aeron_driver_sender.h
index 5c264ccbf0..c0c7a7e800 100644
--- a/aeron-driver/src/main/c/aeron_driver_sender.h
+++ b/aeron-driver/src/main/c/aeron_driver_sender.h
@@ -59,6 +59,7 @@ typedef struct aeron_driver_sender_stct
volatile int64_t *invalid_frames_counter;
volatile int64_t *status_messages_received_counter;
volatile int64_t *nak_messages_received_counter;
+ volatile int64_t *error_messages_received_counter;
volatile int64_t *resolution_changes_counter;
volatile int64_t *short_sends_counter;
diff --git a/aeron-driver/src/main/c/aeron_flow_control.c b/aeron-driver/src/main/c/aeron_flow_control.c
index 0a29119d73..f20fb67c52 100644
--- a/aeron-driver/src/main/c/aeron_flow_control.c
+++ b/aeron-driver/src/main/c/aeron_flow_control.c
@@ -110,6 +110,15 @@ int64_t aeron_max_flow_control_strategy_on_sm(
return snd_lmt > window_edge ? snd_lmt : window_edge;
}
+void aeron_max_flow_control_strategy_on_error(
+ void *state,
+ const uint8_t *error,
+ size_t length,
+ struct sockaddr_storage *recv_addr,
+ int64_t now_ns)
+{
+}
+
size_t aeron_flow_control_calculate_retransmission_length(
size_t resend_length,
size_t term_buffer_length,
@@ -192,6 +201,7 @@ int aeron_max_multicast_flow_control_strategy_supplier(
_strategy->on_idle = aeron_max_flow_control_strategy_on_idle;
_strategy->on_status_message = aeron_max_flow_control_strategy_on_sm;
_strategy->on_setup = aeron_max_flow_control_strategy_on_setup;
+ _strategy->on_error = aeron_max_flow_control_strategy_on_error;
_strategy->fini = aeron_max_flow_control_strategy_fini;
_strategy->has_required_receivers = aeron_flow_control_strategy_has_required_receivers_default;
_strategy->on_trigger_send_setup = aeron_max_flow_control_strategy_on_trigger_send_setup;
@@ -224,6 +234,7 @@ int aeron_unicast_flow_control_strategy_supplier(
_strategy->on_idle = aeron_max_flow_control_strategy_on_idle;
_strategy->on_status_message = aeron_max_flow_control_strategy_on_sm;
_strategy->on_setup = aeron_max_flow_control_strategy_on_setup;
+ _strategy->on_error = aeron_max_flow_control_strategy_on_error;
_strategy->fini = aeron_max_flow_control_strategy_fini;
_strategy->has_required_receivers = aeron_flow_control_strategy_has_required_receivers_default;
_strategy->on_trigger_send_setup = aeron_max_flow_control_strategy_on_trigger_send_setup;
diff --git a/aeron-driver/src/main/c/aeron_flow_control.h b/aeron-driver/src/main/c/aeron_flow_control.h
index 6a27fa53ba..3f5695d9d3 100644
--- a/aeron-driver/src/main/c/aeron_flow_control.h
+++ b/aeron-driver/src/main/c/aeron_flow_control.h
@@ -57,6 +57,13 @@ typedef int64_t (*aeron_flow_control_strategy_on_setup_func_t)(
size_t position_bits_to_shift,
int64_t snd_pos);
+typedef void (*aeron_flow_control_strategy_on_error_func_t)(
+ void *state,
+ const uint8_t *error,
+ size_t length,
+ struct sockaddr_storage *recv_addr,
+ int64_t now_ns);
+
typedef void (*aeron_flow_control_strategy_on_trigger_send_setup_func_t)(
void *state,
const uint8_t *sm,
@@ -80,6 +87,7 @@ typedef struct aeron_flow_control_strategy_stct
aeron_flow_control_strategy_on_sm_func_t on_status_message;
aeron_flow_control_strategy_on_idle_func_t on_idle;
aeron_flow_control_strategy_on_setup_func_t on_setup;
+ aeron_flow_control_strategy_on_error_func_t on_error;
aeron_flow_control_strategy_fini_func_t fini;
aeron_flow_control_strategy_has_required_receivers_func_t has_required_receivers;
aeron_flow_control_strategy_on_trigger_send_setup_func_t on_trigger_send_setup;
diff --git a/aeron-driver/src/main/c/aeron_min_flow_control.c b/aeron-driver/src/main/c/aeron_min_flow_control.c
index 7b2aad6863..47c138b811 100644
--- a/aeron-driver/src/main/c/aeron_min_flow_control.c
+++ b/aeron-driver/src/main/c/aeron_min_flow_control.c
@@ -295,6 +295,27 @@ int64_t aeron_min_flow_control_strategy_on_setup(
return snd_lmt;
}
+void aeron_min_flow_control_strategy_on_error(
+ void *state,
+ const uint8_t *error,
+ size_t length,
+ struct sockaddr_storage *recv_addr,
+ int64_t now_ns)
+{
+ aeron_min_flow_control_strategy_state_t *strategy_state = (aeron_min_flow_control_strategy_state_t *)state;
+ aeron_error_t *error_header = (aeron_error_t *)error;
+
+ for (size_t i = 0; i < strategy_state->receivers.length; i++)
+ {
+ aeron_min_flow_control_strategy_receiver_t *receiver = &strategy_state->receivers.array[i];
+
+ if (error_header->receiver_id == receiver->receiver_id)
+ {
+ receiver->eos_flagged = true;
+ }
+ }
+}
+
int64_t aeron_tagged_flow_control_strategy_on_sm(
void *state,
const uint8_t *sm,
@@ -513,6 +534,7 @@ int aeron_tagged_flow_control_strategy_supplier_init(
aeron_tagged_flow_control_strategy_on_sm : aeron_min_flow_control_strategy_on_sm;
_strategy->on_setup = is_group_tag_aware ?
aeron_tagged_flow_control_strategy_on_setup : aeron_min_flow_control_strategy_on_setup;
+ _strategy->on_error = aeron_min_flow_control_strategy_on_error;
_strategy->fini = aeron_min_flow_control_strategy_fini;
_strategy->has_required_receivers = aeron_min_flow_control_strategy_has_required_receivers;
_strategy->on_trigger_send_setup = is_group_tag_aware ?
diff --git a/aeron-driver/src/main/c/aeron_network_publication.c b/aeron-driver/src/main/c/aeron_network_publication.c
index 3ae8807f9b..b92559f525 100644
--- a/aeron-driver/src/main/c/aeron_network_publication.c
+++ b/aeron-driver/src/main/c/aeron_network_publication.c
@@ -40,11 +40,12 @@ struct mmsghdr
};
#endif
-static inline void aeron_network_publication_liveness_on_remote_close(
+static inline bool aeron_network_publication_liveness_on_remote_close(
aeron_network_publication_t *publication,
int64_t receiver_id)
{
- aeron_int64_counter_map_remove(&publication->receiver_liveness_tracker, receiver_id);
+ int64_t missing_value = publication->receiver_liveness_tracker.initial_value;
+ return missing_value != aeron_int64_counter_map_remove(&publication->receiver_liveness_tracker, receiver_id);
}
static inline int aeron_network_publication_liveness_on_status_message(
@@ -311,7 +312,7 @@ int aeron_network_publication_create(
_pub->is_response = AERON_UDP_CHANNEL_CONTROL_MODE_RESPONSE == endpoint->conductor_fields.udp_channel->control_mode;
_pub->response_correlation_id = params->response_correlation_id;
- aeron_int64_counter_map_init(&_pub->receiver_liveness_tracker, AERON_NULL_VALUE, 16, 0.6);
+ aeron_int64_counter_map_init(&_pub->receiver_liveness_tracker, AERON_NULL_VALUE, 16, 0.6f);
*publication = _pub;
@@ -831,6 +832,36 @@ void aeron_network_publication_on_status_message(
aeron_network_publication_has_required_receivers(publication));
}
+void aeron_network_publication_on_error(
+ aeron_network_publication_t *publication,
+ int64_t destination_registration_id,
+ const uint8_t *buffer,
+ size_t length,
+ struct sockaddr_storage *src_address,
+ aeron_driver_conductor_proxy_t *conductor_proxy)
+{
+ aeron_error_t *error = (aeron_error_t *)buffer;
+ const uint8_t *error_text = (const uint8_t *)(error + 1);
+ const int64_t time_ns = aeron_clock_cached_nano_time(publication->cached_clock);
+ publication->flow_control->on_error(publication->flow_control->state, buffer, length, src_address, time_ns);
+ if (aeron_network_publication_liveness_on_remote_close(publication, error->receiver_id))
+ {
+ const int64_t registration_id = aeron_network_publication_registration_id(publication);
+ aeron_driver_conductor_proxy_on_publication_error(
+ conductor_proxy,
+ registration_id,
+ destination_registration_id,
+ error->session_id,
+ error->stream_id,
+ error->receiver_id,
+ AERON_ERROR_HAS_GROUP_TAG_FLAG & error->frame_header.flags ? error->group_tag : AERON_NULL_VALUE,
+ src_address,
+ error->error_code,
+ error->error_length,
+ error_text);
+ }
+}
+
void aeron_network_publication_on_rttm(
aeron_network_publication_t *publication, const uint8_t *buffer, size_t length, struct sockaddr_storage *addr)
{
@@ -1233,3 +1264,5 @@ extern bool aeron_network_publication_has_sender_released(aeron_network_publicat
extern int64_t aeron_network_publication_max_spy_position(aeron_network_publication_t *publication, int64_t snd_pos);
extern bool aeron_network_publication_is_accepting_subscriptions(aeron_network_publication_t *publication);
+
+extern inline int64_t aeron_network_publication_registration_id(aeron_network_publication_t *publication);
diff --git a/aeron-driver/src/main/c/aeron_network_publication.h b/aeron-driver/src/main/c/aeron_network_publication.h
index b774abb9ba..0a82a37406 100644
--- a/aeron-driver/src/main/c/aeron_network_publication.h
+++ b/aeron-driver/src/main/c/aeron_network_publication.h
@@ -181,6 +181,14 @@ void aeron_network_publication_on_status_message(
size_t length,
struct sockaddr_storage *addr);
+void aeron_network_publication_on_error(
+ aeron_network_publication_t *publication,
+ int64_t destination_registration_id,
+ const uint8_t *buffer,
+ size_t length,
+ struct sockaddr_storage *src_address,
+ aeron_driver_conductor_proxy_t *pStct);
+
void aeron_network_publication_on_rttm(
aeron_network_publication_t *publication, const uint8_t *buffer, size_t length, struct sockaddr_storage *addr);
@@ -321,4 +329,9 @@ inline bool aeron_network_publication_is_accepting_subscriptions(aeron_network_p
aeron_counter_get_volatile(publication->snd_pos_position.value_addr));
}
+inline int64_t aeron_network_publication_registration_id(aeron_network_publication_t *publication)
+{
+ return publication->log_meta_data->correlation_id;
+}
+
#endif //AERON_NETWORK_PUBLICATION_H
diff --git a/aeron-driver/src/main/c/aeron_publication_image.c b/aeron-driver/src/main/c/aeron_publication_image.c
index c61e791399..73b3ef21cc 100644
--- a/aeron-driver/src/main/c/aeron_publication_image.c
+++ b/aeron-driver/src/main/c/aeron_publication_image.c
@@ -273,6 +273,7 @@ int aeron_publication_image_create(
_image->is_sending_eos_sm = false;
_image->has_receiver_released = false;
_image->sm_timeout_ns = (int64_t)context->status_message_timeout_ns;
+ _image->invalidation_reason = NULL;
memcpy(&_image->source_address, source_address, sizeof(_image->source_address));
const int source_identity_length = aeron_format_source_identity(
@@ -374,6 +375,7 @@ bool aeron_publication_image_free(aeron_publication_image_t *image)
aeron_counter_add_ordered(image->mapped_bytes_counter, -((int64_t)image->mapped_raw_log.mapped_file.length));
aeron_free(image->log_file_name);
+ aeron_free((void *)image->invalidation_reason);
aeron_free(image);
return true;
@@ -620,6 +622,11 @@ int aeron_publication_image_insert_packet(
return 0;
}
+ if (NULL != image->invalidation_reason)
+ {
+ return 0;
+ }
+
const bool is_heartbeat = aeron_publication_image_is_heartbeat(buffer, length);
const int64_t packet_position = aeron_logbuffer_compute_position(
term_id, term_offset, image->position_bits_to_shift, image->initial_term_id);
@@ -695,10 +702,33 @@ int aeron_publication_image_send_pending_status_message(aeron_publication_image_
{
int work_count = 0;
int64_t change_number;
- int32_t response_session_id = 0;
AERON_GET_VOLATILE(change_number, image->end_sm_change);
const bool has_sm_timed_out = now_ns > (image->time_of_last_sm_ns + image->sm_timeout_ns);
+ if (NULL != image->invalidation_reason)
+ {
+ if (has_sm_timed_out)
+ {
+ for (size_t i = 0, len = image->connections.length; i < len; i++)
+ {
+ aeron_publication_image_connection_t *connection = &image->connections.array[i];
+ aeron_receiver_channel_endpoint_send_error_frame(
+ image->endpoint,
+ connection->destination,
+ connection->control_addr,
+ image->session_id,
+ image->stream_id,
+ AERON_ERROR_CODE_GENERIC_ERROR,
+ image->invalidation_reason);
+ }
+
+ image->time_of_last_sm_ns = now_ns;
+ }
+
+ return 0;
+ }
+
+ int32_t response_session_id = 0;
if (has_sm_timed_out && aeron_publication_image_check_and_get_response_session_id(image, &response_session_id))
{
for (size_t i = 0, len = image->connections.length; i < len; i++)
@@ -1101,6 +1131,12 @@ void aeron_publication_image_receiver_release(aeron_publication_image_t *image)
AERON_PUT_ORDERED(image->has_receiver_released, true);
}
+void aeron_publication_image_invalidate(aeron_publication_image_t *image, int32_t reason_length, const char *reason)
+{
+ aeron_alloc((void **)&image->invalidation_reason, reason_length + 1);
+ memcpy((void *)image->invalidation_reason, reason, reason_length);
+}
+
void aeron_publication_image_remove_response_session_id(aeron_publication_image_t *image)
{
aeron_publication_image_set_response_session_id(image, AERON_PUBLICATION_RESPONSE_NULL_RESPONSE_SESSION_ID);
diff --git a/aeron-driver/src/main/c/aeron_publication_image.h b/aeron-driver/src/main/c/aeron_publication_image.h
index 68b3e62a18..088f3776e0 100644
--- a/aeron-driver/src/main/c/aeron_publication_image.h
+++ b/aeron-driver/src/main/c/aeron_publication_image.h
@@ -136,6 +136,7 @@ typedef struct aeron_publication_image_stct
int64_t sm_timeout_ns;
int64_t time_of_last_packet_ns;
+ const char *invalidation_reason;
volatile int64_t response_session_id;
@@ -218,6 +219,8 @@ void aeron_publication_image_on_time_event(
void aeron_publication_image_receiver_release(aeron_publication_image_t *image);
+void aeron_publication_image_invalidate(aeron_publication_image_t *image, int32_t reason_length, const char *reason);
+
inline bool aeron_publication_image_is_heartbeat(const uint8_t *buffer, size_t length)
{
return length == AERON_DATA_HEADER_LENGTH && 0 == ((aeron_frame_header_t *)buffer)->frame_length;
diff --git a/aeron-driver/src/main/c/aeron_system_counters.c b/aeron-driver/src/main/c/aeron_system_counters.c
index 75030b41d1..261ae536fc 100644
--- a/aeron-driver/src/main/c/aeron_system_counters.c
+++ b/aeron-driver/src/main/c/aeron_system_counters.c
@@ -60,7 +60,9 @@ static aeron_system_counter_t system_counters[] =
{ "Aeron software: version=" AERON_VERSION_TXT " commit=" AERON_VERSION_GITSHA, AERON_SYSTEM_COUNTER_AERON_VERSION },
{ "Bytes currently mapped", AERON_SYSTEM_COUNTER_BYTES_CURRENTLY_MAPPED },
{ "Retransmitted bytes", AERON_SYSTEM_COUNTER_RETRANSMITTED_BYTES },
- { "Retransmit Pool Overflow count", AERON_SYSTEM_COUNTER_RETRANSMIT_OVERFLOW }
+ { "Retransmit Pool Overflow count", AERON_SYSTEM_COUNTER_RETRANSMIT_OVERFLOW },
+ { "Error Frames received", AERON_SYSTEM_COUNTER_ERROR_FRAMES_RECEIVED },
+ { "Error Frames sent", AERON_SYSTEM_COUNTER_ERROR_FRAMES_SENT }
};
static size_t num_system_counters = sizeof(system_counters) / sizeof(aeron_system_counter_t);
diff --git a/aeron-driver/src/main/c/aeron_system_counters.h b/aeron-driver/src/main/c/aeron_system_counters.h
index 2824447bdf..b46749230b 100644
--- a/aeron-driver/src/main/c/aeron_system_counters.h
+++ b/aeron-driver/src/main/c/aeron_system_counters.h
@@ -59,6 +59,8 @@ typedef enum aeron_system_counter_enum_stct
AERON_SYSTEM_COUNTER_BYTES_CURRENTLY_MAPPED = 35,
AERON_SYSTEM_COUNTER_RETRANSMITTED_BYTES = 36,
AERON_SYSTEM_COUNTER_RETRANSMIT_OVERFLOW = 37,
+ AERON_SYSTEM_COUNTER_ERROR_FRAMES_RECEIVED = 38,
+ AERON_SYSTEM_COUNTER_ERROR_FRAMES_SENT = 39,
// Add all new counters before this one (used for a static assertion).
AERON_SYSTEM_COUNTER_DUMMY_LAST,
diff --git a/aeron-driver/src/main/c/agent/aeron_driver_agent.c b/aeron-driver/src/main/c/agent/aeron_driver_agent.c
index d734297fa0..0b453d51f4 100644
--- a/aeron-driver/src/main/c/agent/aeron_driver_agent.c
+++ b/aeron-driver/src/main/c/agent/aeron_driver_agent.c
@@ -131,6 +131,7 @@ static aeron_driver_agent_log_event_t log_events[] =
{ "SEND_NAK_MESSAGE", AERON_DRIVER_AGENT_EVENT_TYPE_OTHER, false },
{ "RESEND", AERON_DRIVER_AGENT_EVENT_TYPE_OTHER, false },
{ "CMD_IN_REMOVE_DESTINATION_BY_ID", AERON_DRIVER_AGENT_EVENT_TYPE_CMD_IN, false },
+ { "CMD_IN_REJECT_IMAGE", AERON_DRIVER_AGENT_EVENT_TYPE_CMD_IN, false },
{ "ADD_DYNAMIC_DISSECTOR", AERON_DRIVER_AGENT_EVENT_TYPE_OTHER, false },
{ "DYNAMIC_DISSECTOR_EVENT", AERON_DRIVER_AGENT_EVENT_TYPE_OTHER, false },
};
@@ -601,6 +602,9 @@ static aeron_driver_agent_event_t command_id_to_driver_event_id(const int32_t ms
case AERON_COMMAND_REMOVE_DESTINATION_BY_ID:
return AERON_DRIVER_EVENT_CMD_IN_REMOVE_DESTINATION_BY_ID;
+ case AERON_COMMAND_REJECT_IMAGE:
+ return AERON_DRIVER_EVENT_CMD_IN_REJECT_IMAGE;
+
default:
return AERON_DRIVER_EVENT_UNKNOWN_EVENT;
}
@@ -1557,6 +1561,22 @@ static const char *dissect_cmd_in(int64_t cmd_id, const void *message, size_t le
break;
}
+ case AERON_COMMAND_REJECT_IMAGE:
+ {
+ aeron_reject_image_command_t *command = (aeron_reject_image_command_t *)message;
+
+ snprintf(
+ buffer,
+ sizeof(buffer) - 1,
+ "clientId=%" PRId64 " correlationId=%" PRId64 " imageCorrelationId=%" PRId64 "position=%" PRId64 " reason=%.*s",
+ command->correlated.client_id,
+ command->correlated.correlation_id,
+ command->image_correlation_id,
+ command->position,
+ command->reason_length,
+ command->reason_text);
+ }
+
default:
break;
}
diff --git a/aeron-driver/src/main/c/agent/aeron_driver_agent.h b/aeron-driver/src/main/c/agent/aeron_driver_agent.h
index 89c050e40d..bc62261876 100644
--- a/aeron-driver/src/main/c/agent/aeron_driver_agent.h
+++ b/aeron-driver/src/main/c/agent/aeron_driver_agent.h
@@ -79,10 +79,11 @@ typedef enum aeron_driver_agent_event_enum
AERON_DRIVER_EVENT_SEND_NAK_MESSAGE = 54,
AERON_DRIVER_EVENT_RESEND = 55,
AERON_DRIVER_EVENT_CMD_IN_REMOVE_DESTINATION_BY_ID = 56,
+ AERON_DRIVER_EVENT_CMD_IN_REJECT_IMAGE = 57,
// C-specific events. Note: event IDs are dynamic to avoid gaps in the sparse arrays.
- AERON_DRIVER_EVENT_ADD_DYNAMIC_DISSECTOR = 57,
- AERON_DRIVER_EVENT_DYNAMIC_DISSECTOR_EVENT = 58,
+ AERON_DRIVER_EVENT_ADD_DYNAMIC_DISSECTOR = 58,
+ AERON_DRIVER_EVENT_DYNAMIC_DISSECTOR_EVENT = 59,
}
aeron_driver_agent_event_t;
diff --git a/aeron-driver/src/main/c/media/aeron_receive_channel_endpoint.c b/aeron-driver/src/main/c/media/aeron_receive_channel_endpoint.c
index f7c530b6b6..1a4e455ab4 100644
--- a/aeron-driver/src/main/c/media/aeron_receive_channel_endpoint.c
+++ b/aeron-driver/src/main/c/media/aeron_receive_channel_endpoint.c
@@ -122,6 +122,8 @@ int aeron_receive_channel_endpoint_create(
_endpoint->short_sends_counter = aeron_system_counter_addr(system_counters, AERON_SYSTEM_COUNTER_SHORT_SENDS);
_endpoint->possible_ttl_asymmetry_counter = aeron_system_counter_addr(
system_counters, AERON_SYSTEM_COUNTER_POSSIBLE_TTL_ASYMMETRY);
+ _endpoint->errors_frames_sent_counter = aeron_system_counter_addr(
+ system_counters, AERON_SYSTEM_COUNTER_ERROR_FRAMES_SENT);
_endpoint->cached_clock = context->receiver_cached_clock;
@@ -424,6 +426,51 @@ int aeron_receive_channel_endpoint_send_response_setup(
return bytes_sent;
}
+int aeron_receiver_channel_endpoint_send_error_frame(
+ aeron_receive_channel_endpoint_t *channel_endpoint,
+ aeron_receive_destination_t *destination,
+ struct sockaddr_storage *control_addr,
+ int32_t session_id,
+ int32_t stream_id,
+ int32_t error_code,
+ const char *invalidation_reason)
+{
+ uint8_t buffer[AERON_ERROR_MAX_FRAME_LENGTH];
+ aeron_error_t *error = (aeron_error_t *)buffer;
+ struct iovec iov;
+
+ const size_t error_message_length = strnlen(invalidation_reason, AERON_ERROR_MAX_TEXT_LENGTH);
+ const size_t frame_length = sizeof(aeron_error_t) + error_message_length;
+ error->frame_header.frame_length = (int32_t)frame_length;
+ error->frame_header.version = AERON_FRAME_HEADER_VERSION;
+ error->frame_header.flags = channel_endpoint->group_tag.is_present ? AERON_ERROR_HAS_GROUP_TAG_FLAG : UINT8_C(0);
+ error->frame_header.type = AERON_HDR_TYPE_ERR;
+ error->session_id = session_id;
+ error->stream_id = stream_id;
+ error->receiver_id = channel_endpoint->receiver_id;
+ error->group_tag = channel_endpoint->group_tag.value;
+ error->error_code = error_code;
+ error->error_length = (int32_t)error_message_length;
+ memcpy(&buffer[sizeof(aeron_error_t)], invalidation_reason, error_message_length);
+
+ iov.iov_base = buffer;
+ iov.iov_len = (unsigned long)frame_length;
+ int bytes_sent = aeron_receive_channel_endpoint_send(channel_endpoint, destination, control_addr, &iov);
+ if (bytes_sent != (int)iov.iov_len)
+ {
+ if (bytes_sent >= 0)
+ {
+ aeron_counter_increment(channel_endpoint->short_sends_counter, 1);
+ }
+ }
+ else
+ {
+ aeron_counter_increment(channel_endpoint->errors_frames_sent_counter, 1);
+ }
+
+ return bytes_sent;
+}
+
void aeron_receive_channel_endpoint_dispatch(
aeron_udp_channel_data_paths_t *data_paths,
aeron_udp_channel_transport_t *transport,
diff --git a/aeron-driver/src/main/c/media/aeron_receive_channel_endpoint.h b/aeron-driver/src/main/c/media/aeron_receive_channel_endpoint.h
index fc306a883e..119994d4c4 100644
--- a/aeron-driver/src/main/c/media/aeron_receive_channel_endpoint.h
+++ b/aeron-driver/src/main/c/media/aeron_receive_channel_endpoint.h
@@ -81,6 +81,7 @@ typedef struct aeron_receive_channel_endpoint_stct
int64_t *short_sends_counter;
int64_t *possible_ttl_asymmetry_counter;
+ int64_t *errors_frames_sent_counter;
}
aeron_receive_channel_endpoint_t;
@@ -147,6 +148,15 @@ int aeron_receive_channel_endpoint_send_response_setup(
int32_t session_id,
int32_t response_session_id);
+int aeron_receiver_channel_endpoint_send_error_frame(
+ aeron_receive_channel_endpoint_t *channel_endpoint,
+ aeron_receive_destination_t *destination,
+ struct sockaddr_storage *control_addr,
+ int32_t session_id,
+ int32_t stream_id,
+ int32_t error_code,
+ const char *invalidation_reason);
+
void aeron_receive_channel_endpoint_dispatch(
aeron_udp_channel_data_paths_t *data_paths,
aeron_udp_channel_transport_t *transport,
diff --git a/aeron-driver/src/main/c/media/aeron_send_channel_endpoint.c b/aeron-driver/src/main/c/media/aeron_send_channel_endpoint.c
index c18acf24f7..0a43071d34 100644
--- a/aeron-driver/src/main/c/media/aeron_send_channel_endpoint.c
+++ b/aeron-driver/src/main/c/media/aeron_send_channel_endpoint.c
@@ -414,6 +414,18 @@ void aeron_send_channel_endpoint_dispatch(
}
break;
+ case AERON_HDR_TYPE_ERR:
+ if (length >= sizeof(aeron_error_t) && length >= (size_t)frame_header->frame_length)
+ {
+ result = aeron_send_channel_endpoint_on_error(endpoint, conductor_proxy, buffer, length, addr);
+ aeron_counter_ordered_increment(sender->error_messages_received_counter, 1);
+ }
+ else
+ {
+ aeron_counter_increment(sender->invalid_frames_counter, 1);
+ }
+ break;
+
case AERON_HDR_TYPE_RSP_SETUP:
if (length >= sizeof(aeron_response_setup_header_t))
{
@@ -512,6 +524,36 @@ int aeron_send_channel_endpoint_on_status_message(
return result;
}
+int aeron_send_channel_endpoint_on_error(
+ aeron_send_channel_endpoint_t *endpoint,
+ aeron_driver_conductor_proxy_t *conductor_proxy,
+ uint8_t *buffer,
+ size_t length,
+ struct sockaddr_storage *addr)
+{
+ aeron_error_t *error = (aeron_error_t *)buffer;
+
+ int64_t destination_registration_id = AERON_NULL_VALUE;
+ if (NULL != endpoint->destination_tracker)
+ {
+ destination_registration_id = aeron_udp_destination_tracker_find_registration_id(
+ endpoint->destination_tracker, buffer, length, addr);
+ }
+
+ int64_t key_value = aeron_map_compound_key(error->stream_id, error->session_id);
+ aeron_network_publication_t *publication = aeron_int64_to_ptr_hash_map_get(
+ &endpoint->publication_dispatch_map, key_value);
+ int result = 0;
+
+ if (NULL != publication)
+ {
+ aeron_network_publication_on_error(
+ publication, destination_registration_id, buffer, length, addr, conductor_proxy);
+ }
+
+ return result;
+}
+
void aeron_send_channel_endpoint_on_rttm(
aeron_send_channel_endpoint_t *endpoint, uint8_t *buffer, size_t length, struct sockaddr_storage *addr)
{
diff --git a/aeron-driver/src/main/c/media/aeron_send_channel_endpoint.h b/aeron-driver/src/main/c/media/aeron_send_channel_endpoint.h
index f5272e6a7f..b83ea52d46 100644
--- a/aeron-driver/src/main/c/media/aeron_send_channel_endpoint.h
+++ b/aeron-driver/src/main/c/media/aeron_send_channel_endpoint.h
@@ -123,6 +123,13 @@ int aeron_send_channel_endpoint_on_status_message(
size_t length,
struct sockaddr_storage *addr);
+int aeron_send_channel_endpoint_on_error(
+ aeron_send_channel_endpoint_t *endpoint,
+ aeron_driver_conductor_proxy_t *conductor_proxy,
+ uint8_t *buffer,
+ size_t length,
+ struct sockaddr_storage *addr);
+
void aeron_send_channel_endpoint_on_rttm(
aeron_send_channel_endpoint_t *endpoint, uint8_t *buffer, size_t length, struct sockaddr_storage *addr);
diff --git a/aeron-driver/src/main/c/media/aeron_udp_destination_tracker.c b/aeron-driver/src/main/c/media/aeron_udp_destination_tracker.c
index 3cec4967dd..0af24f52b3 100644
--- a/aeron-driver/src/main/c/media/aeron_udp_destination_tracker.c
+++ b/aeron-driver/src/main/c/media/aeron_udp_destination_tracker.c
@@ -70,7 +70,7 @@ int aeron_udp_destination_tracker_close(aeron_udp_destination_tracker_t *tracker
return 0;
}
-void aeron_udp_destination_tracker_remove_inactive_destinations(
+static void aeron_udp_destination_tracker_remove_inactive_destinations(
aeron_udp_destination_tracker_t *tracker,
int64_t now_ns)
{
@@ -169,7 +169,7 @@ int aeron_udp_destination_tracker_send(
return min_bytes_sent;
}
-bool aeron_udp_destination_tracker_same_port(struct sockaddr_storage *lhs, struct sockaddr_storage *rhs)
+static bool aeron_udp_destination_tracker_same_port(struct sockaddr_storage *lhs, struct sockaddr_storage *rhs)
{
bool result = false;
@@ -191,7 +191,7 @@ bool aeron_udp_destination_tracker_same_port(struct sockaddr_storage *lhs, struc
return result;
}
-bool aeron_udp_destination_tracker_same_addr(struct sockaddr_storage *lhs, struct sockaddr_storage *rhs)
+static bool aeron_udp_destination_tracker_same_addr(struct sockaddr_storage *lhs, struct sockaddr_storage *rhs)
{
bool result = false;
@@ -213,6 +213,18 @@ bool aeron_udp_destination_tracker_same_addr(struct sockaddr_storage *lhs, struc
return result;
}
+static bool aeron_udp_destination_tracker_is_match(
+ aeron_udp_destination_entry_t *entry,
+ int64_t receiver_id,
+ struct sockaddr_storage *addr)
+{
+ return
+ (entry->is_receiver_id_valid && receiver_id == entry->receiver_id &&
+ aeron_udp_destination_tracker_same_port(&entry->addr, addr)) ||
+ (!entry->is_receiver_id_valid && aeron_udp_destination_tracker_same_addr(&entry->addr, addr) &&
+ aeron_udp_destination_tracker_same_port(&entry->addr, addr));
+}
+
int aeron_udp_destination_tracker_add_destination(
aeron_udp_destination_tracker_t *tracker,
int64_t receiver_id,
@@ -258,21 +270,16 @@ int aeron_udp_destination_tracker_on_status_message(
{
aeron_udp_destination_entry_t *entry = &tracker->destinations.array[i];
- if (entry->is_receiver_id_valid && receiver_id == entry->receiver_id &&
- aeron_udp_destination_tracker_same_port(&entry->addr, addr))
- {
- entry->time_of_last_activity_ns = now_ns;
- is_existing = true;
- break;
- }
- else if (!entry->is_receiver_id_valid &&
- aeron_udp_destination_tracker_same_addr(&entry->addr, addr) &&
- aeron_udp_destination_tracker_same_port(&entry->addr, addr))
+ is_existing = aeron_udp_destination_tracker_is_match(entry, receiver_id, addr);
+ if (is_existing)
{
+ if (!entry->is_receiver_id_valid)
+ {
+ entry->receiver_id = receiver_id;
+ entry->is_receiver_id_valid = true;
+ }
entry->time_of_last_activity_ns = now_ns;
- entry->receiver_id = receiver_id;
- entry->is_receiver_id_valid = true;
- is_existing = true;
+
break;
}
}
@@ -344,7 +351,9 @@ int aeron_udp_destination_tracker_remove_destination(
}
int aeron_udp_destination_tracker_remove_destination_by_id(
- aeron_udp_destination_tracker_t *tracker, int64_t destination_registration_id, aeron_uri_t **removed_uri)
+ aeron_udp_destination_tracker_t *tracker,
+ int64_t destination_registration_id,
+ aeron_uri_t **removed_uri)
{
for (int last_index = (int)tracker->destinations.length - 1, i = last_index; i >= 0; i--)
{
@@ -370,6 +379,27 @@ int aeron_udp_destination_tracker_remove_destination_by_id(
return 0;
}
+int64_t aeron_udp_destination_tracker_find_registration_id(
+ aeron_udp_destination_tracker_t *tracker,
+ const uint8_t *buffer,
+ size_t len,
+ struct sockaddr_storage *addr)
+{
+ aeron_error_t *error = (aeron_error_t *)buffer;
+ const int64_t receiver_id = error->receiver_id;
+
+ for (size_t i = 0, size = tracker->destinations.length; i < size; i++)
+ {
+ aeron_udp_destination_entry_t *entry = &tracker->destinations.array[i];
+ if (aeron_udp_destination_tracker_is_match(entry, receiver_id, addr))
+ {
+ return entry->registration_id;
+ }
+ }
+
+ return AERON_NULL_VALUE;
+}
+
void aeron_udp_destination_tracker_check_for_re_resolution(
aeron_udp_destination_tracker_t *tracker,
aeron_send_channel_endpoint_t *endpoint,
diff --git a/aeron-driver/src/main/c/media/aeron_udp_destination_tracker.h b/aeron-driver/src/main/c/media/aeron_udp_destination_tracker.h
index ad98c3ac3a..77a0cda1d7 100644
--- a/aeron-driver/src/main/c/media/aeron_udp_destination_tracker.h
+++ b/aeron-driver/src/main/c/media/aeron_udp_destination_tracker.h
@@ -92,6 +92,9 @@ int aeron_udp_destination_tracker_remove_destination(
int aeron_udp_destination_tracker_remove_destination_by_id(
aeron_udp_destination_tracker_t *tracker, int64_t destination_registration_id, aeron_uri_t **removed_uri);
+int64_t aeron_udp_destination_tracker_find_registration_id(
+ aeron_udp_destination_tracker_t *tracker, const uint8_t *buffer, size_t len, struct sockaddr_storage *addr);
+
void aeron_udp_destination_tracker_check_for_re_resolution(
aeron_udp_destination_tracker_t *tracker,
aeron_send_channel_endpoint_t *endpoint,
diff --git a/aeron-driver/src/main/java/io/aeron/driver/AbstractMinMulticastFlowControl.java b/aeron-driver/src/main/java/io/aeron/driver/AbstractMinMulticastFlowControl.java
index bec0263223..076e632c71 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/AbstractMinMulticastFlowControl.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/AbstractMinMulticastFlowControl.java
@@ -18,6 +18,7 @@
import io.aeron.CommonContext;
import io.aeron.driver.media.UdpChannel;
import io.aeron.driver.status.FlowControlReceivers;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.SetupFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import org.agrona.CloseHelper;
@@ -312,6 +313,31 @@ protected void processSendSetupTrigger(
}
}
+ /**
+ * Process an error frame from a downstream receiver.
+ *
+ * @param error flyweight over the error frame.
+ * @param receiverAddress of the receiver.
+ * @param timeNs current time in nanoseconds.
+ * @param hasMatchingTag if the error message comes from a receiver with a tag matching the group.
+ */
+ protected void processError(
+ final ErrorFlyweight error,
+ final InetSocketAddress receiverAddress,
+ final long timeNs,
+ final boolean hasMatchingTag)
+ {
+ final long receiverId = error.receiverId();
+
+ for (final Receiver receiver : receivers)
+ {
+ if (hasMatchingTag && receiverId == receiver.receiverId)
+ {
+ receiver.eosFlagged = true;
+ }
+ }
+ }
+
/**
* Timeout after which an inactive receiver will be dropped.
*
diff --git a/aeron-driver/src/main/java/io/aeron/driver/ClientCommandAdapter.java b/aeron-driver/src/main/java/io/aeron/driver/ClientCommandAdapter.java
index 56915446c9..46d14a0e87 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/ClientCommandAdapter.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/ClientCommandAdapter.java
@@ -44,6 +44,7 @@ final class ClientCommandAdapter implements ControlledMessageHandler
private final CounterMessageFlyweight counterMsgFlyweight = new CounterMessageFlyweight();
private final StaticCounterMessageFlyweight staticCounterMessageFlyweight = new StaticCounterMessageFlyweight();
private final TerminateDriverFlyweight terminateDriverFlyweight = new TerminateDriverFlyweight();
+ private final RejectImageFlyweight rejectImageFlyweight = new RejectImageFlyweight();
private final DestinationByIdMessageFlyweight destinationByIdMessageFlyweight =
new DestinationByIdMessageFlyweight();
private final DriverConductor conductor;
@@ -287,6 +288,20 @@ else if (channel.startsWith(SPY_QUALIFIER))
break;
}
+ case REJECT_IMAGE:
+ {
+ rejectImageFlyweight.wrap(buffer, index);
+ rejectImageFlyweight.validateLength(msgTypeId, length);
+ correlationId = rejectImageFlyweight.correlationId();
+
+ conductor.onRejectImage(
+ rejectImageFlyweight.correlationId(),
+ rejectImageFlyweight.imageCorrelationId(),
+ rejectImageFlyweight.position(),
+ rejectImageFlyweight.reason());
+ break;
+ }
+
case REMOVE_DESTINATION_BY_ID:
{
destinationByIdMessageFlyweight.wrap(buffer, index);
diff --git a/aeron-driver/src/main/java/io/aeron/driver/ClientProxy.java b/aeron-driver/src/main/java/io/aeron/driver/ClientProxy.java
index 0e49d06dc0..4530a0fab9 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/ClientProxy.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/ClientProxy.java
@@ -15,6 +15,7 @@
*/
package io.aeron.driver;
+import io.aeron.Aeron;
import io.aeron.ErrorCode;
import io.aeron.command.*;
import org.agrona.DirectBuffer;
@@ -22,6 +23,8 @@
import org.agrona.MutableDirectBuffer;
import org.agrona.concurrent.broadcast.BroadcastTransmitter;
+import java.net.InetSocketAddress;
+
import static io.aeron.command.ControlProtocolEvents.*;
/**
@@ -33,6 +36,7 @@ final class ClientProxy
private final BroadcastTransmitter transmitter;
private final ErrorResponseFlyweight errorResponse = new ErrorResponseFlyweight();
+ private final PublicationErrorFrameFlyweight publicationErrorFrame = new PublicationErrorFrameFlyweight();
private final PublicationBuffersReadyFlyweight publicationReady = new PublicationBuffersReadyFlyweight();
private final SubscriptionReadyFlyweight subscriptionReady = new SubscriptionReadyFlyweight();
private final ImageBuffersReadyFlyweight imageReady = new ImageBuffersReadyFlyweight();
@@ -47,6 +51,7 @@ final class ClientProxy
this.transmitter = transmitter;
errorResponse.wrap(buffer, 0);
+ publicationErrorFrame.wrap(buffer, 0);
imageReady.wrap(buffer, 0);
publicationReady.wrap(buffer, 0);
subscriptionReady.wrap(buffer, 0);
@@ -67,6 +72,30 @@ void onError(final long correlationId, final ErrorCode errorCode, final String e
transmit(ON_ERROR, buffer, 0, errorResponse.length());
}
+ void onPublicationErrorFrame(
+ final long registrationId,
+ final long destinationRegistrationId, final int sessionId,
+ final int streamId,
+ final long receiverId,
+ final Long groupTag,
+ final InetSocketAddress srcAddress,
+ final int errorCode,
+ final String errorMessage)
+ {
+ publicationErrorFrame
+ .registrationId(registrationId)
+ .destinationRegistrationId(destinationRegistrationId)
+ .sessionId(sessionId)
+ .streamId(streamId)
+ .receiverId(receiverId)
+ .groupTag(null == groupTag ? Aeron.NULL_VALUE : groupTag)
+ .sourceAddress(srcAddress)
+ .errorCode(ErrorCode.get(errorCode))
+ .errorMessage(errorMessage);
+
+ transmit(ON_PUBLICATION_ERROR, buffer, 0, publicationErrorFrame.length());
+ }
+
void onAvailableImage(
final long correlationId,
final int streamId,
diff --git a/aeron-driver/src/main/java/io/aeron/driver/DriverConductor.java b/aeron-driver/src/main/java/io/aeron/driver/DriverConductor.java
index 263e2d046d..60f62ecb94 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/DriverConductor.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/DriverConductor.java
@@ -29,10 +29,12 @@
import io.aeron.driver.media.UdpChannel;
import io.aeron.driver.status.*;
import io.aeron.exceptions.AeronEvent;
+import io.aeron.exceptions.AeronException;
import io.aeron.exceptions.ControlProtocolException;
import io.aeron.logbuffer.LogBufferDescriptor;
import io.aeron.protocol.DataHeaderFlyweight;
import io.aeron.protocol.SetupFlyweight;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.status.ChannelEndpointStatus;
import org.agrona.BitUtil;
import org.agrona.CloseHelper;
@@ -56,6 +58,7 @@
import org.agrona.concurrent.status.UnsafeBufferPosition;
import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Objects;
@@ -384,6 +387,30 @@ void onChannelEndpointError(final long statusIndicatorId, final Exception ex)
clientProxy.onError(statusIndicatorId, CHANNEL_ENDPOINT_ERROR, errorMessage);
}
+ void onPublicationError(
+ final long registrationId,
+ final long destinationRegistrationId,
+ final int sessionId,
+ final int streamId,
+ final long receiverId,
+ final Long groupId,
+ final InetSocketAddress srcAddress,
+ final int errorCode,
+ final String errorMessage)
+ {
+ recordError(new AeronException(errorMessage, AeronException.Category.WARN));
+ clientProxy.onPublicationErrorFrame(
+ registrationId,
+ destinationRegistrationId,
+ sessionId,
+ streamId,
+ receiverId,
+ groupId,
+ srcAddress,
+ errorCode,
+ errorMessage);
+ }
+
void onReResolveEndpoint(
final String endpoint, final SendChannelEndpoint channelEndpoint, final InetSocketAddress address)
{
@@ -396,8 +423,7 @@ void onReResolveEndpoint(
final InetSocketAddress newAddress = asyncResult.get();
if (newAddress.isUnresolved())
{
- ctx.errorHandler().onError(new AeronEvent("could not re-resolve: endpoint=" + endpoint));
- errorCounter.increment();
+ recordError(new AeronEvent("could not re-resolve: endpoint=" + endpoint));
}
else if (!address.equals(newAddress))
{
@@ -406,8 +432,7 @@ else if (!address.equals(newAddress))
}
catch (final Exception ex)
{
- ctx.errorHandler().onError(ex);
- errorCounter.increment();
+ recordError(ex);
}
});
}
@@ -427,8 +452,7 @@ void onReResolveControl(
final InetSocketAddress newAddress = asyncResult.get();
if (newAddress.isUnresolved())
{
- ctx.errorHandler().onError(new AeronEvent("could not re-resolve: control=" + control));
- errorCounter.increment();
+ recordError(new AeronEvent("could not re-resolve: control=" + control));
}
else if (!address.equals(newAddress))
{
@@ -437,8 +461,7 @@ else if (!address.equals(newAddress))
}
catch (final Exception ex)
{
- ctx.errorHandler().onError(ex);
- errorCounter.increment();
+ recordError(ex);
}
});
}
@@ -610,6 +633,19 @@ private PublicationImage findResponsePublicationImage(final PublicationParams pa
throw new IllegalArgumentException("image.correlationId=" + params.responseCorrelationId + " not found");
}
+ private PublicationImage findPublicationImage(final long correlationId)
+ {
+ for (final PublicationImage publicationImage : publicationImages)
+ {
+ if (correlationId == publicationImage.correlationId())
+ {
+ return publicationImage;
+ }
+ }
+
+ return null;
+ }
+
void responseSetup(final long responseCorrelationId, final int responseSessionId)
{
for (int i = 0, subscriptionLinksSize = subscriptionLinks.size(); i < subscriptionLinksSize; i++)
@@ -1458,6 +1494,29 @@ void onTerminateDriver(final DirectBuffer tokenBuffer, final int tokenOffset, fi
}
}
+ void onRejectImage(
+ final long correlationId,
+ final long imageCorrelationId,
+ final long position,
+ final String reason)
+ {
+ if (ErrorFlyweight.MAX_ERROR_MESSAGE_LENGTH < reason.getBytes(StandardCharsets.UTF_8).length)
+ {
+ throw new ControlProtocolException(GENERIC_ERROR, "Invalidation reason must be 1023 bytes or less");
+ }
+
+ final PublicationImage publicationImage = findPublicationImage(imageCorrelationId);
+
+ if (null == publicationImage)
+ {
+ throw new ControlProtocolException(
+ GENERIC_ERROR, "Unable to resolve image for correlationId=" + imageCorrelationId);
+ }
+
+ receiverProxy.rejectImage(imageCorrelationId, position, reason);
+ clientProxy.operationSucceeded(correlationId);
+ }
+
private void heartbeatAndCheckTimers(final long nowNs)
{
final long nowMs = cachedEpochClock.time();
@@ -2651,4 +2710,10 @@ static AsyncResult of(final Supplier supplier)
}
}
}
+
+ private void recordError(final Exception ex)
+ {
+ ctx.errorHandler().onError(ex);
+ errorCounter.increment();
+ }
}
diff --git a/aeron-driver/src/main/java/io/aeron/driver/DriverConductorProxy.java b/aeron-driver/src/main/java/io/aeron/driver/DriverConductorProxy.java
index 0bde8f7076..c0a2185331 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/DriverConductorProxy.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/DriverConductorProxy.java
@@ -247,6 +247,45 @@ void createPublicationImage(
}
}
+ void onPublicationError(
+ final long registrationId,
+ final long destinationRegistrationId,
+ final int sessionId,
+ final int streamId,
+ final long receiverId,
+ final Long groupId,
+ final InetSocketAddress srcAddress,
+ final int errorCode,
+ final String errorMessage)
+ {
+ if (notConcurrent())
+ {
+ driverConductor.onPublicationError(
+ registrationId,
+ destinationRegistrationId,
+ sessionId,
+ streamId,
+ receiverId,
+ groupId,
+ srcAddress,
+ errorCode,
+ errorMessage);
+ }
+ else
+ {
+ offer(() -> driverConductor.onPublicationError(
+ registrationId,
+ destinationRegistrationId,
+ sessionId,
+ streamId,
+ receiverId,
+ groupId,
+ srcAddress,
+ errorCode,
+ errorMessage));
+ }
+ }
+
private void offer(final Runnable cmd)
{
if (!commandQueue.offer(cmd))
diff --git a/aeron-driver/src/main/java/io/aeron/driver/FlowControl.java b/aeron-driver/src/main/java/io/aeron/driver/FlowControl.java
index b581d17380..013c8d2d1f 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/FlowControl.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/FlowControl.java
@@ -16,6 +16,7 @@
package io.aeron.driver;
import io.aeron.driver.media.UdpChannel;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.SetupFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import org.agrona.concurrent.status.CountersManager;
@@ -100,6 +101,15 @@ long onSetup(
int positionBitsToShift,
long timeNs);
+ /**
+ * Update the sender flow control strategy if an error comes from one of the receivers.
+ *
+ * @param errorFlyweight over the error received.
+ * @param receiverAddress the address of the receiver.
+ * @param timeNs current time in nanoseconds
+ */
+ void onError(ErrorFlyweight errorFlyweight, InetSocketAddress receiverAddress, long timeNs);
+
/**
* Initialize the flow control strategy for a stream.
*
diff --git a/aeron-driver/src/main/java/io/aeron/driver/MaxMulticastFlowControl.java b/aeron-driver/src/main/java/io/aeron/driver/MaxMulticastFlowControl.java
index 7dd650cf93..7025f99ac6 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/MaxMulticastFlowControl.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/MaxMulticastFlowControl.java
@@ -16,6 +16,7 @@
package io.aeron.driver;
import io.aeron.driver.media.UdpChannel;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.SetupFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import org.agrona.concurrent.status.CountersManager;
@@ -120,6 +121,12 @@ public long onIdle(final long timeNs, final long senderLimit, final long senderP
return senderLimit;
}
+ /**
+ * {@inheritDoc}
+ */
+ public void onError(final ErrorFlyweight errorFlyweight, final InetSocketAddress receiverAddress, final long timeNs)
+ {
+ }
/**
* {@inheritDoc}
diff --git a/aeron-driver/src/main/java/io/aeron/driver/MinMulticastFlowControl.java b/aeron-driver/src/main/java/io/aeron/driver/MinMulticastFlowControl.java
index 7e94afa529..0c816c307d 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/MinMulticastFlowControl.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/MinMulticastFlowControl.java
@@ -15,6 +15,7 @@
*/
package io.aeron.driver;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import java.net.InetSocketAddress;
@@ -59,4 +60,12 @@ public void onTriggerSendSetup(
{
processSendSetupTrigger(flyweight, receiverAddress, timeNs, true);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onError(final ErrorFlyweight errorFlyweight, final InetSocketAddress receiverAddress, final long timeNs)
+ {
+ processError(errorFlyweight, receiverAddress, timeNs, true);
+ }
}
diff --git a/aeron-driver/src/main/java/io/aeron/driver/NetworkPublication.java b/aeron-driver/src/main/java/io/aeron/driver/NetworkPublication.java
index 71328b5112..78ba9aa3b0 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/NetworkPublication.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/NetworkPublication.java
@@ -24,6 +24,7 @@
import io.aeron.logbuffer.LogBufferDescriptor;
import io.aeron.logbuffer.LogBufferUnblocker;
import io.aeron.protocol.DataHeaderFlyweight;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.RttMeasurementFlyweight;
import io.aeron.protocol.SetupFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
@@ -480,6 +481,36 @@ public void onStatusMessage(
}
}
+ /**
+ * Process an error message from a receiver.
+ *
+ * @param msg flyweight over the network packet.
+ * @param srcAddress that the setup message has come from.
+ * @param destinationRegistrationId registrationId of the relevant MDC destination or {@link Aeron#NULL_VALUE}
+ * @param conductorProxy to send messages back to the conductor.
+ */
+ public void onError(
+ final ErrorFlyweight msg,
+ final InetSocketAddress srcAddress,
+ final long destinationRegistrationId,
+ final DriverConductorProxy conductorProxy)
+ {
+ flowControl.onError(msg, srcAddress, cachedNanoClock.nanoTime());
+ if (livenessTracker.onRemoteClose(msg.receiverId()))
+ {
+ conductorProxy.onPublicationError(
+ registrationId,
+ destinationRegistrationId,
+ msg.sessionId(),
+ msg.streamId(),
+ msg.receiverId(),
+ msg.groupTag(),
+ srcAddress,
+ msg.errorCode(),
+ msg.errorMessage());
+ }
+ }
+
/**
* Process RTT (Round Trip Timing) message from a receiver.
*
diff --git a/aeron-driver/src/main/java/io/aeron/driver/PublicationImage.java b/aeron-driver/src/main/java/io/aeron/driver/PublicationImage.java
index f33dbd0660..7b674ab31b 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/PublicationImage.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/PublicationImage.java
@@ -47,6 +47,7 @@
import static io.aeron.CommonContext.UNTETHERED_RESTING_TIMEOUT_PARAM_NAME;
import static io.aeron.CommonContext.UNTETHERED_WINDOW_LIMIT_TIMEOUT_PARAM_NAME;
+import static io.aeron.ErrorCode.GENERIC_ERROR;
import static io.aeron.driver.LossDetector.lossFound;
import static io.aeron.driver.LossDetector.rebuildOffset;
import static io.aeron.driver.status.SystemCounterDescriptor.*;
@@ -87,6 +88,7 @@ class PublicationImageReceiverFields extends PublicationImagePadding2
boolean isSendingEosSm = false;
long timeOfLastPacketNs;
ImageConnection[] imageConnections = new ImageConnection[1];
+ String rejectionReason = null;
}
class PublicationImagePadding3 extends PublicationImageReceiverFields
@@ -449,7 +451,7 @@ RawLog rawLog()
void activate()
{
timeOfLastStateChangeNs = cachedNanoClock.nanoTime();
- state = State.ACTIVE;
+ state(State.ACTIVE);
}
/**
@@ -475,7 +477,7 @@ void deactivate()
timeOfLastSmNs = nowNs - smTimeoutNs - 1;
}
- state = State.DRAINING;
+ state(State.DRAINING);
}
}
@@ -586,6 +588,13 @@ int insertPacket(
final int transportIndex,
final InetSocketAddress srcAddress)
{
+ final boolean isEndOfStream = DataHeaderFlyweight.isEndOfStream(buffer);
+
+ if (null != rejectionReason)
+ {
+ return 0;
+ }
+
final boolean isHeartbeat = DataHeaderFlyweight.isHeartbeat(buffer, length);
final long packetPosition = computePosition(termId, termOffset, positionBitsToShift, initialTermId);
final long proposedPosition = isHeartbeat ? packetPosition : packetPosition + length;
@@ -603,15 +612,15 @@ int insertPacket(
timeOfLastPacketNs = nowNs;
trackConnection(transportIndex, srcAddress, nowNs);
- if (DataHeaderFlyweight.isEndOfStream(buffer))
+ if (isEndOfStream)
{
imageConnections[transportIndex].eosPosition = packetPosition;
imageConnections[transportIndex].isEos = true;
- if (!isEndOfStream && isAllConnectedEos())
+ if (!this.isEndOfStream && isAllConnectedEos())
{
LogBufferDescriptor.endOfStreamPosition(rawLog.metaData(), findEosPosition());
- isEndOfStream = true;
+ this.isEndOfStream = true;
}
}
@@ -677,7 +686,7 @@ void checkEosForDrainTransition(final long nowNs)
isSendingEosSm = true;
timeOfLastSmNs = nowNs - smTimeoutNs - 1;
- state = State.DRAINING;
+ state(State.DRAINING);
}
}
}
@@ -693,8 +702,22 @@ int sendPendingStatusMessage(final long nowNs)
int workCount = 0;
final long changeNumber = endSmChange;
final boolean hasSmTimedOut = (timeOfLastSmNs + smTimeoutNs) - nowNs < 0;
- final Integer responseSessionId;
+ if (null != rejectionReason)
+ {
+ if (hasSmTimedOut)
+ {
+ channelEndpoint.sendErrorFrame(
+ imageConnections, sessionId, streamId, GENERIC_ERROR.value(), rejectionReason);
+
+ timeOfLastSmNs = nowNs;
+ workCount++;
+ }
+
+ return workCount;
+ }
+
+ final Integer responseSessionId;
if (hasSmTimedOut && null != (responseSessionId = this.responseSessionId))
{
channelEndpoint.sendResponseSetup(imageConnections, sessionId, streamId, responseSessionId);
@@ -864,7 +887,7 @@ public void onTimeEvent(final long timeNs, final long timesMs, final DriverCondu
timeOfLastStateChangeNs = timeNs;
isReceiverReleaseTriggered = true;
- state = State.LINGER;
+ state(State.LINGER);
}
break;
@@ -873,7 +896,7 @@ public void onTimeEvent(final long timeNs, final long timesMs, final DriverCondu
{
conductor.cleanupImage(this);
timeOfLastStateChangeNs = timeNs;
- state = State.DONE;
+ state(State.DONE);
}
break;
@@ -891,6 +914,16 @@ public boolean hasReachedEndOfLife()
return hasReceiverReleased && State.DONE == state;
}
+ void reject(final String reason)
+ {
+ rejectionReason = reason;
+ }
+
+ private void state(final State state)
+ {
+ this.state = state;
+ }
+
private boolean isDrained()
{
final long rebuildPosition = this.rebuildPosition.get();
diff --git a/aeron-driver/src/main/java/io/aeron/driver/Receiver.java b/aeron-driver/src/main/java/io/aeron/driver/Receiver.java
index 08075e0d53..d4901b90e8 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/Receiver.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/Receiver.java
@@ -334,6 +334,18 @@ void onResolutionChange(
channelEndpoint.updateControlAddress(transportIndex, newAddress);
}
+ void onRejectImage(final long imageCorrelationId, final long position, final String reason)
+ {
+ for (final PublicationImage image : publicationImages)
+ {
+ if (imageCorrelationId == image.correlationId())
+ {
+ image.reject(reason);
+ break;
+ }
+ }
+ }
+
private void checkPendingSetupMessages(final long nowNs)
{
for (int lastIndex = pendingSetupMessages.size() - 1, i = lastIndex; i >= 0; i--)
diff --git a/aeron-driver/src/main/java/io/aeron/driver/ReceiverLivenessTracker.java b/aeron-driver/src/main/java/io/aeron/driver/ReceiverLivenessTracker.java
index bf0a34316c..be488f1edd 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/ReceiverLivenessTracker.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/ReceiverLivenessTracker.java
@@ -33,9 +33,9 @@ public void onStatusMessage(final long receiverId, final long nowNs)
lastSmTimestampNsByReceiverIdMap.put(receiverId, nowNs);
}
- public void onRemoteClose(final long receiverId)
+ public boolean onRemoteClose(final long receiverId)
{
- lastSmTimestampNsByReceiverIdMap.remove(receiverId);
+ return MISSING_VALUE != lastSmTimestampNsByReceiverIdMap.remove(receiverId);
}
public void onIdle(final long nowNs, final long timeoutNs)
diff --git a/aeron-driver/src/main/java/io/aeron/driver/ReceiverProxy.java b/aeron-driver/src/main/java/io/aeron/driver/ReceiverProxy.java
index 2d8ba4276a..486ada901c 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/ReceiverProxy.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/ReceiverProxy.java
@@ -195,4 +195,16 @@ void requestSetup(
offer(() -> receiver.onRequestSetup(channelEndpoint, streamId, sessionId));
}
}
+
+ void rejectImage(final long imageCorrelationId, final long position, final String reason)
+ {
+ if (notConcurrent())
+ {
+ receiver.onRejectImage(imageCorrelationId, position, reason);
+ }
+ else
+ {
+ offer(() -> receiver.onRejectImage(imageCorrelationId, position, reason));
+ }
+ }
}
diff --git a/aeron-driver/src/main/java/io/aeron/driver/TaggedMulticastFlowControl.java b/aeron-driver/src/main/java/io/aeron/driver/TaggedMulticastFlowControl.java
index 492b73188d..c7052fae51 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/TaggedMulticastFlowControl.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/TaggedMulticastFlowControl.java
@@ -15,6 +15,7 @@
*/
package io.aeron.driver;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import java.net.InetSocketAddress;
@@ -68,6 +69,14 @@ public void onTriggerSendSetup(
processSendSetupTrigger(flyweight, receiverAddress, timeNs, matchesTag(flyweight));
}
+ /**
+ * {@inheritDoc}
+ */
+ public void onError(final ErrorFlyweight errorFlyweight, final InetSocketAddress receiverAddress, final long timeNs)
+ {
+ processError(errorFlyweight, receiverAddress, timeNs, matchesTag(errorFlyweight));
+ }
+
@SuppressWarnings("deprecation")
private boolean matchesTag(final StatusMessageFlyweight flyweight)
{
@@ -97,4 +106,9 @@ else if (asfLength >= SIZE_OF_INT)
return result;
}
+
+ private boolean matchesTag(final ErrorFlyweight errorFlyweight)
+ {
+ return errorFlyweight.hasGroupTag() && errorFlyweight.groupTag() == super.groupTag();
+ }
}
diff --git a/aeron-driver/src/main/java/io/aeron/driver/UnicastFlowControl.java b/aeron-driver/src/main/java/io/aeron/driver/UnicastFlowControl.java
index c9f0fd3aa0..38da930c63 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/UnicastFlowControl.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/UnicastFlowControl.java
@@ -16,6 +16,7 @@
package io.aeron.driver;
import io.aeron.driver.media.UdpChannel;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.SetupFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import org.agrona.concurrent.status.CountersManager;
@@ -85,6 +86,13 @@ public long onSetup(
return senderLimit;
}
+ /**
+ * {@inheritDoc}
+ */
+ public void onError(final ErrorFlyweight errorFlyweight, final InetSocketAddress receiverAddress, final long timeNs)
+ {
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/aeron-driver/src/main/java/io/aeron/driver/media/ControlTransportPoller.java b/aeron-driver/src/main/java/io/aeron/driver/media/ControlTransportPoller.java
index 23f6ca0cd6..e2a85ccd61 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/media/ControlTransportPoller.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/media/ControlTransportPoller.java
@@ -17,6 +17,7 @@
import io.aeron.driver.Configuration;
import io.aeron.driver.DriverConductorProxy;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.NakFlyweight;
import io.aeron.protocol.ResponseSetupFlyweight;
import io.aeron.protocol.RttMeasurementFlyweight;
@@ -52,6 +53,7 @@ public final class ControlTransportPoller extends UdpTransportPoller
private final StatusMessageFlyweight statusMessage = new StatusMessageFlyweight(unsafeBuffer);
private final RttMeasurementFlyweight rttMeasurement = new RttMeasurementFlyweight(unsafeBuffer);
private final ResponseSetupFlyweight responseSetup = new ResponseSetupFlyweight(unsafeBuffer);
+ private final ErrorFlyweight error = new ErrorFlyweight(unsafeBuffer);
private final DriverConductorProxy conductorProxy;
private final Consumer selectorPoller =
(selectionKey) -> poll((SendChannelEndpoint)selectionKey.attachment());
@@ -185,6 +187,11 @@ else if (HDR_TYPE_SM == frameType)
channelEndpoint.onStatusMessage(
statusMessage, unsafeBuffer, bytesReceived, srcAddress, conductorProxy);
}
+ else if (HDR_TYPE_ERR == frameType)
+ {
+ channelEndpoint.onError(
+ error, unsafeBuffer, bytesReceived, srcAddress, conductorProxy);
+ }
else if (HDR_TYPE_RTTM == frameType)
{
channelEndpoint.onRttMeasurement(rttMeasurement, unsafeBuffer, bytesReceived, srcAddress);
diff --git a/aeron-driver/src/main/java/io/aeron/driver/media/ReceiveChannelEndpoint.java b/aeron-driver/src/main/java/io/aeron/driver/media/ReceiveChannelEndpoint.java
index 625e7202b3..cb32ee11fa 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/media/ReceiveChannelEndpoint.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/media/ReceiveChannelEndpoint.java
@@ -20,6 +20,7 @@
import io.aeron.driver.DataPacketDispatcher;
import io.aeron.driver.DriverConductorProxy;
import io.aeron.driver.MediaDriver;
+import io.aeron.driver.status.SystemCounterDescriptor;
import io.aeron.exceptions.AeronException;
import io.aeron.exceptions.ControlProtocolException;
import io.aeron.protocol.*;
@@ -66,6 +67,10 @@ abstract class ReceiveChannelEndpointLhsPadding extends UdpChannelTransport
abstract class ReceiveChannelEndpointHotFields extends ReceiveChannelEndpointLhsPadding
{
+ /**
+ * Counter for the number of errors frames send back by this channel endpoint.
+ */
+ protected final AtomicCounter errorFramesSent;
long timeOfLastActivityNs;
ReceiveChannelEndpointHotFields(
@@ -76,6 +81,7 @@ abstract class ReceiveChannelEndpointHotFields extends ReceiveChannelEndpointLhs
final MediaDriver.Context context)
{
super(udpChannel, endPointAddress, bindAddress, connectAddress, context);
+ errorFramesSent = context.systemCounters().get(SystemCounterDescriptor.ERROR_FRAMES_SENT);
}
}
@@ -114,6 +120,8 @@ public class ReceiveChannelEndpoint extends ReceiveChannelEndpointRhsPadding
private final RttMeasurementFlyweight rttMeasurementFlyweight;
private final ByteBuffer responseSetupBuffer;
private final ResponseSetupFlyweight responseSetupHeader;
+ private final ByteBuffer errorBuffer;
+ private final ErrorFlyweight errorFlyweight;
private final AtomicCounter shortSends;
private final AtomicCounter possibleTtlAsymmetry;
private final AtomicCounter statusIndicator;
@@ -162,6 +170,8 @@ public ReceiveChannelEndpoint(
rttMeasurementFlyweight = threadLocals.rttMeasurementFlyweight();
responseSetupBuffer = threadLocals.responseSetupBuffer();
responseSetupHeader = threadLocals.responseSetupHeader();
+ errorBuffer = threadLocals.errorBuffer();
+ errorFlyweight = threadLocals.errorFlyweight();
cachedNanoClock = context.receiverCachedNanoClock();
timeOfLastActivityNs = cachedNanoClock.nanoTime();
receiverId = threadLocals.nextReceiverId();
@@ -938,6 +948,37 @@ public void sendResponseSetup(
send(responseSetupBuffer, ResponseSetupFlyweight.HEADER_LENGTH, controlAddresses);
}
+ /**
+ * Send an error frame back to the source publications to indicate this image has errored.
+ *
+ * @param controlAddresses of the sources.
+ * @param sessionId for the image.
+ * @param streamId for the image.
+ * @param errorCode for the error being sent.
+ * @param errorMessage to be sent back to the publication.
+ */
+ public void sendErrorFrame(
+ final ImageConnection[] controlAddresses,
+ final int sessionId,
+ final int streamId,
+ final int errorCode,
+ final String errorMessage)
+ {
+ errorFramesSent.increment();
+
+ errorBuffer.clear();
+ errorFlyweight
+ .sessionId(sessionId)
+ .streamId(streamId)
+ .receiverId(receiverId)
+ .groupTag(groupTag)
+ .errorCode(errorCode)
+ .errorMessage(errorMessage);
+ errorBuffer.limit(errorFlyweight.frameLength());
+
+ send(errorBuffer, errorFlyweight.frameLength(), controlAddresses);
+ }
+
/**
* Dispatcher for the channel.
*
diff --git a/aeron-driver/src/main/java/io/aeron/driver/media/ReceiveChannelEndpointThreadLocals.java b/aeron-driver/src/main/java/io/aeron/driver/media/ReceiveChannelEndpointThreadLocals.java
index 67c4a280ea..90b5a4960d 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/media/ReceiveChannelEndpointThreadLocals.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/media/ReceiveChannelEndpointThreadLocals.java
@@ -15,6 +15,7 @@
*/
package io.aeron.driver.media;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.HeaderFlyweight;
import io.aeron.protocol.NakFlyweight;
import io.aeron.protocol.ResponseSetupFlyweight;
@@ -44,6 +45,8 @@ public final class ReceiveChannelEndpointThreadLocals
private final RttMeasurementFlyweight rttMeasurementFlyweight;
private final ByteBuffer responseSetupBuffer;
private final ResponseSetupFlyweight responseSetupHeader;
+ private final ByteBuffer errorBuffer;
+ private final ErrorFlyweight errorFlyweight;
private long nextReceiverId;
/**
@@ -56,7 +59,8 @@ public ReceiveChannelEndpointThreadLocals()
BitUtil.align(smLength, CACHE_LINE_LENGTH) +
BitUtil.align(NakFlyweight.HEADER_LENGTH, CACHE_LINE_LENGTH) +
BitUtil.align(RttMeasurementFlyweight.HEADER_LENGTH, CACHE_LINE_LENGTH) +
- BitUtil.align(ResponseSetupFlyweight.HEADER_LENGTH, CACHE_LINE_LENGTH);
+ BitUtil.align(ResponseSetupFlyweight.HEADER_LENGTH, CACHE_LINE_LENGTH) +
+ BitUtil.align(ErrorFlyweight.MAX_ERROR_FRAME_LENGTH, CACHE_LINE_LENGTH);
final UUID uuid = UUID.randomUUID();
nextReceiverId = uuid.getMostSignificantBits() ^ uuid.getLeastSignificantBits();
@@ -83,6 +87,12 @@ public ReceiveChannelEndpointThreadLocals()
responseSetupBuffer = byteBuffer.slice();
responseSetupHeader = new ResponseSetupFlyweight(responseSetupBuffer);
+ final int errorOffset = responseSetupOffset + BitUtil.align(
+ ResponseSetupFlyweight.HEADER_LENGTH, FRAME_ALIGNMENT);
+ byteBuffer.limit(errorOffset + ErrorFlyweight.MAX_ERROR_FRAME_LENGTH).position(errorOffset);
+ errorBuffer = byteBuffer.slice();
+ errorFlyweight = new ErrorFlyweight(errorBuffer);
+
statusMessageFlyweight
.version(HeaderFlyweight.CURRENT_VERSION)
.headerType(HeaderFlyweight.HDR_TYPE_SM)
@@ -102,6 +112,11 @@ public ReceiveChannelEndpointThreadLocals()
.version(HeaderFlyweight.CURRENT_VERSION)
.headerType(HeaderFlyweight.HDR_TYPE_RSP_SETUP)
.frameLength(ResponseSetupFlyweight.HEADER_LENGTH);
+
+ errorFlyweight
+ .version(HeaderFlyweight.CURRENT_VERSION)
+ .headerType(HeaderFlyweight.HDR_TYPE_ERR)
+ .frameLength(ResponseSetupFlyweight.HEADER_LENGTH);
}
/**
@@ -184,6 +199,26 @@ public ResponseSetupFlyweight responseSetupHeader()
return responseSetupHeader;
}
+ /**
+ * Buffer for writing the Error messages to send.
+ *
+ * @return buffer for writing the error messages to send.
+ */
+ public ByteBuffer errorBuffer()
+ {
+ return errorBuffer;
+ }
+
+ /**
+ * Flyweight over the {@link #errorBuffer()}
+ *
+ * @return flyweight over the {@link #errorBuffer()}
+ */
+ public ErrorFlyweight errorFlyweight()
+ {
+ return errorFlyweight;
+ }
+
/**
* Get the next receiver id to be used for a receiver channel identity.
*
diff --git a/aeron-driver/src/main/java/io/aeron/driver/media/SendChannelEndpoint.java b/aeron-driver/src/main/java/io/aeron/driver/media/SendChannelEndpoint.java
index 3686695f5b..0e8f0c2565 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/media/SendChannelEndpoint.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/media/SendChannelEndpoint.java
@@ -26,6 +26,7 @@
import io.aeron.driver.status.MdcDestinations;
import io.aeron.exceptions.ControlProtocolException;
import io.aeron.protocol.DataHeaderFlyweight;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.NakFlyweight;
import io.aeron.protocol.ResponseSetupFlyweight;
import io.aeron.protocol.RttMeasurementFlyweight;
@@ -52,6 +53,7 @@
import static io.aeron.driver.media.SendChannelEndpoint.DESTINATION_TIMEOUT;
import static io.aeron.driver.media.UdpChannelTransport.sendError;
+import static io.aeron.driver.status.SystemCounterDescriptor.ERROR_FRAMES_RECEIVED;
import static io.aeron.driver.status.SystemCounterDescriptor.NAK_MESSAGES_RECEIVED;
import static io.aeron.driver.status.SystemCounterDescriptor.STATUS_MESSAGES_RECEIVED;
import static io.aeron.protocol.StatusMessageFlyweight.SEND_SETUP_FLAG;
@@ -76,6 +78,7 @@ public class SendChannelEndpoint extends UdpChannelTransport
private final AtomicCounter statusMessagesReceived;
private final AtomicCounter nakMessagesReceived;
private final AtomicCounter statusIndicator;
+ private final AtomicCounter errorMessagesReceived;
private final boolean isChannelSendTimestampEnabled;
private final EpochNanoClock sendTimestampClock;
private final UnsafeBuffer bufferForTimestamping = new UnsafeBuffer();
@@ -102,6 +105,7 @@ public SendChannelEndpoint(
nakMessagesReceived = context.systemCounters().get(NAK_MESSAGES_RECEIVED);
statusMessagesReceived = context.systemCounters().get(STATUS_MESSAGES_RECEIVED);
+ errorMessagesReceived = context.systemCounters().get(ERROR_FRAMES_RECEIVED);
this.statusIndicator = statusIndicator;
MultiSndDestination multiSndDestination = null;
@@ -400,6 +404,38 @@ public void onStatusMessage(
}
}
+
+ /**
+ * Callback back handler for received error messages.
+ *
+ * @param msg flyweight over the status message.
+ * @param buffer containing the message.
+ * @param length of the message.
+ * @param srcAddress of the message.
+ * @param conductorProxy to send messages back to the conductor.
+ */
+ public void onError(
+ final ErrorFlyweight msg,
+ final UnsafeBuffer buffer,
+ final int length,
+ final InetSocketAddress srcAddress,
+ final DriverConductorProxy conductorProxy)
+ {
+ final int sessionId = msg.sessionId();
+ final int streamId = msg.streamId();
+
+ errorMessagesReceived.incrementOrdered();
+
+ final long destinationRegistrationId = (null != multiSndDestination) ?
+ multiSndDestination.findRegistrationId(msg, srcAddress) : Aeron.NULL_VALUE;
+
+ final NetworkPublication publication = publicationBySessionAndStreamId.get(compoundKey(sessionId, streamId));
+ if (null != publication)
+ {
+ publication.onError(msg, srcAddress, destinationRegistrationId, conductorProxy);
+ }
+ }
+
/**
* Callback back handler for received NAK messages.
*
@@ -699,6 +735,11 @@ else if (datagramChannel.isOpen())
return bytesSent;
}
+
+ public long findRegistrationId(final ErrorFlyweight msg, final InetSocketAddress srcAddress)
+ {
+ return Aeron.NULL_VALUE;
+ }
}
class ManualSndMultiDestination extends MultiSndDestination
@@ -715,20 +756,15 @@ void onStatusMessage(final StatusMessageFlyweight msg, final InetSocketAddress a
for (final Destination destination : destinations)
{
- if (destination.isReceiverIdValid &&
- receiverId == destination.receiverId &&
- address.getPort() == destination.port)
- {
- destination.timeOfLastActivityNs = nowNs;
- break;
- }
- else if (!destination.isReceiverIdValid &&
- address.getPort() == destination.port &&
- address.getAddress().equals(destination.address.getAddress()))
+ if (destination.isMatch(msg.receiverId(), address))
{
+ if (!destination.isReceiverIdValid)
+ {
+ destination.receiverId = receiverId;
+ destination.isReceiverIdValid = true;
+ }
+
destination.timeOfLastActivityNs = nowNs;
- destination.receiverId = receiverId;
- destination.isReceiverIdValid = true;
break;
}
}
@@ -868,6 +904,19 @@ void updateDestination(final String endpoint, final InetSocketAddress newAddress
}
}
}
+
+ public long findRegistrationId(final ErrorFlyweight msg, final InetSocketAddress address)
+ {
+ for (final Destination destination : destinations)
+ {
+ if (destination.isMatch(msg.receiverId(), address))
+ {
+ return destination.registrationId;
+ }
+ }
+
+ return Aeron.NULL_VALUE;
+ }
}
class DynamicSndMultiDestination extends MultiSndDestination
@@ -1062,4 +1111,12 @@ final class Destination extends DestinationRhsPadding
this.port = address.getPort();
this.registrationId = registrationId;
}
+
+ boolean isMatch(final long receiverId, final InetSocketAddress address)
+ {
+ return
+ (isReceiverIdValid && receiverId == this.receiverId && address.getPort() == this.port) ||
+ (!isReceiverIdValid &&
+ address.getPort() == this.port && address.getAddress().equals(this.address.getAddress()));
+ }
}
\ No newline at end of file
diff --git a/aeron-driver/src/main/java/io/aeron/driver/status/SystemCounterDescriptor.java b/aeron-driver/src/main/java/io/aeron/driver/status/SystemCounterDescriptor.java
index e119f34623..c4b6ee552d 100644
--- a/aeron-driver/src/main/java/io/aeron/driver/status/SystemCounterDescriptor.java
+++ b/aeron-driver/src/main/java/io/aeron/driver/status/SystemCounterDescriptor.java
@@ -221,7 +221,17 @@ public enum SystemCounterDescriptor
/**
* A count of the number of times that the retransmit pool has been overflowed.
*/
- RETRANSMIT_OVERFLOW(37, "Retransmit Pool Overflow count");
+ RETRANSMIT_OVERFLOW(37, "Retransmit Pool Overflow count"),
+
+ /**
+ * A count of the number of error frames received by this driver.
+ */
+ ERROR_FRAMES_RECEIVED(38, "Error Frames received"),
+
+ /**
+ * A count of the number of error frames sent by this driver.
+ */
+ ERROR_FRAMES_SENT(39, "Error Frames sent");
/**
* All system counters have the same type id, i.e. system counters are the same type. Other types can exist.
diff --git a/aeron-driver/src/test/java/io/aeron/driver/ReceiverLivenessTrackerTest.java b/aeron-driver/src/test/java/io/aeron/driver/ReceiverLivenessTrackerTest.java
index 4c1b1272a0..8db2525f18 100644
--- a/aeron-driver/src/test/java/io/aeron/driver/ReceiverLivenessTrackerTest.java
+++ b/aeron-driver/src/test/java/io/aeron/driver/ReceiverLivenessTrackerTest.java
@@ -80,7 +80,6 @@ void shouldNotBeLiveIfReceiverTimedOutAfter()
assertFalse(receiverLivenessTracker.hasReceivers());
}
-
@Test
void shouldNotBeLiveIfReceiverRemoved()
{
@@ -104,4 +103,29 @@ void shouldNotBeLiveIfReceiverRemoved()
assertFalse(receiverLivenessTracker.hasReceivers());
}
+ @Test
+ void shouldReturnFalseIfAlreadyRemoved()
+ {
+ final long receiverId1 = 10001;
+ final long receiverId2 = 10002;
+ final long receiverId3 = 10003;
+ final long nowNs = 10000000000L;
+
+ final ReceiverLivenessTracker receiverLivenessTracker = new ReceiverLivenessTracker();
+ receiverLivenessTracker.onStatusMessage(receiverId1, nowNs);
+ receiverLivenessTracker.onStatusMessage(receiverId2, nowNs);
+ receiverLivenessTracker.onStatusMessage(receiverId3, nowNs);
+
+ assertTrue(receiverLivenessTracker.onRemoteClose(receiverId1));
+ assertFalse(receiverLivenessTracker.onRemoteClose(receiverId1));
+ assertTrue(receiverLivenessTracker.hasReceivers());
+
+ assertTrue(receiverLivenessTracker.onRemoteClose(receiverId2));
+ assertFalse(receiverLivenessTracker.onRemoteClose(receiverId2));
+ assertTrue(receiverLivenessTracker.hasReceivers());
+
+ assertTrue(receiverLivenessTracker.onRemoteClose(receiverId3));
+ assertFalse(receiverLivenessTracker.onRemoteClose(receiverId3));
+ assertFalse(receiverLivenessTracker.hasReceivers());
+ }
}
\ No newline at end of file
diff --git a/aeron-driver/src/test/java/io/aeron/driver/TaggedMulticastFlowControlTest.java b/aeron-driver/src/test/java/io/aeron/driver/TaggedMulticastFlowControlTest.java
index fa6bb54bdd..510ca494ce 100644
--- a/aeron-driver/src/test/java/io/aeron/driver/TaggedMulticastFlowControlTest.java
+++ b/aeron-driver/src/test/java/io/aeron/driver/TaggedMulticastFlowControlTest.java
@@ -16,6 +16,7 @@
package io.aeron.driver;
import io.aeron.driver.media.UdpChannel;
+import io.aeron.protocol.ErrorFlyweight;
import io.aeron.protocol.StatusMessageFlyweight;
import io.aeron.test.Tests;
import org.agrona.concurrent.UnsafeBuffer;
@@ -26,8 +27,10 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
+import java.nio.ByteBuffer;
import java.util.stream.Stream;
+import static io.aeron.protocol.ErrorFlyweight.MAX_ERROR_FRAME_LENGTH;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -171,6 +174,53 @@ void shouldNotIncludeReceiverMoreThanWindowSizeBehindMinPosition()
assertEquals(termOffset2 + WINDOW_LENGTH, onStatusMessage(flowControl, 3, termOffset2, senderLimit, 123L));
}
+ @Test
+ void shouldRemoveEntryFromFlowControlOnEndOfStream()
+ {
+ final UdpChannel udpChannel = UdpChannel.parse(
+ "aeron:udp?endpoint=224.20.30.39:24326|interface=localhost|fc=min,g:123/2");
+
+ flowControl.initialize(
+ newContext(), countersManager, udpChannel, 0, 0, 0, 0, 0);
+
+ final int senderLimit = 5000;
+ final int termOffset1 = WINDOW_LENGTH;
+ final int termOffset2 = WINDOW_LENGTH + 1;
+ final int termOffset3 = WINDOW_LENGTH + 2;
+
+ assertEquals(senderLimit, onStatusMessage(flowControl, 1, termOffset1, senderLimit, 123L));
+ assertEquals(termOffset1 + WINDOW_LENGTH, onStatusMessage(flowControl, 2, termOffset2, senderLimit, 123L));
+ assertEquals(termOffset1 + WINDOW_LENGTH, onStatusMessage(flowControl, 3, termOffset3, senderLimit, 123L));
+
+ final long publicationLimit = onEosStatusMessage(flowControl, 1, termOffset1, senderLimit, 123L);
+
+ assertEquals(termOffset1 + WINDOW_LENGTH, publicationLimit);
+ assertEquals(termOffset2 + WINDOW_LENGTH, onIdle(flowControl, senderLimit));
+ }
+
+ @Test
+ void shouldRemoveEntryFromFlowControlOnError()
+ {
+ final UdpChannel udpChannel = UdpChannel.parse(
+ "aeron:udp?endpoint=224.20.30.39:24326|interface=localhost|fc=min,g:123/2");
+
+ flowControl.initialize(
+ newContext(), countersManager, udpChannel, 0, 0, 0, 0, 0);
+
+ final int senderLimit = 5000;
+ final int termOffset1 = WINDOW_LENGTH;
+ final int termOffset2 = WINDOW_LENGTH + 1;
+ final int termOffset3 = WINDOW_LENGTH + 2;
+
+ assertEquals(senderLimit, onStatusMessage(flowControl, 1, termOffset1, senderLimit, 123L));
+ assertEquals(termOffset1 + WINDOW_LENGTH, onStatusMessage(flowControl, 2, termOffset2, senderLimit, 123L));
+ assertEquals(termOffset1 + WINDOW_LENGTH, onStatusMessage(flowControl, 3, termOffset3, senderLimit, 123L));
+
+ onErrorMessage(flowControl, 1, 123L);
+
+ assertEquals(termOffset2 + WINDOW_LENGTH, onIdle(flowControl, senderLimit));
+ }
+
private long onStatusMessage(
final TaggedMulticastFlowControl flowControl,
final long receiverId,
@@ -178,13 +228,48 @@ private long onStatusMessage(
final long senderLimit,
final Long groupTag)
{
- final StatusMessageFlyweight statusMessageFlyweight = new StatusMessageFlyweight();
- statusMessageFlyweight.wrap(new byte[1024]);
+ return onStatusMessage(flowControl, receiverId, termOffset, senderLimit, groupTag, false);
+ }
+
+ private long onEosStatusMessage(
+ final TaggedMulticastFlowControl flowControl,
+ final long receiverId,
+ final int termOffset,
+ final long senderLimit,
+ final Long groupTag)
+ {
+ return onStatusMessage(flowControl, receiverId, termOffset, senderLimit, groupTag, true);
+ }
+
+ private void onErrorMessage(
+ final TaggedMulticastFlowControl flowControl,
+ final long receiverId,
+ final Long groupTag)
+ {
+ final ErrorFlyweight error = new ErrorFlyweight(ByteBuffer.allocate(MAX_ERROR_FRAME_LENGTH));
+ error
+ .receiverId(receiverId)
+ .groupTag(groupTag);
+
+ flowControl.onError(error, null, 0);
+ }
+
+ private static long onStatusMessage(
+ final TaggedMulticastFlowControl flowControl,
+ final long receiverId,
+ final int termOffset,
+ final long senderLimit,
+ final Long groupTag,
+ final boolean isEos)
+ {
+ final StatusMessageFlyweight statusMessageFlyweight = new StatusMessageFlyweight(ByteBuffer.allocate(1024));
+ final short flags = (isEos ? StatusMessageFlyweight.END_OF_STREAM_FLAG : 0);
statusMessageFlyweight.receiverId(receiverId);
statusMessageFlyweight.consumptionTermId(0);
statusMessageFlyweight.consumptionTermOffset(termOffset);
statusMessageFlyweight.groupTag(groupTag);
+ statusMessageFlyweight.flags(flags);
statusMessageFlyweight.receiverWindowLength(WINDOW_LENGTH);
return flowControl.onStatusMessage(statusMessageFlyweight, null, senderLimit, 0, 0, 0);
diff --git a/aeron-system-tests/src/test/java/io/aeron/RejectImageTest.java b/aeron-system-tests/src/test/java/io/aeron/RejectImageTest.java
new file mode 100644
index 0000000000..6f3194bddf
--- /dev/null
+++ b/aeron-system-tests/src/test/java/io/aeron/RejectImageTest.java
@@ -0,0 +1,601 @@
+/*
+ * Copyright 2014-2024 Real Logic Limited.
+ *
+ * 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
+ *
+ * https://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.driver.MediaDriver;
+import io.aeron.driver.ThreadingMode;
+import io.aeron.exceptions.AeronException;
+import io.aeron.status.PublicationErrorFrame;
+import io.aeron.test.EventLogExtension;
+import io.aeron.test.InterruptAfter;
+import io.aeron.test.InterruptingTestCallback;
+import io.aeron.test.SlowTest;
+import io.aeron.test.SystemTestWatcher;
+import io.aeron.test.Tests;
+import io.aeron.test.driver.TestMediaDriver;
+import org.agrona.CloseHelper;
+import org.agrona.DirectBuffer;
+import org.agrona.concurrent.OneToOneConcurrentArrayQueue;
+import org.agrona.concurrent.UnsafeBuffer;
+import org.agrona.concurrent.status.CountersReader;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+
+import static io.aeron.driver.status.SystemCounterDescriptor.ERRORS;
+import static io.aeron.driver.status.SystemCounterDescriptor.ERROR_FRAMES_RECEIVED;
+import static io.aeron.driver.status.SystemCounterDescriptor.ERROR_FRAMES_SENT;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+@ExtendWith({ EventLogExtension.class, InterruptingTestCallback.class })
+public class RejectImageTest
+{
+ public static final long A_VALUE_THAT_SHOWS_WE_ARENT_SPAMMING_ERROR_MESSAGES = 1000L;
+
+ @RegisterExtension
+ final SystemTestWatcher systemTestWatcher = new SystemTestWatcher();
+
+ private final String channel = "aeron:udp?endpoint=localhost:10000";
+ private final int streamId = 10000;
+ private final DirectBuffer message = new UnsafeBuffer("this is a test message".getBytes(US_ASCII));
+
+ private final MediaDriver.Context context = new MediaDriver.Context()
+ .dirDeleteOnStart(true)
+ .threadingMode(ThreadingMode.SHARED);
+ private TestMediaDriver driver;
+
+ @AfterEach
+ void tearDown()
+ {
+ CloseHelper.quietClose(driver);
+ }
+
+ private TestMediaDriver launch()
+ {
+ driver = TestMediaDriver.launch(context, systemTestWatcher);
+ return driver;
+ }
+
+ private static final class QueuedErrorFrameHandler implements PublicationErrorFrameHandler
+ {
+ private final AtomicInteger counter = new AtomicInteger(0);
+ private final OneToOneConcurrentArrayQueue errorFrameQueue =
+ new OneToOneConcurrentArrayQueue<>(512);
+
+ public void onPublicationError(final PublicationErrorFrame errorFrame)
+ {
+ if (!errorFrameQueue.offer(errorFrame.clone()))
+ {
+ counter.incrementAndGet();
+ }
+ }
+
+ PublicationErrorFrame poll()
+ {
+ if (counter.get() > 0)
+ {
+ throw new RuntimeException("Failed to offer to the errorFrameQueue in the test");
+ }
+
+ return errorFrameQueue.poll();
+ }
+ }
+
+ @Test
+ @InterruptAfter(10)
+ @SlowTest
+ void shouldRejectSubscriptionsImage() throws IOException
+ {
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+
+ final TestMediaDriver driver = launch();
+ final QueuedErrorFrameHandler errorFrameHandler = new QueuedErrorFrameHandler();
+
+ final Aeron.Context ctx = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName())
+ .publicationErrorFrameHandler(errorFrameHandler);
+
+ final AtomicBoolean imageUnavailable = new AtomicBoolean(false);
+
+ try (Aeron aeron = Aeron.connect(ctx);
+ Publication pub = aeron.addPublication(channel, streamId);
+ Subscription sub = aeron.addSubscription(channel, streamId, null, image -> imageUnavailable.set(true)))
+ {
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ final CountersReader countersReader = aeron.countersReader();
+ final long initialErrorFramesReceived = countersReader
+ .getCounterValue(ERROR_FRAMES_RECEIVED.id());
+ final long initialErrorFramesSent = countersReader
+ .getCounterValue(ERROR_FRAMES_SENT.id());
+ final long initialErrors = countersReader
+ .getCounterValue(ERRORS.id());
+
+ while (pub.offer(message) < 0)
+ {
+ Tests.yield();
+ }
+
+ while (0 == sub.poll((buffer, offset, length, header) -> {}, 1))
+ {
+ Tests.yield();
+ }
+
+ final Image image = sub.imageAtIndex(0);
+ assertEquals(pub.position(), image.position());
+
+ final String reason = "Needs to be closed";
+ image.reject(reason);
+
+ final long t0 = System.nanoTime();
+ while (pub.isConnected())
+ {
+ Tests.yield();
+ }
+ final long t1 = System.nanoTime();
+ final long value = driver.context().publicationConnectionTimeoutNs();
+ assertThat(t1 - t0, lessThan(value));
+
+ while (initialErrorFramesReceived == countersReader.getCounterValue(ERROR_FRAMES_RECEIVED.id()) ||
+ initialErrorFramesSent == countersReader.getCounterValue(ERROR_FRAMES_SENT.id()))
+ {
+ Tests.yield();
+ }
+
+ while (initialErrors == countersReader
+ .getCounterValue(ERRORS.id()))
+ {
+ Tests.yield();
+ }
+
+ PublicationErrorFrame errorFrame;
+ while (null == (errorFrame = errorFrameHandler.poll()))
+ {
+ Tests.yield();
+ }
+
+ assertEquals(reason, errorFrame.errorMessage());
+ assertEquals(pub.registrationId(), errorFrame.registrationId());
+
+ while (!imageUnavailable.get())
+ {
+ Tests.yield();
+ }
+
+ assertThat(
+ countersReader.getCounterValue(ERROR_FRAMES_RECEIVED.id()) - initialErrorFramesReceived,
+ lessThan(A_VALUE_THAT_SHOWS_WE_ARENT_SPAMMING_ERROR_MESSAGES));
+
+ assertEquals(1, countersReader.getCounterValue(ERRORS.id()) - initialErrors);
+
+ SystemTests.waitForErrorToOccur(driver.aeronDirectoryName(), containsString(reason), Tests.SLEEP_1_MS);
+ }
+ }
+
+ @Test
+ @InterruptAfter(10)
+ @SlowTest
+ void shouldOnlyReceivePublicationErrorFrameOnRelevantClient() throws IOException
+ {
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+
+ final TestMediaDriver driver = launch();
+ final QueuedErrorFrameHandler errorFrameHandler1 = new QueuedErrorFrameHandler();
+ final QueuedErrorFrameHandler errorFrameHandler2 = new QueuedErrorFrameHandler();
+
+ final Aeron.Context ctx1 = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName())
+ .publicationErrorFrameHandler(errorFrameHandler1);
+
+ final Aeron.Context ctx2 = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName())
+ .publicationErrorFrameHandler(errorFrameHandler2);
+
+ try (Aeron aeron1 = Aeron.connect(ctx1);
+ Aeron aeron2 = Aeron.connect(ctx2);
+ Publication pub = aeron1.addPublication(channel, streamId);
+ Subscription sub = aeron1.addSubscription(channel, streamId))
+ {
+ assertNotNull(aeron2);
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ while (pub.offer(message) < 0)
+ {
+ Tests.yield();
+ }
+
+ while (0 == sub.poll((buffer, offset, length, header) -> {}, 1))
+ {
+ Tests.yield();
+ }
+
+ final Image image = sub.imageAtIndex(0);
+ final String reason = "Needs to be closed";
+ image.reject(reason);
+
+ while (null == errorFrameHandler1.poll())
+ {
+ Tests.yield();
+ }
+
+ final long deadlineMs = System.currentTimeMillis() + 1_000;
+ while (System.currentTimeMillis() < deadlineMs)
+ {
+ assertNull(errorFrameHandler2.poll(), "Aeron client without publication should not report error");
+ Tests.yield();
+ }
+ }
+ }
+
+ @Test
+ @InterruptAfter(10)
+ @SlowTest
+ void shouldReceivePublicationErrorFramesAllRelevantClients() throws IOException
+ {
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+
+ final TestMediaDriver driver = launch();
+ final QueuedErrorFrameHandler errorFrameHandler1 = new QueuedErrorFrameHandler();
+ final QueuedErrorFrameHandler errorFrameHandler2 = new QueuedErrorFrameHandler();
+
+ final Aeron.Context ctx1 = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName())
+ .publicationErrorFrameHandler(errorFrameHandler1);
+
+ final Aeron.Context ctx2 = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName())
+ .publicationErrorFrameHandler(errorFrameHandler2);
+
+ try (Aeron aeron1 = Aeron.connect(ctx1);
+ Aeron aeron2 = Aeron.connect(ctx2);
+ Publication pub = aeron1.addPublication(channel, streamId);
+ Publication pubOther = aeron2.addPublication(channel, streamId);
+ Subscription sub = aeron1.addSubscription(channel, streamId))
+ {
+ assertNotNull(aeron2);
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(pubOther);
+ Tests.awaitConnected(sub);
+
+ while (pub.offer(message) < 0)
+ {
+ Tests.yield();
+ }
+
+ while (0 == sub.poll((buffer, offset, length, header) -> {}, 1))
+ {
+ Tests.yield();
+ }
+
+ final Image image = sub.imageAtIndex(0);
+ final String reason = "Needs to be closed";
+ image.reject(reason);
+
+ while (null == errorFrameHandler1.poll())
+ {
+ Tests.yield();
+ }
+
+ while (null == errorFrameHandler2.poll())
+ {
+ Tests.yield();
+ }
+ }
+ }
+
+ @Test
+ @InterruptAfter(10)
+ @SlowTest
+ void shouldRejectSubscriptionsImageManualMdc()
+ {
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+
+ final QueuedErrorFrameHandler errorFrameHandler = new QueuedErrorFrameHandler();
+ final TestMediaDriver driver = launch();
+
+ final Aeron.Context ctx = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName())
+ .publicationErrorFrameHandler(errorFrameHandler);
+
+ final AtomicInteger imageAvailable = new AtomicInteger(0);
+ final AtomicInteger imageUnavailable = new AtomicInteger(0);
+ final String mdc = "aeron:udp?control-mode=manual";
+ final String channel = "aeron:udp?endpoint=localhost:10000";
+
+ try (Aeron aeron = Aeron.connect(ctx);
+ Publication pub = aeron.addPublication(mdc, streamId);
+ Subscription sub = aeron.addSubscription(
+ channel,
+ streamId,
+ (image) -> imageAvailable.incrementAndGet(),
+ (image) -> imageUnavailable.incrementAndGet()))
+ {
+ final long destinationRegistrationId = pub.asyncAddDestination(channel);
+
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ final long initialErrorMessagesReceived = aeron.countersReader()
+ .getCounterValue(ERROR_FRAMES_RECEIVED.id());
+
+ while (pub.offer(message) < 0)
+ {
+ Tests.yield();
+ }
+
+ while (0 == sub.poll((buffer, offset, length, header) -> {}, 1))
+ {
+ Tests.yield();
+ }
+
+ final Image image = sub.imageAtIndex(0);
+ assertEquals(pub.position(), image.position());
+
+ final int initialAvailable = imageAvailable.get();
+ final String reason = "Needs to be closed";
+ image.reject(reason);
+
+ final long t0 = System.nanoTime();
+ while (pub.isConnected())
+ {
+ Tests.yield();
+ }
+ final long t1 = System.nanoTime();
+ final long value = driver.context().publicationConnectionTimeoutNs();
+ assertThat(t1 - t0, lessThan(value));
+
+ while (initialErrorMessagesReceived == aeron.countersReader()
+ .getCounterValue(ERROR_FRAMES_RECEIVED.id()))
+ {
+ Tests.yield();
+ }
+
+ while (0 == imageUnavailable.get())
+ {
+ Tests.yield();
+ }
+
+ assertThat(
+ aeron.countersReader().getCounterValue(ERROR_FRAMES_RECEIVED.id()),
+ lessThan(A_VALUE_THAT_SHOWS_WE_ARENT_SPAMMING_ERROR_MESSAGES));
+
+ while (initialAvailable != imageAvailable.get())
+ {
+ Tests.yield();
+ }
+
+ PublicationErrorFrame errorFrame;
+ while (null == (errorFrame = errorFrameHandler.poll()))
+ {
+ Tests.yield();
+ }
+
+ assertEquals(reason, errorFrame.errorMessage());
+ assertEquals(pub.registrationId(), errorFrame.registrationId());
+ assertEquals(destinationRegistrationId, errorFrame.destinationRegistrationId());
+ }
+ }
+
+ @Test
+ @InterruptAfter(10)
+ void shouldErrorIfRejectionReasonIsTooLong()
+ {
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+ final byte[] bytes = new byte[1024];
+ Arrays.fill(bytes, (byte)'x');
+ final String tooLongReason = new String(bytes, US_ASCII);
+
+ final TestMediaDriver driver = launch();
+
+ final Aeron.Context ctx = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName());
+
+ try (Aeron aeron = Aeron.connect(ctx);
+ Publication pub = aeron.addPublication(channel, streamId);
+ Subscription sub = aeron.addSubscription(channel, streamId))
+ {
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ assertThrows(AeronException.class, () -> sub.imageAtIndex(0).reject(tooLongReason));
+ }
+ }
+
+
+ @Test
+ @InterruptAfter(10)
+ void shouldErrorIfRejectionReasonIsTooLongForLocalBuffer()
+ {
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+ final byte[] bytes = new byte[1024 * 1024];
+ Arrays.fill(bytes, (byte)'x');
+ final String tooLongReason = new String(bytes, US_ASCII);
+
+ final TestMediaDriver driver = launch();
+
+ final Aeron.Context ctx = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName());
+
+ try (Aeron aeron = Aeron.connect(ctx);
+ Publication pub = aeron.addPublication(channel, streamId);
+ Subscription sub = aeron.addSubscription(channel, streamId))
+ {
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ assertThrows(IllegalArgumentException.class, () -> sub.imageAtIndex(0).reject(tooLongReason));
+ }
+ }
+
+ @Test
+ @InterruptAfter(10)
+ void shouldErrorIfUsingAndIpcChannel()
+ {
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+ final String rejectionReason = "Reject this";
+
+ final TestMediaDriver driver = launch();
+
+ final Aeron.Context ctx = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName());
+
+ try (Aeron aeron = Aeron.connect(ctx);
+ Publication pub = aeron.addPublication(CommonContext.IPC_CHANNEL, streamId);
+ Subscription sub = aeron.addSubscription(CommonContext.IPC_CHANNEL, streamId))
+ {
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ final AeronException ex = assertThrows(AeronException.class, () -> sub.imageAtIndex(0)
+ .reject(rejectionReason));
+ assertThat(ex.getMessage(), containsString("Unable to resolve image for correlationId"));
+ }
+ }
+
+
+ @ParameterizedTest
+ @ValueSource(strings = { "127.0.0.1", "[::1]" })
+ @InterruptAfter(5)
+ void shouldReturnAllParametersToApi(final String addressStr) throws UnknownHostException
+ {
+ final InetAddress address = InetAddress.getByName(addressStr);
+ assumeTrue(address instanceof Inet4Address || System.getProperty("java.net.preferIPv4Stack") == null);
+
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+
+ final TestMediaDriver driver = launch();
+ final QueuedErrorFrameHandler errorFrameHandler = new QueuedErrorFrameHandler();
+
+ final Aeron.Context ctx = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName())
+ .publicationErrorFrameHandler(errorFrameHandler);
+
+ final long groupTag = 1001;
+ final int port = 10001;
+
+ final String mdc = "aeron:udp?control-mode=dynamic|control=" + addressStr + ":10000|fc=tagged,g:" + groupTag;
+ final String channel =
+ "aeron:udp?control=" + addressStr + ":10000|endpoint=" + addressStr + ":" + port + "|gtag=" + groupTag;
+
+ try (Aeron aeron = Aeron.connect(ctx);
+ Publication pub = aeron.addPublication(mdc, streamId);
+ Subscription sub = aeron.addSubscription(channel, streamId))
+ {
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ while (pub.offer(message) < 0)
+ {
+ Tests.yield();
+ }
+
+ while (0 == sub.poll((buffer, offset, length, header) -> {}, 1))
+ {
+ Tests.yield();
+ }
+
+ final Image image = sub.imageAtIndex(0);
+ assertEquals(pub.position(), image.position());
+
+ final String reason = "Needs to be closed";
+ image.reject(reason);
+
+ PublicationErrorFrame errorFrame;
+ while (null == (errorFrame = errorFrameHandler.poll()))
+ {
+ Tests.yield();
+ }
+
+ assertEquals(reason, errorFrame.errorMessage());
+ assertEquals(pub.registrationId(), errorFrame.registrationId());
+ assertEquals(Aeron.NULL_VALUE, errorFrame.destinationRegistrationId());
+ assertEquals(pub.streamId(), errorFrame.streamId());
+ assertEquals(pub.sessionId(), errorFrame.sessionId());
+ assertEquals(groupTag, errorFrame.groupTag());
+ assertNotNull(errorFrame.sourceAddress());
+ assertEquals(new InetSocketAddress(addressStr, port), errorFrame.sourceAddress());
+ }
+ }
+
+ @ParameterizedTest
+ @InterruptAfter(5)
+ @ValueSource(booleans = { true, false })
+ void shouldOnlyReceivePublicationErrorFrames(final boolean isExclusive) throws IOException
+ {
+ context.imageLivenessTimeoutNs(TimeUnit.SECONDS.toNanos(3));
+
+ final TestMediaDriver driver = launch();
+ final QueuedErrorFrameHandler errorFrameHandler = new QueuedErrorFrameHandler();
+
+ final Aeron.Context ctx = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName())
+ .publicationErrorFrameHandler(errorFrameHandler);
+
+ final Function addPub = (aeron) -> isExclusive ?
+ aeron.addExclusivePublication(channel, streamId) : aeron.addPublication(channel, streamId);
+
+ try (Aeron aeron = Aeron.connect(ctx);
+ Publication pub = addPub.apply(aeron);
+ Subscription sub = aeron.addSubscription(channel, streamId))
+ {
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ while (pub.offer(message) < 0)
+ {
+ Tests.yield();
+ }
+
+ while (0 == sub.poll((buffer, offset, length, header) -> {}, 1))
+ {
+ Tests.yield();
+ }
+
+ final Image image = sub.imageAtIndex(0);
+ final String reason = "Needs to be closed";
+ image.reject(reason);
+
+ while (null == errorFrameHandler.poll())
+ {
+ Tests.yield();
+ }
+ }
+ }
+}
diff --git a/aeron-system-tests/src/test/java/io/aeron/SubscriberEndOfStreamTest.java b/aeron-system-tests/src/test/java/io/aeron/SubscriberEndOfStreamTest.java
index cf3e107ff1..1a33e1e7e0 100644
--- a/aeron-system-tests/src/test/java/io/aeron/SubscriberEndOfStreamTest.java
+++ b/aeron-system-tests/src/test/java/io/aeron/SubscriberEndOfStreamTest.java
@@ -308,4 +308,62 @@ void shouldNotLingerUnicastPublicationWhenReceivingEndOfStream()
assertThat((t1 - t0), lessThan(lingerTimeoutMs));
}
}
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "aeron:udp?control-mode=manual|fc=min",
+ "aeron:udp?control-mode=manual|fc=max",
+ })
+ @InterruptAfter(20)
+ @SlowTest
+ void shouldDisconnectPublicationWithEosIfSubscriptionIsClosedMdcManual(final String publicationChannel)
+ {
+ final TestMediaDriver driver = launch();
+ final int streamId = 10000;
+ final String subscriptionChannel = "aeron:udp?endpoint=localhost:10000";
+
+ final Aeron.Context ctx = new Aeron.Context()
+ .aeronDirectoryName(driver.aeronDirectoryName());
+
+ final AtomicBoolean imageUnavailable = new AtomicBoolean(false);
+
+ try (Aeron aeron = Aeron.connect(ctx);
+ Publication pub = aeron.addPublication(publicationChannel, streamId))
+ {
+ pub.addDestination(subscriptionChannel);
+ final Subscription sub = aeron.addSubscription(
+ subscriptionChannel, streamId, image -> {}, image -> imageUnavailable.set(true));
+
+ Tests.awaitConnected(pub);
+ Tests.awaitConnected(sub);
+
+ while (pub.offer(message) < 0)
+ {
+ Tests.yield();
+ }
+
+ while (0 == sub.poll((buffer, offset, length, header) -> {}, 1))
+ {
+ Tests.yield();
+ }
+
+ final Image image = sub.imageAtIndex(0);
+ assertEquals(pub.position(), image.position());
+
+ CloseHelper.close(sub);
+
+ final long t0 = System.nanoTime();
+ while (pub.isConnected())
+ {
+ Tests.yield();
+ }
+ final long t1 = System.nanoTime();
+ assertThat(t1 - t0, lessThan(driver.context().publicationConnectionTimeoutNs()));
+
+ while (!imageUnavailable.get())
+ {
+ Tests.yield();
+ }
+ }
+ }
}