diff --git a/aeron-agent/src/main/java/io/aeron/agent/CmdInterceptor.java b/aeron-agent/src/main/java/io/aeron/agent/CmdInterceptor.java index 9bbe4ebf2d..ee8f57c0bb 100644 --- a/aeron-agent/src/main/java/io/aeron/agent/CmdInterceptor.java +++ b/aeron-agent/src/main/java/io/aeron/agent/CmdInterceptor.java @@ -51,7 +51,8 @@ class CmdInterceptor CMD_IN_REMOVE_RCV_DESTINATION, CMD_OUT_ON_CLIENT_TIMEOUT, CMD_IN_TERMINATE_DRIVER, - CMD_IN_REMOVE_DESTINATION_BY_ID); + CMD_IN_REMOVE_DESTINATION_BY_ID, + CMD_IN_REJECT_IMAGE); @SuppressWarnings("methodlength") @Advice.OnMethodEnter @@ -158,6 +159,10 @@ static void logCmd(final int msgTypeId, final DirectBuffer buffer, final int ind case REMOVE_DESTINATION_BY_ID: LOGGER.log(CMD_IN_REMOVE_DESTINATION_BY_ID, buffer, index, length); break; + + case REJECT_IMAGE: + LOGGER.log(CMD_IN_REJECT_IMAGE, buffer, index, length); + break; } } } diff --git a/aeron-agent/src/main/java/io/aeron/agent/DriverEventCode.java b/aeron-agent/src/main/java/io/aeron/agent/DriverEventCode.java index af61b78bbc..0e1af12356 100644 --- a/aeron-agent/src/main/java/io/aeron/agent/DriverEventCode.java +++ b/aeron-agent/src/main/java/io/aeron/agent/DriverEventCode.java @@ -215,7 +215,12 @@ public enum DriverEventCode implements EventCode /** * Remove destination by id */ - CMD_IN_REMOVE_DESTINATION_BY_ID(56, DriverEventDissector::dissectCommand); + CMD_IN_REMOVE_DESTINATION_BY_ID(56, DriverEventDissector::dissectCommand), + + /** + * Reject image command received by the driver. + */ + CMD_IN_REJECT_IMAGE(57, DriverEventDissector::dissectCommand); static final int EVENT_CODE_TYPE = EventCodeType.DRIVER.getTypeCode(); diff --git a/aeron-agent/src/main/java/io/aeron/agent/DriverEventDissector.java b/aeron-agent/src/main/java/io/aeron/agent/DriverEventDissector.java index c3af7d3a58..3487dc9342 100644 --- a/aeron-agent/src/main/java/io/aeron/agent/DriverEventDissector.java +++ b/aeron-agent/src/main/java/io/aeron/agent/DriverEventDissector.java @@ -56,6 +56,7 @@ final class DriverEventDissector private static final ClientTimeoutFlyweight CLIENT_TIMEOUT = new ClientTimeoutFlyweight(); private static final TerminateDriverFlyweight TERMINATE_DRIVER = new TerminateDriverFlyweight(); private static final DestinationByIdMessageFlyweight DESTINATION_BY_ID = new DestinationByIdMessageFlyweight(); + private static final RejectImageFlyweight REJECT_IMAGE = new RejectImageFlyweight(); static final String CONTEXT = "DRIVER"; @@ -220,6 +221,11 @@ static void dissectCommand( dissectDestinationById(builder); break; + case CMD_IN_REJECT_IMAGE: + REJECT_IMAGE.wrap(buffer, offset + encodedLength); + dissectRejectImage(builder); + break; + default: builder.append("COMMAND_UNKNOWN: ").append(code); break; @@ -779,4 +785,14 @@ private static void dissectDestinationById(final StringBuilder builder) .append("resourceRegistrationId=").append(DESTINATION_BY_ID.resourceRegistrationId()) .append(" destinationRegistrationId=").append(DESTINATION_BY_ID.destinationRegistrationId()); } + + private static void dissectRejectImage(final StringBuilder builder) + { + builder + .append("clientId=").append(REJECT_IMAGE.clientId()) + .append(" correlationId=").append(REJECT_IMAGE.correlationId()) + .append(" imageCorrelationId=").append(REJECT_IMAGE.imageCorrelationId()) + .append(" position=").append(REJECT_IMAGE.position()) + .append(" reason=").append(REJECT_IMAGE.reason()); + } } diff --git a/aeron-client/src/main/c/aeron_client_conductor.c b/aeron-client/src/main/c/aeron_client_conductor.c index c598f7ebbe..7ae44a8e4f 100644 --- a/aeron-client/src/main/c/aeron_client_conductor.c +++ b/aeron-client/src/main/c/aeron_client_conductor.c @@ -165,6 +165,9 @@ int aeron_client_conductor_init(aeron_client_conductor_t *conductor, aeron_conte conductor->error_handler = context->error_handler; conductor->error_handler_clientd = context->error_handler_clientd; + conductor->error_frame_handler = context->error_frame_handler; + conductor->error_frame_handler_clientd = context->error_frame_handler_clientd; + conductor->on_new_publication = context->on_new_publication; conductor->on_new_publication_clientd = context->on_new_publication_clientd; @@ -352,6 +355,19 @@ void aeron_client_conductor_on_driver_response(int32_t type_id, uint8_t *buffer, break; } + case AERON_RESPONSE_ON_PUBLICATION_ERROR: + { + aeron_publication_error_t *response = (aeron_publication_error_t *)buffer; + + if (length < sizeof(aeron_publication_error_t)) + { + goto malformed_command; + } + + result = aeron_client_conductor_on_error_frame(conductor, response); + break; + } + case AERON_RESPONSE_ON_STATIC_COUNTER: { aeron_static_counter_response_t *response = (aeron_static_counter_response_t *)buffer; @@ -367,7 +383,6 @@ void aeron_client_conductor_on_driver_response(int32_t type_id, uint8_t *buffer, default: { - AERON_CLIENT_FORMAT_BUFFER(error_message, "response=%x unknown", type_id); conductor->error_handler( conductor->error_handler_clientd, AERON_ERROR_CODE_UNKNOWN_COMMAND_TYPE_ID, error_message); @@ -2526,6 +2541,119 @@ int aeron_client_conductor_on_operation_success( return 0; } +struct aeron_client_conductor_clientd_stct +{ + aeron_client_conductor_t *conductor; + void *clientd; +}; +typedef struct aeron_client_conductor_clientd_stct aeron_client_conductor_clientd_t; + +void aeron_client_conductor_forward_error(void *clientd, int64_t key, void *value) +{ + aeron_client_conductor_clientd_t *conductor_clientd = (aeron_client_conductor_clientd_t *)clientd; + aeron_client_conductor_t *conductor = conductor_clientd->conductor; + aeron_publication_error_t *response = (aeron_publication_error_t *)conductor_clientd->clientd; + aeron_client_command_base_t *resource = (aeron_client_command_base_t *)value; + + const bool is_publication = AERON_CLIENT_TYPE_PUBLICATION == resource->type && + ((aeron_publication_t *)resource)->original_registration_id == response->registration_id; + const bool is_exclusive_publication = AERON_CLIENT_TYPE_EXCLUSIVE_PUBLICATION == resource->type && + ((aeron_exclusive_publication_t *)resource)->original_registration_id == response->registration_id; + + if (is_publication || is_exclusive_publication) + { + conductor->error_frame_handler( + conductor->error_frame_handler_clientd, (aeron_publication_error_values_t *)response); + } +} + +#ifdef _MSC_VER +#define _Static_assert static_assert +#endif + +_Static_assert( + sizeof(aeron_publication_error_t) == sizeof(aeron_publication_error_values_t), + "sizeof(aeron_publication_error_t) must be equal to sizeof(aeron_publication_error_values_t)"); +_Static_assert( + offsetof(aeron_publication_error_t, registration_id) == offsetof(aeron_publication_error_values_t, registration_id), + "offsetof(aeron_publication_error_t, registration_id) must match offsetof(aeron_publication_error_values_t, registration_id)"); +_Static_assert( + offsetof(aeron_publication_error_t, destination_registration_id) == offsetof(aeron_publication_error_values_t, destination_registration_id), + "offsetof(aeron_publication_error_t, destination_registration_id) must match offsetof(aeron_publication_error_values_t, destination_registration_id)"); +_Static_assert( + offsetof(aeron_publication_error_t, session_id) == offsetof(aeron_publication_error_values_t, session_id), + "offsetof(aeron_publication_error_t, session_id) must match offsetof(aeron_publication_error_values_t, session_id)"); +_Static_assert( + offsetof(aeron_publication_error_t, stream_id) == offsetof(aeron_publication_error_values_t, stream_id), + "offsetof(aeron_publication_error_t, stream_id) must match offsetof(aeron_publication_error_values_t, stream_id)"); +_Static_assert( + offsetof(aeron_publication_error_t, receiver_id) == offsetof(aeron_publication_error_values_t, receiver_id), + "offsetof(aeron_publication_error_t, receiver_id) must match offsetof(aeron_publication_error_values_t, receiver_id)"); +_Static_assert( + offsetof(aeron_publication_error_t, group_tag) == offsetof(aeron_publication_error_values_t, group_tag), + "offsetof(aeron_publication_error_t, group_tag) must match offsetof(aeron_publication_error_values_t, group_tag)"); +_Static_assert( + offsetof(aeron_publication_error_t, address_type) == offsetof(aeron_publication_error_values_t, address_type), + "offsetof(aeron_publication_error_t, address_type) must match offsetof(aeron_publication_error_values_t, address_type)"); +_Static_assert( + offsetof(aeron_publication_error_t, source_port) == offsetof(aeron_publication_error_values_t, source_port), + "offsetof(aeron_publication_error_t, address_port) must match offsetof(aeron_publication_error_values_t, address_port)"); +_Static_assert( + offsetof(aeron_publication_error_t, source_address) == offsetof(aeron_publication_error_values_t, source_address), + "offsetof(aeron_publication_error_t, source_address) must match offsetof(aeron_publication_error_values_t, source_address)"); +_Static_assert( + offsetof(aeron_publication_error_t, error_code) == offsetof(aeron_publication_error_values_t, error_code), + "offsetof(aeron_publication_error_t, error_code) must match offsetof(aeron_publication_error_values_t, error_code)"); +_Static_assert( + offsetof(aeron_publication_error_t, error_message_length) == offsetof(aeron_publication_error_values_t, error_message_length), + "offsetof(aeron_publication_error_t, error_message_length) must match offsetof(aeron_publication_error_values_t, error_message_length)"); +_Static_assert( + offsetof(aeron_publication_error_t, error_message) == offsetof(aeron_publication_error_values_t, error_message), + "offsetof(aeron_publication_error_t, error_message) must match offsetof(aeron_publication_error_values_t, error_message)"); + +int aeron_client_conductor_on_error_frame(aeron_client_conductor_t *conductor, aeron_publication_error_t *response) +{ + aeron_client_conductor_clientd_t clientd = { + .conductor = conductor, + .clientd = response + }; + + aeron_int64_to_ptr_hash_map_for_each( + &conductor->resource_by_id_map, aeron_client_conductor_forward_error, (void *)&clientd); + + return 0; +} + +int aeron_publication_error_values_copy(aeron_publication_error_values_t **dst, aeron_publication_error_values_t *src) +{ + if (NULL == src) + { + AERON_SET_ERR(-1, "%s", "src must not be NULL"); + return -1; + } + + if (NULL == dst) + { + AERON_SET_ERR(-1, "%s", "dst must not be NULL"); + return -1; + } + + size_t error_values_size = sizeof(*src) + (size_t)src->error_message_length; + if (aeron_alloc((void **)dst, error_values_size) < 0) + { + AERON_APPEND_ERR("%s", ""); + return -1; + } + + memcpy((void *)*dst, (void *)src, error_values_size); + return 0; +} + +void aeron_publication_error_values_delete(aeron_publication_error_values_t *to_delete) +{ + aeron_free(to_delete); +} + aeron_subscription_t *aeron_client_conductor_find_subscription_by_id( aeron_client_conductor_t *conductor, int64_t registration_id) { @@ -2859,6 +2987,44 @@ int aeron_client_conductor_offer_destination_command( return 0; } +int aeron_client_conductor_reject_image( + aeron_client_conductor_t *conductor, + int64_t image_correlation_id, + int64_t position, + const char *reason, + int32_t command_type) +{ + size_t reason_length = strlen(reason); + const size_t command_length = sizeof(aeron_reject_image_command_t) + reason_length; + + int rb_offer_fail_count = 0; + int32_t offset; + while ((offset = aeron_mpsc_rb_try_claim(&conductor->to_driver_buffer, command_type, command_length)) < 0) + { + if (++rb_offer_fail_count > AERON_CLIENT_COMMAND_RB_FAIL_THRESHOLD) + { + const char *err_buffer = "reject_image command could not be sent"; + conductor->error_handler(conductor->error_handler_clientd, AERON_CLIENT_ERROR_BUFFER_FULL, err_buffer); + AERON_SET_ERR(AERON_CLIENT_ERROR_BUFFER_FULL, "%s", err_buffer); + return -1; + } + + sched_yield(); + } + + uint8_t *ptr = (conductor->to_driver_buffer.buffer + offset); + aeron_reject_image_command_t *command = (aeron_reject_image_command_t *)ptr; + command->image_correlation_id = image_correlation_id; + command->position = position; + command->reason_length = (int32_t)reason_length; + memcpy(ptr + offsetof(aeron_reject_image_command_t, reason_text), reason, reason_length); + command->reason_text[reason_length] = '\0'; + + aeron_mpsc_rb_commit(&conductor->to_driver_buffer, offset); + + return 0; +} + extern int aeron_counter_heartbeat_timestamp_find_counter_id_by_registration_id( aeron_counters_reader_t *counters_reader, int32_t type_id, int64_t registration_id); extern bool aeron_counter_heartbeat_timestamp_is_active( diff --git a/aeron-client/src/main/c/aeron_client_conductor.h b/aeron-client/src/main/c/aeron_client_conductor.h index 64ab81e7fd..7c02ba00ea 100644 --- a/aeron-client/src/main/c/aeron_client_conductor.h +++ b/aeron-client/src/main/c/aeron_client_conductor.h @@ -226,6 +226,9 @@ typedef struct aeron_client_conductor_stct aeron_error_handler_t error_handler; void *error_handler_clientd; + aeron_publication_error_frame_handler_t error_frame_handler; + void *error_frame_handler_clientd; + aeron_on_new_publication_t on_new_publication; void *on_new_publication_clientd; @@ -384,6 +387,7 @@ int aeron_client_conductor_on_unavailable_counter( int aeron_client_conductor_on_static_counter(aeron_client_conductor_t *conductor, aeron_static_counter_response_t *response); int aeron_client_conductor_on_client_timeout(aeron_client_conductor_t *conductor, aeron_client_timeout_t *response); +int aeron_client_conductor_on_error_frame(aeron_client_conductor_t *conductor, aeron_publication_error_t *response); int aeron_client_conductor_get_or_create_log_buffer( aeron_client_conductor_t *conductor, @@ -405,6 +409,13 @@ int aeron_client_conductor_offer_destination_command( const char *uri, int64_t *correlation_id); +int aeron_client_conductor_reject_image( + aeron_client_conductor_t *conductor, + int64_t image_correlation_id, + int64_t position, + const char *reason, + int32_t command_type); + inline int aeron_counter_heartbeat_timestamp_find_counter_id_by_registration_id( aeron_counters_reader_t *counters_reader, int32_t type_id, int64_t registration_id) { diff --git a/aeron-client/src/main/c/aeron_context.c b/aeron-client/src/main/c/aeron_context.c index 32b54c61a5..3a3f5f26cf 100644 --- a/aeron-client/src/main/c/aeron_context.c +++ b/aeron-client/src/main/c/aeron_context.c @@ -44,6 +44,10 @@ void aeron_default_error_handler(void *clientd, int errcode, const char *message exit(EXIT_FAILURE); } +void aeron_default_error_frame_handler(void *clientd, aeron_publication_error_values_t *message) +{ +} + int aeron_context_init(aeron_context_t **context) { aeron_context_t *_context = NULL; @@ -78,6 +82,7 @@ int aeron_context_init(aeron_context_t **context) _context->client_name = NULL; _context->error_handler = aeron_default_error_handler; _context->error_handler_clientd = NULL; + _context->error_frame_handler = aeron_default_error_frame_handler; _context->on_new_publication = NULL; _context->on_new_publication_clientd = NULL; _context->on_new_exclusive_publication = NULL; @@ -337,6 +342,26 @@ void *aeron_context_get_error_handler_clientd(aeron_context_t *context) return NULL != context ? context->error_handler_clientd : NULL; } +int aeron_context_set_publication_error_frame_handler(aeron_context_t *context, aeron_publication_error_frame_handler_t handler, void *clientd) +{ + AERON_CONTEXT_SET_CHECK_ARG_AND_RETURN(-1, context); + + context->error_frame_handler = handler; + context->error_frame_handler_clientd = clientd; + return 0; +} + +aeron_publication_error_frame_handler_t aeron_context_get_publication_error_frame_handler(aeron_context_t *context) +{ + return NULL != context ? context->error_frame_handler : NULL; +} + +void *aeron_context_get_publication_error_frame_handler_clientd(aeron_context_t *context) +{ + return NULL != context ? context->error_frame_handler_clientd : NULL; +} + + int aeron_context_set_on_new_publication(aeron_context_t *context, aeron_on_new_publication_t handler, void *clientd) { AERON_CONTEXT_SET_CHECK_ARG_AND_RETURN(-1, context); diff --git a/aeron-client/src/main/c/aeron_context.h b/aeron-client/src/main/c/aeron_context.h index bb293b1ae4..f56638df9a 100644 --- a/aeron-client/src/main/c/aeron_context.h +++ b/aeron-client/src/main/c/aeron_context.h @@ -55,6 +55,9 @@ typedef struct aeron_context_stct aeron_on_unavailable_counter_t on_unavailable_counter; void *on_unavailable_counter_clientd; + aeron_publication_error_frame_handler_t error_frame_handler; + void *error_frame_handler_clientd; + aeron_agent_on_start_func_t agent_on_start_func; void *agent_on_start_state; diff --git a/aeron-client/src/main/c/aeron_image.c b/aeron-client/src/main/c/aeron_image.c index 43f4e0c3ee..90182689f6 100644 --- a/aeron-client/src/main/c/aeron_image.c +++ b/aeron-client/src/main/c/aeron_image.c @@ -805,6 +805,26 @@ bool aeron_image_is_closed(aeron_image_t *image) return is_closed; } +int aeron_image_reject(aeron_image_t *image, const char *reason) +{ + if (NULL == image) + { + AERON_SET_ERR(EINVAL, "%s", "image is null"); + return -1; + } + + int64_t position = 0; + AERON_GET_VOLATILE(position, *image->subscriber_position); + + if (aeron_subscription_reject_image(image->subscription, image->key.correlation_id, position, reason) < 0) + { + AERON_APPEND_ERR("%s", ""); + return -1; + } + + return 0; +} + extern int64_t aeron_image_removal_change_number(aeron_image_t *image); extern bool aeron_image_is_in_use_by_subscription(aeron_image_t *image, int64_t last_change_number); diff --git a/aeron-client/src/main/c/aeron_subscription.c b/aeron-client/src/main/c/aeron_subscription.c index 6c4b1d5321..6b7d8b4950 100644 --- a/aeron-client/src/main/c/aeron_subscription.c +++ b/aeron-client/src/main/c/aeron_subscription.c @@ -734,6 +734,19 @@ int aeron_subscription_try_resolve_channel_endpoint_port( return result; } +int aeron_subscription_reject_image( + aeron_subscription_t *subscription, int64_t image_correlation_id, int64_t position, const char *reason) +{ + if (aeron_client_conductor_reject_image( + subscription->conductor, image_correlation_id, position, reason, AERON_COMMAND_REJECT_IMAGE) < 0) + { + AERON_APPEND_ERR("%s", ""); + return -1; + } + + return 0; +} + extern int aeron_subscription_find_image_index(aeron_image_list_t *volatile image_list, aeron_image_t *image); extern int64_t aeron_subscription_last_image_list_change_number(aeron_subscription_t *subscription); extern void aeron_subscription_propose_last_image_change_number( diff --git a/aeron-client/src/main/c/aeron_subscription.h b/aeron-client/src/main/c/aeron_subscription.h index 143314a7ad..8ab6d428a9 100644 --- a/aeron-client/src/main/c/aeron_subscription.h +++ b/aeron-client/src/main/c/aeron_subscription.h @@ -133,4 +133,7 @@ inline void aeron_subscription_propose_last_image_change_number( } } +int aeron_subscription_reject_image( + aeron_subscription_t *subscription, int64_t image_correlation_id, int64_t position, const char *reason); + #endif //AERON_C_SUBSCRIPTION_H diff --git a/aeron-client/src/main/c/aeronc.h b/aeron-client/src/main/c/aeronc.h index 254a6135c5..a0599c8919 100644 --- a/aeron-client/src/main/c/aeronc.h +++ b/aeron-client/src/main/c/aeronc.h @@ -34,6 +34,9 @@ extern "C" #define AERON_CLIENT_ERROR_BUFFER_FULL (-1003) #define AERON_CLIENT_MAX_LOCAL_ADDRESS_STR_LEN (64) +#define AERON_RESPONSE_ADDRESS_TYPE_IPV4 (0x1) +#define AERON_RESPONSE_ADDRESS_TYPE_IPV6 (0x2) + typedef struct aeron_context_stct aeron_context_t; typedef struct aeron_stct aeron_t; typedef struct aeron_buffer_claim_stct aeron_buffer_claim_t; @@ -64,6 +67,23 @@ typedef struct aeron_header_values_stct size_t position_bits_to_shift; } aeron_header_values_t; + +struct aeron_publication_error_values_stct +{ + 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; + int16_t address_type; + uint16_t source_port; + uint8_t source_address[16]; + int32_t error_code; + int32_t error_message_length; + uint8_t error_message[1]; +}; +typedef struct aeron_publication_error_values_stct aeron_publication_error_values_t; #pragma pack(pop) typedef struct aeron_subscription_stct aeron_subscription_t; @@ -85,6 +105,7 @@ typedef struct aeron_image_controlled_fragment_assembler_stct aeron_image_contro typedef struct aeron_fragment_assembler_stct aeron_fragment_assembler_t; typedef struct aeron_controlled_fragment_assembler_stct aeron_controlled_fragment_assembler_t; + /** * Environment variables and functions used for setting values of an aeron_context_t. */ @@ -130,6 +151,30 @@ const char *aeron_context_get_client_name(aeron_context_t *context); */ typedef void (*aeron_error_handler_t)(void *clientd, int errcode, const char *message); +/** + * The error frame handler to be called when the driver notifies the client about an error frame being received. + * The data passed to this callback will only be valid for the lifetime of the callback. The user should use + * aeron_publication_error_values_copy if they require the data to live longer than that. + */ +typedef void (*aeron_publication_error_frame_handler_t)(void *clientd, aeron_publication_error_values_t *error_frame); + +/** + * Copy an existing aeron_publication_error_values_t to the supplied pointer. The caller is responsible for freeing the + * allocated memory using aeron_publication_error_values_delete when the copy is not longer required. + * + * @param dst to copy the values to. + * @param src to copy the values from. + * @return 0 if this is successful, -1 otherwise. Will set aeron_errcode() and aeron_errmsg() on failure. + */ +int aeron_publication_error_values_copy(aeron_publication_error_values_t **dst, aeron_publication_error_values_t *src); + +/** + * Delete a instance of aeron_publication_error_values_t that was created when making a copy + * (aeron_publication_error_values_copy). This should not be use on the pointer received via the aeron_frame_handler_t. + * @param to_delete to be deleted. + */ +void aeron_publication_error_values_delete(aeron_publication_error_values_t *to_delete); + /** * Generalised notification callback. */ @@ -139,6 +184,10 @@ int aeron_context_set_error_handler(aeron_context_t *context, aeron_error_handle aeron_error_handler_t aeron_context_get_error_handler(aeron_context_t *context); void *aeron_context_get_error_handler_clientd(aeron_context_t *context); +int aeron_context_set_publication_error_frame_handler(aeron_context_t *context, aeron_publication_error_frame_handler_t handler, void *clientd); +aeron_publication_error_frame_handler_t aeron_context_get_publication_error_frame_handler(aeron_context_t *context); +void *aeron_context_get_publication_error_frame_handler_clientd(aeron_context_t *context); + /** * Function called by aeron_client_t to deliver notification that the media driver has added an aeron_publication_t * or aeron_exclusive_publication_t successfully. @@ -2062,6 +2111,14 @@ int aeron_image_block_poll( bool aeron_image_is_closed(aeron_image_t *image); +/** + * Force the driver to disconnect this image from the remote publication. + * + * @param image to be rejected. + * @param reason an error message to be forwarded back to the publication. + */ +int aeron_image_reject(aeron_image_t *image, const char *reason); + /** * A fragment handler that sits in a chain-of-responsibility pattern that reassembles fragmented messages * so that the next handler in the chain only sees whole messages. diff --git a/aeron-client/src/main/c/command/aeron_control_protocol.h b/aeron-client/src/main/c/command/aeron_control_protocol.h index e465c42a6b..92f526de2c 100644 --- a/aeron-client/src/main/c/command/aeron_control_protocol.h +++ b/aeron-client/src/main/c/command/aeron_control_protocol.h @@ -35,7 +35,8 @@ #define AERON_COMMAND_REMOVE_RCV_DESTINATION (0x0D) #define AERON_COMMAND_TERMINATE_DRIVER (0x0E) #define AERON_COMMAND_ADD_STATIC_COUNTER (0x0F) -#define AERON_COMMAND_REMOVE_DESTINATION_BY_ID (0x10) +#define AERON_COMMAND_REJECT_IMAGE (0x10) +#define AERON_COMMAND_REMOVE_DESTINATION_BY_ID (0x11) #define AERON_RESPONSE_ON_ERROR (0x0F01) #define AERON_RESPONSE_ON_AVAILABLE_IMAGE (0x0F02) @@ -48,6 +49,7 @@ #define AERON_RESPONSE_ON_UNAVAILABLE_COUNTER (0x0F09) #define AERON_RESPONSE_ON_CLIENT_TIMEOUT (0x0F0A) #define AERON_RESPONSE_ON_STATIC_COUNTER (0x0F0B) +#define AERON_RESPONSE_ON_PUBLICATION_ERROR (0x0F0C) /* error codes */ #define AERON_ERROR_CODE_UNKNOWN_CODE_VALUE (-1) @@ -208,6 +210,33 @@ typedef struct aeron_terminate_driver_command_stct } aeron_terminate_driver_command_t; +typedef struct aeron_reject_image_command_stct +{ + aeron_correlated_command_t correlated; + int64_t image_correlation_id; + int64_t position; + int32_t reason_length; + uint8_t reason_text[1]; +} +aeron_reject_image_command_t; + +struct aeron_publication_error_stct +{ + 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; + int16_t address_type; + uint16_t source_port; + uint8_t source_address[16]; + int32_t error_code; + int32_t error_message_length; + uint8_t error_message[1]; +}; +typedef struct aeron_publication_error_stct aeron_publication_error_t; + #pragma pack(pop) #endif //AERON_CONTROL_PROTOCOL_H diff --git a/aeron-client/src/main/c/protocol/aeron_udp_protocol.h b/aeron-client/src/main/c/protocol/aeron_udp_protocol.h index 374016ddc5..a7918903ee 100644 --- a/aeron-client/src/main/c/protocol/aeron_udp_protocol.h +++ b/aeron-client/src/main/c/protocol/aeron_udp_protocol.h @@ -79,6 +79,18 @@ typedef struct aeron_status_message_header_stct } aeron_status_message_header_t; +struct aeron_error_stct +{ + aeron_frame_header_t frame_header; + int32_t session_id; + int32_t stream_id; + int64_t receiver_id; + int64_t group_tag; + int32_t error_code; + int32_t error_length; +}; +typedef struct aeron_error_stct aeron_error_t; + typedef struct aeron_status_message_optional_header_stct { int64_t group_tag; @@ -199,6 +211,10 @@ int aeron_udp_protocol_group_tag(aeron_status_message_header_t *sm, int64_t *gro #define AERON_OPT_HDR_ALIGNMENT (4u) +#define AERON_ERROR_MAX_TEXT_LENGTH (1023) +#define AERON_ERROR_MAX_FRAME_LENGTH (sizeof(aeron_error_t) + AERON_ERROR_MAX_TEXT_LENGTH) +#define AERON_ERROR_HAS_GROUP_TAG_FLAG (0x08) + inline size_t aeron_res_header_address_length(int8_t res_type) { return AERON_RES_HEADER_TYPE_NAME_TO_IP6_MD == res_type ? diff --git a/aeron-client/src/main/c/util/aeron_strutil.c b/aeron-client/src/main/c/util/aeron_strutil.c index 657c364fc6..33a486eb81 100644 --- a/aeron-client/src/main/c/util/aeron_strutil.c +++ b/aeron-client/src/main/c/util/aeron_strutil.c @@ -264,3 +264,4 @@ int getopt(int argc, char *const argv[], const char *opt_string) extern uint64_t aeron_fnv_64a_buf(uint8_t *buf, size_t len); extern bool aeron_str_length(const char *str, size_t length_bound, size_t *length); +extern void aeron_str_null_terminate(uint8_t *text, int index); diff --git a/aeron-client/src/main/c/util/aeron_strutil.h b/aeron-client/src/main/c/util/aeron_strutil.h index c5db9ca7aa..ba88705ae9 100644 --- a/aeron-client/src/main/c/util/aeron_strutil.h +++ b/aeron-client/src/main/c/util/aeron_strutil.h @@ -115,4 +115,9 @@ inline bool aeron_str_length(const char *str, size_t length_bound, size_t *lengt return result; } +inline void aeron_str_null_terminate(uint8_t *text, int index) +{ + text[index] = '\0'; +} + #endif //AERON_STRUTIL_H diff --git a/aeron-client/src/main/cpp_wrapper/CMakeLists.txt b/aeron-client/src/main/cpp_wrapper/CMakeLists.txt index 1cf7e99d4a..468b0b4a5a 100644 --- a/aeron-client/src/main/cpp_wrapper/CMakeLists.txt +++ b/aeron-client/src/main/cpp_wrapper/CMakeLists.txt @@ -64,6 +64,7 @@ SET(HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/concurrent/status/ReadablePosition.h ${CMAKE_CURRENT_SOURCE_DIR}/concurrent/status/StatusIndicatorReader.h ${CMAKE_CURRENT_SOURCE_DIR}/concurrent/status/UnsafeBufferPosition.h + ${CMAKE_CURRENT_SOURCE_DIR}/status/PublicationErrorFrame.h ${CMAKE_CURRENT_SOURCE_DIR}/util/BitUtil.h ${CMAKE_CURRENT_SOURCE_DIR}/util/CommandOption.h ${CMAKE_CURRENT_SOURCE_DIR}/util/CommandOptionParser.h diff --git a/aeron-client/src/main/cpp_wrapper/Context.h b/aeron-client/src/main/cpp_wrapper/Context.h index cb69715946..12b39a7009 100644 --- a/aeron-client/src/main/cpp_wrapper/Context.h +++ b/aeron-client/src/main/cpp_wrapper/Context.h @@ -24,6 +24,7 @@ #include "concurrent/AgentRunner.h" #include "concurrent/CountersReader.h" #include "CncFileDescriptor.h" +#include "status/PublicationErrorFrame.h" #include "aeronc.h" @@ -133,6 +134,8 @@ typedef std::function on_close_client_t; +typedef std::function on_publication_error_frame_t; + const static long NULL_TIMEOUT = -1; const static long DEFAULT_MEDIA_DRIVER_TIMEOUT_MS = 10000; const static long DEFAULT_RESOURCE_LINGER_MS = 5000; @@ -193,6 +196,10 @@ inline void defaultOnCloseClientHandler() { } +inline void defaultOnErrorFrameHandler(aeron::status::PublicationErrorFrame &) +{ +} + /** * This class provides configuration for the {@link Aeron} class via the {@link Aeron::Aeron} or {@link Aeron::connect} * methods and its overloads. It gives applications some control over the interactions with the Aeron Media Driver. @@ -222,7 +229,8 @@ class Context m_onNewSubscriptionHandler(other.m_onNewSubscriptionHandler), m_onAvailableCounterHandler(other.m_onAvailableCounterHandler), m_onUnavailableCounterHandler(other.m_onUnavailableCounterHandler), - m_onCloseClientHandler(other.m_onCloseClientHandler) + m_onCloseClientHandler(other.m_onCloseClientHandler), + m_onErrorFrameHandler(other.m_onErrorFrameHandler) { other.m_context = nullptr; } @@ -526,6 +534,19 @@ class Context return *this; } + /** + * Set handler to receive notifications when a publication error is received for a publication that this client + * is interested in. + * + * @param handler + * @return reference to this Context instance. + */ + inline this_t &errorFrameHandler(on_publication_error_frame_t &handler) + { + m_onErrorFrameHandler = handler; + return *this; + } + static bool requestDriverTermination( const std::string &directory, const std::uint8_t *tokenBuffer, std::size_t tokenLength) { @@ -576,6 +597,7 @@ class Context on_available_counter_t m_onAvailableCounterHandler = defaultOnAvailableCounterHandler; on_unavailable_counter_t m_onUnavailableCounterHandler = defaultOnUnavailableCounterHandler; on_close_client_t m_onCloseClientHandler = defaultOnCloseClientHandler; + on_publication_error_frame_t m_onErrorFrameHandler = defaultOnErrorFrameHandler; void attachCallbacksToContext() { @@ -634,6 +656,14 @@ class Context { throw IllegalArgumentException(std::string(aeron_errmsg()), SOURCEINFO); } + + if (aeron_context_set_publication_error_frame_handler( + m_context, + errorFrameHandlerCallback, + const_cast(reinterpret_cast(&m_onErrorFrameHandler))) < 0) + { + throw IllegalArgumentException(std::string(aeron_errmsg()), SOURCEINFO); + } } static void errorHandlerCallback(void *clientd, int errcode, const char *message) @@ -685,6 +715,13 @@ class Context on_close_client_t &handler = *reinterpret_cast(clientd); handler(); } + + static void errorFrameHandlerCallback(void *clientd, aeron_publication_error_values_t *error_frame) + { + on_publication_error_frame_t &handler = *reinterpret_cast(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(); + } + } + } }