diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd2e3cf4..76bf97d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: - 'main' env: - BUILDER_VERSION: v0.9.62 + BUILDER_VERSION: v0.9.64 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-c-mqtt diff --git a/CMakeLists.txt b/CMakeLists.txt index b33f24d6..4a3348ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ file(GLOB AWS_MQTT_PRIV_EXPOSED_HEADERS file(GLOB AWS_MQTT_SRC "source/*.c" "source/v5/*.c" + "source/request-response/*.c" ) file(GLOB MQTT_HEADERS @@ -121,6 +122,7 @@ if (BUILD_TESTING) if (NOT CMAKE_CROSSCOMPILING ) add_subdirectory(bin/elastipubsub) add_subdirectory(bin/elastipubsub5) + add_subdirectory(bin/elastishadow) add_subdirectory(bin/mqtt5canary) endif() endif () diff --git a/bin/elastishadow/CMakeLists.txt b/bin/elastishadow/CMakeLists.txt new file mode 100644 index 00000000..c86e47c3 --- /dev/null +++ b/bin/elastishadow/CMakeLists.txt @@ -0,0 +1,29 @@ +project(elastishadow C) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_INSTALL_PREFIX}/lib/cmake") + +file(GLOB ELASTISHADOW_SRC + "*.c" + ) + +set(ELASTISHADOW_PROJECT_NAME elastishadow) +add_executable(${ELASTISHADOW_PROJECT_NAME} ${ELASTISHADOW_SRC}) +aws_set_common_properties(${ELASTISHADOW_PROJECT_NAME}) + + +target_include_directories(${ELASTISHADOW_PROJECT_NAME} PUBLIC + $ + $) + +target_link_libraries(${ELASTISHADOW_PROJECT_NAME} PRIVATE aws-c-mqtt) + +if (BUILD_SHARED_LIBS AND NOT WIN32) + message(INFO " elastishadow will be built with shared libs, but you may need to set LD_LIBRARY_PATH=${CMAKE_INSTALL_PREFIX}/lib to run the application") +endif() + +install(TARGETS ${ELASTISHADOW_PROJECT_NAME} + EXPORT ${ELASTISHADOW_PROJECT_NAME}-targets + COMPONENT Runtime + RUNTIME + DESTINATION bin + COMPONENT Runtime) diff --git a/bin/elastishadow/main.c b/bin/elastishadow/main.c new file mode 100644 index 00000000..b9d15e87 --- /dev/null +++ b/bin/elastishadow/main.c @@ -0,0 +1,1272 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#ifdef _MSC_VER +# pragma warning(disable : 4996) /* Disable warnings about fopen() being insecure */ +# pragma warning(disable : 4204) /* Declared initializers */ +# pragma warning(disable : 4221) /* Local var in declared initializer */ +#endif + +#ifdef WIN32 +// Windows does not need specific imports +#else +# include +#endif + +struct app_ctx { + struct aws_allocator *allocator; + struct aws_mutex lock; + struct aws_condition_variable signal; + struct aws_uri uri; + uint32_t port; + const char *cert; + const char *key; + + struct aws_tls_connection_options tls_connection_options; + + const char *log_filename; + enum aws_log_level log_level; + + struct aws_mqtt5_client *client; + struct aws_mqtt_request_response_client *rr_client; + + /* + * &aws_shadow_streaming_operation.id -> aws_shadow_streaming_operation * + */ + struct aws_hash_table streaming_operations; + uint64_t next_id; +}; + +struct aws_shadow_streaming_operation { + struct aws_allocator *allocator; + + struct aws_byte_buf thing; + struct aws_byte_buf shadow; + struct aws_byte_buf topic_filter; + + uint64_t id; + struct aws_mqtt_rr_client_operation *streaming_operation; +}; + +struct aws_shadow_streaming_operation *s_aws_shadow_streaming_operation_new( + struct aws_allocator *allocator, + struct app_ctx *app_ctx, + struct aws_byte_cursor thing, + struct aws_byte_cursor shadow, + struct aws_byte_cursor topic_filter) { + struct aws_shadow_streaming_operation *operation = + aws_mem_calloc(allocator, 1, sizeof(struct aws_shadow_streaming_operation)); + + operation->allocator = allocator; + operation->id = ++app_ctx->next_id; + operation->streaming_operation = NULL; + + aws_byte_buf_init_copy_from_cursor(&operation->thing, allocator, thing); + aws_byte_buf_init_copy_from_cursor(&operation->shadow, allocator, shadow); + aws_byte_buf_init_copy_from_cursor(&operation->topic_filter, allocator, topic_filter); + + return operation; +} + +void s_aws_shadow_streaming_operation_destroy(struct aws_shadow_streaming_operation *operation) { + if (operation == NULL) { + return; + } + + aws_byte_buf_clean_up(&operation->thing); + aws_byte_buf_clean_up(&operation->shadow); + aws_byte_buf_clean_up(&operation->topic_filter); + + aws_mem_release(operation->allocator, operation); +} + +static void s_release_streaming_operation(void *value) { + struct aws_shadow_streaming_operation *operation = value; + + if (operation->streaming_operation != NULL) { + aws_mqtt_rr_client_operation_release(operation->streaming_operation); + } else { + s_aws_shadow_streaming_operation_destroy(operation); + } +} + +static void s_usage(int exit_code) { + + fprintf(stderr, "usage: elastishadow [options] endpoint\n"); + fprintf(stderr, " endpoint: url to connect to\n"); + fprintf(stderr, "\n Options:\n\n"); + fprintf(stderr, " --cert FILE: path to a PEM encoded certificate to use with mTLS\n"); + fprintf(stderr, " --key FILE: Path to a PEM encoded private key that matches cert.\n"); + fprintf(stderr, " -l, --log FILE: dumps logs to FILE instead of stderr.\n"); + fprintf(stderr, " -v, --verbose: ERROR|INFO|DEBUG|TRACE: log level to configure. Default is none.\n"); + fprintf(stderr, " -h, --help\n"); + fprintf(stderr, " Display this message and quit.\n"); + exit(exit_code); +} + +static struct aws_cli_option s_long_options[] = { + {"cert", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'c'}, + {"key", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'e'}, + {"log", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'l'}, + {"verbose", AWS_CLI_OPTIONS_REQUIRED_ARGUMENT, NULL, 'v'}, + {"help", AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 'h'}, + /* Per getopt(3) the last element of the array has to be filled with all zeros */ + {NULL, AWS_CLI_OPTIONS_NO_ARGUMENT, NULL, 0}, +}; + +static void s_parse_options(int argc, char **argv, struct app_ctx *ctx) { + bool uri_found = false; + + while (true) { + int option_index = 0; + int c = aws_cli_getopt_long(argc, argv, "c:e:l:v:h", s_long_options, &option_index); + if (c == -1) { + break; + } + + switch (c) { + case 0: + /* getopt_long() returns 0 if an option.flag is non-null */ + break; + case 'c': + ctx->cert = aws_cli_optarg; + break; + case 'e': + ctx->key = aws_cli_optarg; + break; + case 'l': + ctx->log_filename = aws_cli_optarg; + break; + case 'v': + if (!strcmp(aws_cli_optarg, "TRACE")) { + ctx->log_level = AWS_LL_TRACE; + } else if (!strcmp(aws_cli_optarg, "INFO")) { + ctx->log_level = AWS_LL_INFO; + } else if (!strcmp(aws_cli_optarg, "DEBUG")) { + ctx->log_level = AWS_LL_DEBUG; + } else if (!strcmp(aws_cli_optarg, "ERROR")) { + ctx->log_level = AWS_LL_ERROR; + } else { + fprintf(stderr, "unsupported log level %s.\n", aws_cli_optarg); + s_usage(1); + } + break; + case 'h': + s_usage(0); + break; + case 0x02: { + struct aws_byte_cursor uri_cursor = aws_byte_cursor_from_c_str(aws_cli_positional_arg); + if (aws_uri_init_parse(&ctx->uri, ctx->allocator, &uri_cursor)) { + fprintf( + stderr, + "Failed to parse uri %s with error %s\n", + (char *)uri_cursor.ptr, + aws_error_debug_str(aws_last_error())); + s_usage(1); + } + uri_found = true; + break; + } + + default: + fprintf(stderr, "Unknown option\n"); + s_usage(1); + } + } + + if (!uri_found) { + fprintf(stderr, "A URI for the request must be supplied.\n"); + s_usage(1); + } +} + +static bool s_skip_whitespace(uint8_t value) { + return value == '\n' || value == '\r' || value == '\t' || value == ' '; +} + +static void s_split_command_line(struct aws_byte_cursor cursor, struct aws_array_list *words) { + struct aws_byte_cursor split_cursor; + AWS_ZERO_STRUCT(split_cursor); + + while (aws_byte_cursor_next_split(&cursor, ' ', &split_cursor)) { + struct aws_byte_cursor word_cursor = aws_byte_cursor_trim_pred(&split_cursor, &s_skip_whitespace); + if (word_cursor.len > 0) { + aws_array_list_push_back(words, &word_cursor); + } + } +} + +static void s_write_correlation_token_string(struct aws_byte_cursor scratch_space) { + struct aws_byte_buf correlation_token_buf = aws_byte_buf_from_empty_array(scratch_space.ptr, scratch_space.len); + + struct aws_uuid uuid; + aws_uuid_init(&uuid); + aws_uuid_to_str(&uuid, &correlation_token_buf); +} + +static void s_on_get_shadow_complete( + const struct aws_byte_cursor *response_topic, + const struct aws_byte_cursor *payload, + int error_code, + void *user_data) { + + struct aws_string *correlation_token = user_data; + + if (payload != NULL) { + printf( + "GetNamedShadow request '%s' response received on topic '" PRInSTR "' with body:\n " PRInSTR "\n\n", + correlation_token->bytes, + AWS_BYTE_CURSOR_PRI(*response_topic), + AWS_BYTE_CURSOR_PRI(*payload)); + } else { + printf( + "GetNamedShadow request '%s' failed with error code %d(%s)\n\n", + correlation_token->bytes, + error_code, + aws_error_debug_str(error_code)); + } + + aws_string_destroy(correlation_token); +} + +static void s_handle_get(struct app_ctx *context, struct aws_allocator *allocator, struct aws_array_list *arguments) { + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count != 2) { + printf("invalid get-named-shadow options:\n"); + printf(" get-named-shadow \n\n"); + return; + } + + struct aws_byte_cursor thing_name_cursor; + AWS_ZERO_STRUCT(thing_name_cursor); + aws_array_list_get_at(arguments, &thing_name_cursor, 1); + + struct aws_byte_cursor shadow_name_cursor; + AWS_ZERO_STRUCT(shadow_name_cursor); + aws_array_list_get_at(arguments, &shadow_name_cursor, 2); + + char subscription_topic_filter[128]; + snprintf( + subscription_topic_filter, + AWS_ARRAY_SIZE(subscription_topic_filter), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/get/+", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + struct aws_byte_cursor subscription_topic_filter_cursor = aws_byte_cursor_from_c_str(subscription_topic_filter); + + char accepted_path[128]; + snprintf( + accepted_path, + AWS_ARRAY_SIZE(accepted_path), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/get/accepted", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + char rejected_path[128]; + snprintf( + rejected_path, + AWS_ARRAY_SIZE(rejected_path), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/get/rejected", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + struct aws_byte_cursor correlation_token_path = aws_byte_cursor_from_c_str("clientToken"); + struct aws_mqtt_request_operation_response_path response_paths[] = { + { + .topic = aws_byte_cursor_from_c_str(accepted_path), + .correlation_token_json_path = correlation_token_path, + }, + { + .topic = aws_byte_cursor_from_c_str(rejected_path), + .correlation_token_json_path = correlation_token_path, + }, + }; + + char publish_topic[128]; + snprintf( + publish_topic, + AWS_ARRAY_SIZE(publish_topic), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/get", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + char correlation_token[128]; + s_write_correlation_token_string(aws_byte_cursor_from_array(correlation_token, AWS_ARRAY_SIZE(correlation_token))); + struct aws_string *correlation_token_string = aws_string_new_from_c_str(allocator, correlation_token); + + char request[256]; + snprintf(request, AWS_ARRAY_SIZE(request), "{\"clientToken\":\"%s\"}", correlation_token); + + struct aws_mqtt_request_operation_options get_options = { + .subscription_topic_filters = &subscription_topic_filter_cursor, + .subscription_topic_filter_count = 1, + .response_paths = response_paths, + .response_path_count = 2, + .publish_topic = aws_byte_cursor_from_c_str(publish_topic), + .serialized_request = aws_byte_cursor_from_c_str(request), + .correlation_token = aws_byte_cursor_from_c_str(correlation_token), + .completion_callback = s_on_get_shadow_complete, + .user_data = correlation_token_string, + }; + + printf( + "Submitting GetNamedShadow request for shadow '" PRInSTR "' of thing '" PRInSTR + "' using correlation token %s...\n", + AWS_BYTE_CURSOR_PRI(shadow_name_cursor), + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + correlation_token); + + if (aws_mqtt_request_response_client_submit_request(context->rr_client, &get_options) == AWS_OP_ERR) { + int error_code = aws_last_error(); + printf("GetNamedShadow synchronous failure: %d(%s)\n", error_code, aws_error_debug_str(error_code)); + aws_string_destroy(correlation_token_string); + } + + printf("\n"); +} + +static void s_on_update_shadow_complete( + const struct aws_byte_cursor *response_topic, + const struct aws_byte_cursor *payload, + int error_code, + void *user_data) { + + struct aws_string *correlation_token = user_data; + + if (payload != NULL) { + printf( + "UpdateNamedShadow request '%s' response received on topic '" PRInSTR "' with body:\n " PRInSTR "\n\n", + correlation_token->bytes, + AWS_BYTE_CURSOR_PRI(*response_topic), + AWS_BYTE_CURSOR_PRI(*payload)); + } else { + printf( + "UpdateNamedShadow request '%s' failed with error code %d(%s)\n\n", + correlation_token->bytes, + error_code, + aws_error_debug_str(error_code)); + } + + aws_string_destroy(correlation_token); +} + +static void s_handle_update( + struct app_ctx *context, + struct aws_allocator *allocator, + struct aws_array_list *arguments, + struct aws_byte_cursor desired_state, + struct aws_byte_cursor correlation_token) { + + struct aws_byte_cursor thing_name_cursor; + AWS_ZERO_STRUCT(thing_name_cursor); + aws_array_list_get_at(arguments, &thing_name_cursor, 1); + + struct aws_byte_cursor shadow_name_cursor; + AWS_ZERO_STRUCT(shadow_name_cursor); + aws_array_list_get_at(arguments, &shadow_name_cursor, 2); + + char subscription_topic_filter_accepted[128]; + snprintf( + subscription_topic_filter_accepted, + AWS_ARRAY_SIZE(subscription_topic_filter_accepted), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/update/accepted", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + struct aws_byte_cursor subscription_topic_filter_accepted_cursor = + aws_byte_cursor_from_c_str(subscription_topic_filter_accepted); + + char subscription_topic_filter_rejected[128]; + snprintf( + subscription_topic_filter_rejected, + AWS_ARRAY_SIZE(subscription_topic_filter_rejected), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/update/rejected", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + struct aws_byte_cursor subscription_topic_filter_rejected_cursor = + aws_byte_cursor_from_c_str(subscription_topic_filter_rejected); + + struct aws_byte_cursor subscription_topic_filters[] = { + subscription_topic_filter_accepted_cursor, + subscription_topic_filter_rejected_cursor, + }; + + char accepted_path[128]; + snprintf( + accepted_path, + AWS_ARRAY_SIZE(accepted_path), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/update/accepted", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + char rejected_path[128]; + snprintf( + rejected_path, + AWS_ARRAY_SIZE(rejected_path), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/update/rejected", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + struct aws_byte_cursor correlation_token_path = aws_byte_cursor_from_c_str("clientToken"); + struct aws_mqtt_request_operation_response_path response_paths[] = { + { + .topic = aws_byte_cursor_from_c_str(accepted_path), + .correlation_token_json_path = correlation_token_path, + }, + { + .topic = aws_byte_cursor_from_c_str(rejected_path), + .correlation_token_json_path = correlation_token_path, + }, + }; + + char publish_topic[128]; + snprintf( + publish_topic, + AWS_ARRAY_SIZE(publish_topic), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/update", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + struct aws_string *correlation_token_string = aws_string_new_from_cursor(allocator, &correlation_token); + + struct aws_mqtt_request_operation_options get_options = { + .subscription_topic_filters = subscription_topic_filters, + .subscription_topic_filter_count = 2, + .response_paths = response_paths, + .response_path_count = 2, + .publish_topic = aws_byte_cursor_from_c_str(publish_topic), + .serialized_request = desired_state, + .correlation_token = correlation_token, + .completion_callback = s_on_update_shadow_complete, + .user_data = correlation_token_string, + }; + + printf( + "Submitting UpdateNamedShadow request for shadow '" PRInSTR "' of thing '" PRInSTR + "' using correlation token '" PRInSTR "'...\n", + AWS_BYTE_CURSOR_PRI(shadow_name_cursor), + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(correlation_token)); + + if (aws_mqtt_request_response_client_submit_request(context->rr_client, &get_options) == AWS_OP_ERR) { + int error_code = aws_last_error(); + printf("UpdateNamedShadow synchronous failure: %d(%s)\n", error_code, aws_error_debug_str(error_code)); + aws_string_destroy(correlation_token_string); + } + + printf("\n"); +} + +static void s_handle_update_desired( + struct app_ctx *context, + struct aws_allocator *allocator, + struct aws_array_list *arguments, + struct aws_byte_cursor line_cursor) { + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count < 3) { + printf("invalid update-named-shadow-desired options:\n"); + printf(" update-named-shadow-desired \n\n"); + return; + } + + struct aws_byte_cursor desired_state_cursor; + aws_array_list_get_at(arguments, &desired_state_cursor, 3); + desired_state_cursor.len = (size_t)(line_cursor.ptr + line_cursor.len - desired_state_cursor.ptr); + + char correlation_token[128]; + s_write_correlation_token_string(aws_byte_cursor_from_array(correlation_token, AWS_ARRAY_SIZE(correlation_token))); + + char request[256]; + snprintf( + request, + AWS_ARRAY_SIZE(request), + "{\"clientToken\":\"%s\",\"state\":{\"desired\":" PRInSTR "}}", + correlation_token, + AWS_BYTE_CURSOR_PRI(desired_state_cursor)); + + s_handle_update( + context, + allocator, + arguments, + aws_byte_cursor_from_c_str(request), + aws_byte_cursor_from_c_str(correlation_token)); +} + +static void s_handle_update_reported( + struct app_ctx *context, + struct aws_allocator *allocator, + struct aws_array_list *arguments, + struct aws_byte_cursor line_cursor) { + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count < 3) { + printf("invalid update-named-shadow-reported options:\n"); + printf(" update-named-shadow-reported \n\n"); + return; + } + + struct aws_byte_cursor reported_state_cursor; + aws_array_list_get_at(arguments, &reported_state_cursor, 3); + reported_state_cursor.len = (size_t)(line_cursor.ptr + line_cursor.len - reported_state_cursor.ptr); + + char correlation_token[128]; + s_write_correlation_token_string(aws_byte_cursor_from_array(correlation_token, AWS_ARRAY_SIZE(correlation_token))); + + char request[256]; + snprintf( + request, + AWS_ARRAY_SIZE(request), + "{\"clientToken\":\"%s\",\"state\":{\"reported\":" PRInSTR "}}", + correlation_token, + AWS_BYTE_CURSOR_PRI(reported_state_cursor)); + + s_handle_update( + context, + allocator, + arguments, + aws_byte_cursor_from_c_str(request), + aws_byte_cursor_from_c_str(correlation_token)); +} + +static void s_on_delete_shadow_complete( + const struct aws_byte_cursor *response_topic, + const struct aws_byte_cursor *payload, + int error_code, + void *user_data) { + + struct aws_string *correlation_token = user_data; + + if (payload != NULL) { + printf( + "DeleteNamedShadow request '%s' response received on topic '" PRInSTR "' with body:\n " PRInSTR "\n\n", + correlation_token->bytes, + AWS_BYTE_CURSOR_PRI(*response_topic), + AWS_BYTE_CURSOR_PRI(*payload)); + } else { + printf( + "DeleteNamedShadow request '%s' failed with error code %d(%s)\n\n", + correlation_token->bytes, + error_code, + aws_error_debug_str(error_code)); + } + + aws_string_destroy(correlation_token); +} + +static void s_handle_delete( + struct app_ctx *context, + struct aws_allocator *allocator, + struct aws_array_list *arguments) { + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count != 2) { + printf("invalid delete-named-shadow options:\n"); + printf(" delete-named-shadow \n\n"); + return; + } + + struct aws_byte_cursor thing_name_cursor; + AWS_ZERO_STRUCT(thing_name_cursor); + aws_array_list_get_at(arguments, &thing_name_cursor, 1); + + struct aws_byte_cursor shadow_name_cursor; + AWS_ZERO_STRUCT(shadow_name_cursor); + aws_array_list_get_at(arguments, &shadow_name_cursor, 2); + + char subscription_topic_filter[128]; + snprintf( + subscription_topic_filter, + AWS_ARRAY_SIZE(subscription_topic_filter), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/delete/+", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + struct aws_byte_cursor subscription_topic_filter_cursor = aws_byte_cursor_from_c_str(subscription_topic_filter); + + char accepted_path[128]; + snprintf( + accepted_path, + AWS_ARRAY_SIZE(accepted_path), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/delete/accepted", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + char rejected_path[128]; + snprintf( + rejected_path, + AWS_ARRAY_SIZE(rejected_path), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/delete/rejected", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + struct aws_byte_cursor correlation_token_path = aws_byte_cursor_from_c_str("clientToken"); + struct aws_mqtt_request_operation_response_path response_paths[] = { + { + .topic = aws_byte_cursor_from_c_str(accepted_path), + .correlation_token_json_path = correlation_token_path, + }, + { + .topic = aws_byte_cursor_from_c_str(rejected_path), + .correlation_token_json_path = correlation_token_path, + }, + }; + + char publish_topic[128]; + snprintf( + publish_topic, + AWS_ARRAY_SIZE(publish_topic), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/delete", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + char correlation_token[128]; + struct aws_byte_buf correlation_token_buf = + aws_byte_buf_from_empty_array(correlation_token, AWS_ARRAY_SIZE(correlation_token)); + + struct aws_uuid uuid; + aws_uuid_init(&uuid); + aws_uuid_to_str(&uuid, &correlation_token_buf); + + char request[256]; + snprintf(request, AWS_ARRAY_SIZE(request), "{\"clientToken\":\"%s\"}", correlation_token); + struct aws_string *correlation_token_string = aws_string_new_from_c_str(allocator, correlation_token); + + struct aws_mqtt_request_operation_options get_options = { + .subscription_topic_filters = &subscription_topic_filter_cursor, + .subscription_topic_filter_count = 1, + .response_paths = response_paths, + .response_path_count = 2, + .publish_topic = aws_byte_cursor_from_c_str(publish_topic), + .serialized_request = aws_byte_cursor_from_c_str(request), + .correlation_token = aws_byte_cursor_from_c_str(correlation_token), + .completion_callback = s_on_delete_shadow_complete, + .user_data = correlation_token_string, + }; + + printf( + "Submitting DeleteNamedShadow request for shadow '" PRInSTR "' of thing '" PRInSTR + "' using correlation token %s...\n", + AWS_BYTE_CURSOR_PRI(shadow_name_cursor), + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + correlation_token); + + if (aws_mqtt_request_response_client_submit_request(context->rr_client, &get_options) == AWS_OP_ERR) { + int error_code = aws_last_error(); + printf("DeleteNamedShadow synchronous failure: %d(%s)\n", error_code, aws_error_debug_str(error_code)); + aws_string_destroy(correlation_token_string); + } + + printf("\n"); +} + +static int s_print_streaming_operation(void *context, struct aws_hash_element *elem) { + (void)context; + struct aws_shadow_streaming_operation *operation = elem->value; + + struct aws_byte_cursor thing_cursor = aws_byte_cursor_from_buf(&operation->thing); + struct aws_byte_cursor shadow_cursor = aws_byte_cursor_from_buf(&operation->shadow); + struct aws_byte_cursor topic_filter_cursor = aws_byte_cursor_from_buf(&operation->topic_filter); + + printf( + " %" PRIu64 " Thing:'" PRInSTR "', Shadow: '" PRInSTR "', TopicFilter:'" PRInSTR "'\n", + operation->id, + AWS_BYTE_CURSOR_PRI(thing_cursor), + AWS_BYTE_CURSOR_PRI(shadow_cursor), + AWS_BYTE_CURSOR_PRI(topic_filter_cursor)); + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +static void s_handle_list_streams( + struct app_ctx *context, + struct aws_allocator *allocator, + struct aws_array_list *arguments) { + (void)allocator; + (void)arguments; + + printf("Open Streams:\n"); + aws_hash_table_foreach(&context->streaming_operations, s_print_streaming_operation, NULL); + printf("\n"); +} + +static const char *s_rr_streaming_subscription_event_type_to_c_str( + enum aws_rr_streaming_subscription_event_type status) { + switch (status) { + case ARRSSET_SUBSCRIPTION_ESTABLISHED: + return "SubscriptionEstablished"; + + case ARRSSET_SUBSCRIPTION_LOST: + return "SubscriptionLost"; + + case ARRSSET_SUBSCRIPTION_HALTED: + return "SubscriptionHalted"; + + default: + return "Unknown"; + } +} + +static void s_stream_subscription_status_fn( + enum aws_rr_streaming_subscription_event_type status, + int error_code, + void *user_data) { + + struct aws_shadow_streaming_operation *operation = user_data; + + struct aws_byte_cursor topic_filter_cursor = aws_byte_cursor_from_buf(&operation->topic_filter); + + printf( + "Streaming operation %" PRIu64 " received subscription status event on topic filter '" PRInSTR "':\n", + operation->id, + AWS_BYTE_CURSOR_PRI(topic_filter_cursor)); + printf( + " Status: %d(%s), ErrorCode: %d(%s)\n\n", + status, + s_rr_streaming_subscription_event_type_to_c_str(status), + error_code, + aws_error_debug_str(error_code)); +} + +static void s_stream_incoming_publish_fn(struct aws_byte_cursor payload, void *user_data) { + struct aws_shadow_streaming_operation *operation = user_data; + + struct aws_byte_cursor thing_cursor = aws_byte_cursor_from_buf(&operation->thing); + struct aws_byte_cursor shadow_cursor = aws_byte_cursor_from_buf(&operation->shadow); + struct aws_byte_cursor topic_filter_cursor = aws_byte_cursor_from_buf(&operation->topic_filter); + + printf( + "Streaming operation %" PRIu64 " ('" PRInSTR "', ' " PRInSTR "') received publish on topic filter '" PRInSTR + "':\n", + operation->id, + AWS_BYTE_CURSOR_PRI(thing_cursor), + AWS_BYTE_CURSOR_PRI(shadow_cursor), + AWS_BYTE_CURSOR_PRI(topic_filter_cursor)); + printf(" " PRInSTR "\n\n", AWS_BYTE_CURSOR_PRI(payload)); +} + +static void s_stream_terminated_fn(void *user_data) { + struct aws_shadow_streaming_operation *operation = user_data; + + printf("Stream %" PRIu64 " terminated\n\n", operation->id); + + s_aws_shadow_streaming_operation_destroy(operation); +} + +static void s_handle_open_delta_stream( + struct app_ctx *context, + struct aws_allocator *allocator, + struct aws_array_list *arguments) { + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count != 2) { + printf("invalid open-named-shadow-delta-stream options:\n"); + printf(" open-named-shadow-delta-stream \n\n"); + return; + } + + struct aws_byte_cursor thing_name_cursor; + AWS_ZERO_STRUCT(thing_name_cursor); + aws_array_list_get_at(arguments, &thing_name_cursor, 1); + + struct aws_byte_cursor shadow_name_cursor; + AWS_ZERO_STRUCT(shadow_name_cursor); + aws_array_list_get_at(arguments, &shadow_name_cursor, 2); + + char subscription_topic_filter[128]; + snprintf( + subscription_topic_filter, + AWS_ARRAY_SIZE(subscription_topic_filter), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/update/delta", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + struct aws_shadow_streaming_operation *operation = s_aws_shadow_streaming_operation_new( + allocator, + context, + thing_name_cursor, + shadow_name_cursor, + aws_byte_cursor_from_c_str(subscription_topic_filter)); + aws_hash_table_put(&context->streaming_operations, &operation->id, operation, NULL); + + struct aws_mqtt_streaming_operation_options open_stream_options = { + .topic_filter = aws_byte_cursor_from_c_str(subscription_topic_filter), + .subscription_status_callback = s_stream_subscription_status_fn, + .incoming_publish_callback = s_stream_incoming_publish_fn, + .terminated_callback = s_stream_terminated_fn, + .user_data = operation, + }; + + printf( + "Opening NamedShadow delta stream with id %" PRIu64 " for shadow '" PRInSTR "' of thing '" PRInSTR "'...\n", + operation->id, + AWS_BYTE_CURSOR_PRI(shadow_name_cursor), + AWS_BYTE_CURSOR_PRI(thing_name_cursor)); + + operation->streaming_operation = + aws_mqtt_request_response_client_create_streaming_operation(context->rr_client, &open_stream_options); + if (operation->streaming_operation == NULL || + aws_mqtt_rr_client_operation_activate(operation->streaming_operation)) { + int error_code = aws_last_error(); + printf( + "NamedShadow delta stream synchronous open failure: %d(%s)\n", error_code, aws_error_debug_str(error_code)); + + aws_hash_table_remove(&context->streaming_operations, &operation->id, NULL, NULL); + } + + printf("\n"); +} + +static void s_handle_open_document_stream( + struct app_ctx *context, + struct aws_allocator *allocator, + struct aws_array_list *arguments) { + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count != 2) { + printf("invalid open-named-shadow-document-stream options:\n"); + printf(" open-named-shadow-document-stream \n\n"); + return; + } + + struct aws_byte_cursor thing_name_cursor; + AWS_ZERO_STRUCT(thing_name_cursor); + aws_array_list_get_at(arguments, &thing_name_cursor, 1); + + struct aws_byte_cursor shadow_name_cursor; + AWS_ZERO_STRUCT(shadow_name_cursor); + aws_array_list_get_at(arguments, &shadow_name_cursor, 2); + + char subscription_topic_filter[128]; + snprintf( + subscription_topic_filter, + AWS_ARRAY_SIZE(subscription_topic_filter), + "$aws/things/" PRInSTR "/shadow/name/" PRInSTR "/update/document", + AWS_BYTE_CURSOR_PRI(thing_name_cursor), + AWS_BYTE_CURSOR_PRI(shadow_name_cursor)); + + struct aws_shadow_streaming_operation *operation = s_aws_shadow_streaming_operation_new( + allocator, + context, + thing_name_cursor, + shadow_name_cursor, + aws_byte_cursor_from_c_str(subscription_topic_filter)); + aws_hash_table_put(&context->streaming_operations, &operation->id, operation, NULL); + + struct aws_mqtt_streaming_operation_options open_stream_options = { + .topic_filter = aws_byte_cursor_from_c_str(subscription_topic_filter), + .subscription_status_callback = s_stream_subscription_status_fn, + .incoming_publish_callback = s_stream_incoming_publish_fn, + .terminated_callback = s_stream_terminated_fn, + .user_data = operation, + }; + + printf( + "Opening NamedShadow document stream with id %" PRIu64 " for shadow '" PRInSTR "' of thing '" PRInSTR "'...\n", + operation->id, + AWS_BYTE_CURSOR_PRI(shadow_name_cursor), + AWS_BYTE_CURSOR_PRI(thing_name_cursor)); + + operation->streaming_operation = + aws_mqtt_request_response_client_create_streaming_operation(context->rr_client, &open_stream_options); + if (operation->streaming_operation == NULL || + aws_mqtt_rr_client_operation_activate(operation->streaming_operation)) { + int error_code = aws_last_error(); + printf( + "NamedShadow document stream synchronous open failure: %d(%s)\n", + error_code, + aws_error_debug_str(error_code)); + + aws_hash_table_remove(&context->streaming_operations, &operation->id, NULL, NULL); + } + + printf("\n"); +} + +static void s_handle_close_stream( + struct app_ctx *context, + struct aws_allocator *allocator, + struct aws_array_list *arguments) { + (void)allocator; + + size_t argument_count = aws_array_list_length(arguments) - 1; + if (argument_count != 1) { + printf("invalid close-stream options:\n"); + printf(" close-stream \n\n"); + return; + } + + struct aws_byte_cursor id_cursor; + AWS_ZERO_STRUCT(id_cursor); + aws_array_list_get_at(arguments, &id_cursor, 1); + + char id_buffer[32]; + snprintf(id_buffer, AWS_ARRAY_SIZE(id_buffer), PRInSTR, AWS_BYTE_CURSOR_PRI(id_cursor)); + uint64_t id = atoi(id_buffer); + + int was_present = 0; + aws_hash_table_remove(&context->streaming_operations, &id, NULL, &was_present); + + if (was_present == 0) { + printf("Stream %" PRIu64 " does not exist\n\n", id); + } else { + printf("Closing stream %" PRIu64 "\n\n", id); + } +} + +static bool s_handle_input(struct app_ctx *context, struct aws_allocator *allocator, const char *input_line) { + + struct aws_mqtt5_client *client = context->client; + + struct aws_byte_cursor quit_cursor = aws_byte_cursor_from_c_str("quit"); + struct aws_byte_cursor start_cursor = aws_byte_cursor_from_c_str("start"); + struct aws_byte_cursor stop_cursor = aws_byte_cursor_from_c_str("stop"); + struct aws_byte_cursor get_cursor = aws_byte_cursor_from_c_str("get-named-shadow"); + struct aws_byte_cursor update_reported_cursor = aws_byte_cursor_from_c_str("update-named-shadow-reported"); + struct aws_byte_cursor update_desired_cursor = aws_byte_cursor_from_c_str("update-named-shadow-desired"); + struct aws_byte_cursor delete_cursor = aws_byte_cursor_from_c_str("delete-named-shadow"); + struct aws_byte_cursor list_streams_cursor = aws_byte_cursor_from_c_str("list-streams"); + struct aws_byte_cursor open_delta_stream_cursor = aws_byte_cursor_from_c_str("open-named-shadow-delta-stream"); + struct aws_byte_cursor open_document_stream_cursor = + aws_byte_cursor_from_c_str("open-named-shadow-document-stream"); + struct aws_byte_cursor close_stream_cursor = aws_byte_cursor_from_c_str("close-stream"); + + struct aws_array_list words; + aws_array_list_init_dynamic(&words, allocator, 10, sizeof(struct aws_byte_cursor)); + + struct aws_byte_cursor line_cursor = aws_byte_cursor_from_c_str(input_line); + line_cursor = aws_byte_cursor_trim_pred(&line_cursor, &s_skip_whitespace); + + bool done = false; + + s_split_command_line(line_cursor, &words); + if (aws_array_list_length(&words) == 0) { + printf("Empty command line\n"); + goto done; + } + + struct aws_byte_cursor command_cursor; + AWS_ZERO_STRUCT(command_cursor); + aws_array_list_get_at(&words, &command_cursor, 0); + + if (aws_byte_cursor_eq_ignore_case(&command_cursor, &quit_cursor)) { + printf("Quitting!\n"); + done = true; + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &start_cursor)) { + printf("Starting client!\n"); + aws_mqtt5_client_start(client); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &stop_cursor)) { + printf("Stopping client!\n"); + aws_mqtt5_client_stop(client, NULL, NULL); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &get_cursor)) { + s_handle_get(context, allocator, &words); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &update_reported_cursor)) { + s_handle_update_reported(context, allocator, &words, line_cursor); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &update_desired_cursor)) { + s_handle_update_desired(context, allocator, &words, line_cursor); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &delete_cursor)) { + s_handle_delete(context, allocator, &words); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &list_streams_cursor)) { + s_handle_list_streams(context, allocator, &words); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &open_delta_stream_cursor)) { + s_handle_open_delta_stream(context, allocator, &words); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &open_document_stream_cursor)) { + s_handle_open_document_stream(context, allocator, &words); + } else if (aws_byte_cursor_eq_ignore_case(&command_cursor, &close_stream_cursor)) { + s_handle_close_stream(context, allocator, &words); + } else { + printf("Unknown command: " PRInSTR "\n", AWS_BYTE_CURSOR_PRI(command_cursor)); + printf("\nValid commands:\n"); + printf(" get-named-shadow - gets the full state of a named shadow\n"); + printf(" delete-named-shadow - deletes a named shadow\n"); + printf(" update-named-shadow-reported - updates the reported state of a named shadow\n"); + printf(" update-named-shadow-desired - updates the desired state of a named shadow\n"); + printf("\n"); + printf(" list-streams - lists all open shadow streams\n"); + printf(" open-named-shadow-delta-stream - creates and opens a new shadow delta stream\n"); + printf(" open-named-shadow-document-stream - creates and opens a new shadow document stream\n"); + printf(" close-stream - closes a specific shadow stream\n"); + printf("\n"); + printf(" start - starts the mqtt5 client underlying the request-response client\n"); + printf(" stop - starts the mqtt5 client underlying the request-response client\n"); + printf(" quit - quits the program\n"); + printf("\n"); + } + +done: + + aws_array_list_clean_up(&words); + return done; +} + +static void s_on_publish_received(const struct aws_mqtt5_packet_publish_view *publish, void *user_data) { + (void)publish; + (void)user_data; +} + +static void s_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + + switch (event->event_type) { + case AWS_MQTT5_CLET_STOPPED: + printf("Lifecycle event: Stopped!\n"); + break; + + case AWS_MQTT5_CLET_ATTEMPTING_CONNECT: + printf("Lifecycle event: Attempting Connect!\n"); + break; + + case AWS_MQTT5_CLET_CONNECTION_FAILURE: + printf("Lifecycle event: Connection Failure!\n"); + printf(" Error Code: %d(%s)\n", event->error_code, aws_error_debug_str(event->error_code)); + break; + + case AWS_MQTT5_CLET_CONNECTION_SUCCESS: + printf("Lifecycle event: Connection Success!\n"); + break; + + case AWS_MQTT5_CLET_DISCONNECTION: + printf("Lifecycle event: Disconnect!\n"); + printf(" Error Code: %d(%s)\n", event->error_code, aws_error_debug_str(event->error_code)); + break; + } + + fflush(stdout); +} + +AWS_STATIC_STRING_FROM_LITERAL(s_client_id, "HelloWorld"); + +int main(int argc, char **argv) { + struct aws_allocator *allocator = aws_mem_tracer_new(aws_default_allocator(), NULL, AWS_MEMTRACE_STACKS, 15); + + aws_mqtt_library_init(allocator); + + struct app_ctx app_ctx; + AWS_ZERO_STRUCT(app_ctx); + app_ctx.allocator = allocator; + app_ctx.signal = (struct aws_condition_variable)AWS_CONDITION_VARIABLE_INIT; + aws_mutex_init(&app_ctx.lock); + app_ctx.port = 1883; + + aws_hash_table_init( + &app_ctx.streaming_operations, + allocator, + 10, + aws_hash_uint64_t_by_identity, + aws_hash_compare_uint64_t_eq, + NULL, + s_release_streaming_operation); + + s_parse_options(argc, argv, &app_ctx); + if (app_ctx.uri.port) { + app_ctx.port = app_ctx.uri.port; + } + + struct aws_logger logger; + AWS_ZERO_STRUCT(logger); + + struct aws_logger_standard_options options = { + .level = app_ctx.log_level, + }; + + if (app_ctx.log_level) { + if (app_ctx.log_filename) { + options.filename = app_ctx.log_filename; + } else { + options.file = stderr; + } + + if (aws_logger_init_standard(&logger, allocator, &options)) { + fprintf(stderr, "Failed to initialize logger with error %s\n", aws_error_debug_str(aws_last_error())); + exit(1); + } + + aws_logger_set(&logger); + } + + if (!app_ctx.cert || !app_ctx.key) { + fprintf(stderr, "Elastishadow requires mtls connections. You must specify a cert and key to use.\n"); + exit(1); + } + + struct aws_tls_ctx *tls_ctx = NULL; + struct aws_tls_ctx_options tls_ctx_options; + AWS_ZERO_STRUCT(tls_ctx_options); + struct aws_tls_connection_options tls_connection_options; + AWS_ZERO_STRUCT(tls_connection_options); + + if (aws_tls_ctx_options_init_client_mtls_from_path(&tls_ctx_options, allocator, app_ctx.cert, app_ctx.key)) { + fprintf( + stderr, + "Failed to load %s and %s with error %s.", + app_ctx.cert, + app_ctx.key, + aws_error_debug_str(aws_last_error())); + exit(1); + } + + if (aws_tls_ctx_options_set_alpn_list(&tls_ctx_options, "x-amzn-mqtt-ca")) { + fprintf(stderr, "Failed to set alpn list with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + tls_ctx = aws_tls_client_ctx_new(allocator, &tls_ctx_options); + + if (!tls_ctx) { + fprintf(stderr, "Failed to initialize TLS context with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + aws_tls_connection_options_init_from_ctx(&tls_connection_options, tls_ctx); + if (aws_tls_connection_options_set_server_name(&tls_connection_options, allocator, &app_ctx.uri.host_name)) { + fprintf(stderr, "Failed to set servername with error %s.", aws_error_debug_str(aws_last_error())); + exit(1); + } + + struct aws_event_loop_group *el_group = aws_event_loop_group_new_default(allocator, 2, NULL); + + struct aws_host_resolver_default_options resolver_options = { + .el_group = el_group, + .max_entries = 8, + }; + + struct aws_host_resolver *resolver = aws_host_resolver_new_default(allocator, &resolver_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = el_group, + .host_resolver = resolver, + }; + + struct aws_client_bootstrap *bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + struct aws_socket_options socket_options = { + .type = AWS_SOCKET_STREAM, + .connect_timeout_ms = (uint32_t)10000, + .keep_alive_timeout_sec = 0, + .keepalive = false, + .keep_alive_interval_sec = 0, + }; + + struct aws_mqtt5_packet_connect_view connect_options = { + .keep_alive_interval_seconds = 30, + .client_id = aws_byte_cursor_from_string(s_client_id), + }; + + struct aws_mqtt5_client_options client_options = { + .host_name = app_ctx.uri.host_name, + .port = app_ctx.port, + .bootstrap = bootstrap, + .socket_options = &socket_options, + .tls_options = &tls_connection_options, + .connect_options = &connect_options, + .session_behavior = AWS_MQTT5_CSBT_CLEAN, + .lifecycle_event_handler = s_lifecycle_event_callback, + .lifecycle_event_handler_user_data = NULL, + .retry_jitter_mode = AWS_EXPONENTIAL_BACKOFF_JITTER_NONE, + .min_reconnect_delay_ms = 1000, + .max_reconnect_delay_ms = 120000, + .min_connected_time_to_reset_reconnect_delay_ms = 30000, + .ping_timeout_ms = 10000, + .publish_received_handler = s_on_publish_received, + }; + + app_ctx.client = aws_mqtt5_client_new(allocator, &client_options); + + struct aws_mqtt_request_response_client_options rr_client_options = { + .max_request_response_subscriptions = 10, + .max_streaming_subscriptions = 5, + .operation_timeout_seconds = 60, + }; + + app_ctx.rr_client = + aws_mqtt_request_response_client_new_from_mqtt5_client(allocator, app_ctx.client, &rr_client_options); + + bool done = false; + while (!done) { + printf("Enter command:\n"); + + char input_buffer[4096]; +#ifdef WIN32 + char *line = gets_s(input_buffer, AWS_ARRAY_SIZE(input_buffer)); +#else + char *line = fgets(input_buffer, AWS_ARRAY_SIZE(input_buffer), stdin); +#endif + done = s_handle_input(&app_ctx, allocator, line); + } + + aws_hash_table_clean_up(&app_ctx.streaming_operations); + + aws_mqtt_request_response_client_release(app_ctx.rr_client); + aws_mqtt5_client_release(app_ctx.client); + + aws_client_bootstrap_release(bootstrap); + aws_host_resolver_release(resolver); + aws_event_loop_group_release(el_group); + + if (tls_ctx) { + aws_tls_connection_options_clean_up(&tls_connection_options); + aws_tls_ctx_release(tls_ctx); + aws_tls_ctx_options_clean_up(&tls_ctx_options); + } + + aws_thread_join_all_managed(); + + const size_t outstanding_bytes = aws_mem_tracer_bytes(allocator); + printf("Summary:\n"); + printf(" Outstanding bytes: %zu\n\n", outstanding_bytes); + + if (app_ctx.log_level) { + aws_logger_set(NULL); + aws_logger_clean_up(&logger); + } + + aws_uri_clean_up(&app_ctx.uri); + + aws_mqtt_library_clean_up(); + + const size_t leaked_bytes = aws_mem_tracer_bytes(allocator); + if (leaked_bytes) { + struct aws_logger memory_logger; + AWS_ZERO_STRUCT(memory_logger); + + aws_logger_init_noalloc(&memory_logger, aws_default_allocator(), &options); + aws_logger_set(&memory_logger); + + aws_mqtt_library_init(aws_default_allocator()); + + printf("Writing memory leaks to log.\n"); + aws_mem_tracer_dump(allocator); + + aws_logger_set(NULL); + aws_logger_clean_up(&memory_logger); + + aws_mqtt_library_clean_up(); + } else { + printf("Finished, with no memory leaks\n"); + } + + aws_mem_tracer_destroy(allocator); + + return 0; +} diff --git a/include/aws/mqtt/mqtt.h b/include/aws/mqtt/mqtt.h index 38510c8c..8d632fd4 100644 --- a/include/aws/mqtt/mqtt.h +++ b/include/aws/mqtt/mqtt.h @@ -82,6 +82,14 @@ enum aws_mqtt_error { AWS_ERROR_MQTT_CONNECTION_RESUBSCRIBE_NO_TOPICS, AWS_ERROR_MQTT_CONNECTION_SUBSCRIBE_FAILURE, AWS_ERROR_MQTT_ACK_REASON_CODE_FAILURE, + AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE, + AWS_ERROR_MQTT_REQUEST_RESPONSE_CLIENT_SHUT_DOWN, + AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, + AWS_ERROR_MQTT_REQUEST_RESPONSE_NO_SUBSCRIPTION_CAPACITY, + AWS_ERROR_MQTT_REQUEST_RESPONSE_SUBSCRIBE_FAILURE, + AWS_ERROR_MQTT_REQUEST_RESPONSE_INTERNAL_ERROR, + AWS_ERROR_MQTT_REQUEST_RESPONSE_PUBLISH_FAILURE, + AWS_ERROR_MQTT_REUQEST_RESPONSE_STREAM_ALREADY_ACTIVATED, AWS_ERROR_END_MQTT_RANGE = AWS_ERROR_ENUM_END_RANGE(AWS_C_MQTT_PACKAGE_ID), }; @@ -94,6 +102,7 @@ enum aws_mqtt_log_subject { AWS_LS_MQTT5_CLIENT, AWS_LS_MQTT5_CANARY, AWS_LS_MQTT5_TO_MQTT3_ADAPTER, + AWS_LS_MQTT_REQUEST_RESPONSE, }; /** Function called on cleanup of a userdata. */ diff --git a/include/aws/mqtt/private/client_impl.h b/include/aws/mqtt/private/client_impl.h index 1d0dd67a..ab9822c3 100644 --- a/include/aws/mqtt/private/client_impl.h +++ b/include/aws/mqtt/private/client_impl.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -153,6 +154,25 @@ struct aws_mqtt_reconnect_task { struct aws_allocator *allocator; }; +struct request_timeout_wrapper; + +/* used for timeout task */ +struct request_timeout_task_arg { + uint16_t packet_id; + struct aws_mqtt_client_connection_311_impl *connection; + struct request_timeout_wrapper *task_arg_wrapper; +}; + +/* + * We want the timeout task to be able to destroy the forward reference from the operation's task arg structure + * to the timeout task. But the operation task arg structures don't have any data structure in common. So to allow + * the timeout to refer back to a zero-able forward pointer, we wrap a pointer to the timeout task and embed it + * in every operation's task arg that needs to create a timeout. + */ +struct request_timeout_wrapper { + struct request_timeout_task_arg *timeout_task_arg; +}; + /* The lifetime of this struct is from subscribe -> suback */ struct subscribe_task_arg { @@ -172,6 +192,9 @@ struct subscribe_task_arg { aws_mqtt_suback_fn *single; } on_suback; void *on_suback_ud; + + struct request_timeout_wrapper timeout_wrapper; + uint64_t timeout_duration_in_ns; }; /* The lifetime of this struct is the same as the lifetime of the subscription */ @@ -255,6 +278,9 @@ struct aws_mqtt_client_connection_311_impl { aws_mqtt_on_operation_statistics_fn *on_any_operation_statistics; void *on_any_operation_statistics_ud; + /* listener callbacks */ + struct aws_mqtt311_callback_set_manager callback_manager; + /* Connection tasks. */ struct aws_mqtt_reconnect_task *reconnect_task; struct aws_channel_task ping_task; @@ -425,4 +451,32 @@ void aws_mqtt_connection_statistics_change_operation_statistic_state( AWS_MQTT_API const struct aws_mqtt_client_connection_packet_handlers *aws_mqtt311_get_default_packet_handlers(void); +AWS_MQTT_API uint16_t aws_mqtt_client_connection_311_unsubscribe( + struct aws_mqtt_client_connection_311_impl *connection, + const struct aws_byte_cursor *topic_filter, + aws_mqtt_op_complete_fn *on_unsuback, + void *on_unsuback_ud, + uint64_t timeout_ns); + +AWS_MQTT_API uint16_t aws_mqtt_client_connection_311_subscribe( + struct aws_mqtt_client_connection_311_impl *connection, + const struct aws_byte_cursor *topic_filter, + enum aws_mqtt_qos qos, + aws_mqtt_client_publish_received_fn *on_publish, + void *on_publish_ud, + aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, + aws_mqtt_suback_fn *on_suback, + void *on_suback_ud, + uint64_t timeout_ns); + +AWS_MQTT_API uint16_t aws_mqtt_client_connection_311_publish( + struct aws_mqtt_client_connection_311_impl *connection, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload, + aws_mqtt_op_complete_fn *on_complete, + void *userdata, + uint64_t timeout_ns); + #endif /* AWS_MQTT_PRIVATE_CLIENT_IMPL_H */ diff --git a/include/aws/mqtt/private/client_impl_shared.h b/include/aws/mqtt/private/client_impl_shared.h index d244cfe7..248b4658 100644 --- a/include/aws/mqtt/private/client_impl_shared.h +++ b/include/aws/mqtt/private/client_impl_shared.h @@ -10,6 +10,20 @@ struct aws_mqtt_client_connection; +/* + * Internal enum that indicates what type of struct the underlying impl pointer actually is. We use this + * to safely interact with private APIs on the implementation or extract the adapted 5 client directly, as + * necessary. + */ +enum aws_mqtt311_impl_type { + + /* 311 connection impl can be cast to `struct aws_mqtt_client_connection_311_impl` */ + AWS_MQTT311_IT_311_CONNECTION, + + /* 311 connection impl can be cast to `struct aws_mqtt_client_connection_5_impl`*/ + AWS_MQTT311_IT_5_ADAPTER, +}; + struct aws_mqtt_client_connection_vtable { struct aws_mqtt_client_connection *(*acquire_fn)(void *impl); @@ -107,6 +121,10 @@ struct aws_mqtt_client_connection_vtable { void *userdata); int (*get_stats_fn)(void *impl, struct aws_mqtt_connection_operation_statistics *stats); + + enum aws_mqtt311_impl_type (*get_impl_type)(const void *impl); + + struct aws_event_loop *(*get_event_loop)(const void *impl); }; struct aws_mqtt_client_connection { @@ -114,10 +132,16 @@ struct aws_mqtt_client_connection { void *impl; }; +AWS_MQTT_API enum aws_mqtt311_impl_type aws_mqtt_client_connection_get_impl_type( + const struct aws_mqtt_client_connection *connection); + AWS_MQTT_API uint64_t aws_mqtt_hash_uint16_t(const void *item); AWS_MQTT_API bool aws_mqtt_compare_uint16_t_eq(const void *a, const void *b); AWS_MQTT_API bool aws_mqtt_byte_cursor_hash_equality(const void *a, const void *b); +AWS_MQTT_API struct aws_event_loop *aws_mqtt_client_connection_get_event_loop( + const struct aws_mqtt_client_connection *connection); + #endif /* AWS_MQTT_PRIVATE_CLIENT_IMPL_SHARED_H */ diff --git a/include/aws/mqtt/private/mqtt311_listener.h b/include/aws/mqtt/private/mqtt311_listener.h new file mode 100644 index 00000000..c66e4f24 --- /dev/null +++ b/include/aws/mqtt/private/mqtt311_listener.h @@ -0,0 +1,204 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_MQTT_MQTT311_LISTENER_H +#define AWS_MQTT_MQTT311_LISTENER_H + +#include + +#include +#include + +AWS_PUSH_SANE_WARNING_LEVEL + +/** + * Callback signature for when an mqtt311 listener has completely destroyed itself. + */ +typedef void(aws_mqtt311_listener_termination_completion_fn)(void *complete_ctx); + +/** + * A record that tracks MQTT311 client connection callbacks which can be dynamically injected via a listener. + * + * All the callbacks that are supported here are invoked only on the 311 connection's event loop. With the + * add/remove callback set also on the event loop, everything is correctly serialized without data races. + * + * If binding additional callbacks, they must only be invoked from the connection's event loop. + * + * We only listen to connection-success because the only connection-level event we care about is a failure + * to rejoin a session (which invalidates all subscriptions that were considered valid) + */ +struct aws_mqtt311_callback_set { + + /* Called from s_packet_handler_publish which is event-loop invoked */ + aws_mqtt_client_publish_received_fn *publish_received_handler; + + /* Called from s_packet_handler_connack which is event-loop invoked */ + aws_mqtt_client_on_connection_success_fn *connection_success_handler; + + /* Called from s_mqtt_client_shutdown which is event-loop invoked */ + aws_mqtt_client_on_connection_interrupted_fn *connection_interrupted_handler; + + /* Called from s_mqtt_client_shutdown which is event-loop invoked */ + aws_mqtt_client_on_disconnect_fn *disconnect_handler; + + void *user_data; +}; + +/** + * An internal type for managing chains of callbacks attached to an mqtt311 client connection. Supports chains for + * lifecycle events and incoming publish packet handling. + * + * Assumed to be owned and used only by an MQTT311 client connection. + */ +struct aws_mqtt311_callback_set_manager { + struct aws_allocator *allocator; + + struct aws_mqtt_client_connection *connection; + + struct aws_linked_list callback_set_entries; + + uint64_t next_callback_set_entry_id; +}; + +/** + * Configuration options for MQTT311 listener objects. + */ +struct aws_mqtt311_listener_config { + + /** + * MQTT311 client connection to listen to events on + */ + struct aws_mqtt_client_connection *connection; + + /** + * Callbacks to invoke when events occur on the MQTT311 client connection + */ + struct aws_mqtt311_callback_set listener_callbacks; + + /** + * Listener destruction is asynchronous and thus requires a termination callback and associated user data + * to notify the user that the listener has been fully destroyed and no further events will be received. + */ + aws_mqtt311_listener_termination_completion_fn *termination_callback; + void *termination_callback_user_data; +}; + +AWS_EXTERN_C_BEGIN + +/** + * Creates a new MQTT311 listener object. For as long as the listener lives, incoming publishes and lifecycle events + * will be forwarded to the callbacks configured on the listener. + * + * @param allocator allocator to use + * @param config listener configuration + * @return a new aws_mqtt311_listener object + */ +AWS_MQTT_API struct aws_mqtt311_listener *aws_mqtt311_listener_new( + struct aws_allocator *allocator, + struct aws_mqtt311_listener_config *config); + +/** + * Adds a reference to an mqtt311 listener. + * + * @param listener listener to add a reference to + * @return the listener object + */ +AWS_MQTT_API struct aws_mqtt311_listener *aws_mqtt311_listener_acquire(struct aws_mqtt311_listener *listener); + +/** + * Removes a reference to an mqtt311 listener. When the reference count drops to zero, the listener's asynchronous + * destruction will be started. + * + * @param listener listener to remove a reference from + * @return NULL + */ +AWS_MQTT_API struct aws_mqtt311_listener *aws_mqtt311_listener_release(struct aws_mqtt311_listener *listener); + +/** + * Initializes a callback set manager + */ +AWS_MQTT_API +void aws_mqtt311_callback_set_manager_init( + struct aws_mqtt311_callback_set_manager *manager, + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *connection); + +/** + * Cleans up a callback set manager. + * + * aws_mqtt311_callback_set_manager_init must have been previously called or this will crash. + */ +AWS_MQTT_API +void aws_mqtt311_callback_set_manager_clean_up(struct aws_mqtt311_callback_set_manager *manager); + +/** + * Adds a callback set to the front of the handler chain. Returns an integer id that can be used to selectively + * remove the callback set from the manager. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +uint64_t aws_mqtt311_callback_set_manager_push_front( + struct aws_mqtt311_callback_set_manager *manager, + struct aws_mqtt311_callback_set *callback_set); + +/** + * Removes a callback set from the handler chain. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +void aws_mqtt311_callback_set_manager_remove( + struct aws_mqtt311_callback_set_manager *manager, + uint64_t callback_set_id); + +/** + * Walks the incoming publish handler chain for an MQTT311 connection, invoking each in sequence. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +void aws_mqtt311_callback_set_manager_on_publish_received( + struct aws_mqtt311_callback_set_manager *manager, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain); + +/** + * Invokes a connection success event on each listener in the manager's collection of callback sets. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +void aws_mqtt311_callback_set_manager_on_connection_success( + struct aws_mqtt311_callback_set_manager *manager, + enum aws_mqtt_connect_return_code return_code, + bool rejoined_session); + +/** + * Invokes a connection interrupted event on each listener in the manager's collection of callback sets. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +void aws_mqtt311_callback_set_manager_on_connection_interrupted( + struct aws_mqtt311_callback_set_manager *manager, + int error_code); + +/** + * Invokes a disconnection event on each listener in the manager's collection of callback sets. + * + * May only be called on the client's event loop thread. + */ +AWS_MQTT_API +void aws_mqtt311_callback_set_manager_on_disconnect(struct aws_mqtt311_callback_set_manager *manager); + +AWS_EXTERN_C_END + +AWS_POP_SANE_WARNING_LEVEL + +#endif /* AWS_MQTT_MQTT311_LISTENER_H */ diff --git a/include/aws/mqtt/private/request-response/protocol_adapter.h b/include/aws/mqtt/private/request-response/protocol_adapter.h new file mode 100644 index 00000000..7d71e805 --- /dev/null +++ b/include/aws/mqtt/private/request-response/protocol_adapter.h @@ -0,0 +1,220 @@ +#ifndef AWS_MQTT_PRIVATE_REQUEST_RESPONSE_PROTOCOL_ADAPTER_H +#define AWS_MQTT_PRIVATE_REQUEST_RESPONSE_PROTOCOL_ADAPTER_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include + +#include + +struct aws_allocator; +struct aws_event_loop; +struct aws_mqtt_client_connection; +struct aws_mqtt5_client; + +/* + * The request-response protocol adapter is a translation layer that sits between the request-response native client + * implementation and a protocol client capable of subscribing, unsubscribing, and publishing MQTT messages. + * Valid protocol clients include the CRT MQTT5 client, the CRT MQTT311 client, and an eventstream RPC connection + * that belongs to a Greengrass IPC client. Each of these protocol clients has a different (or even implicit) + * contract for carrying out pub-sub operations. The protocol adapter abstracts these details with a simple, + * minimal interface based on the requirements identified in the request-response design documents. + */ + +/* + * Minimal MQTT subscribe options + */ +struct aws_protocol_adapter_subscribe_options { + struct aws_byte_cursor topic_filter; + uint32_t ack_timeout_seconds; +}; + +/* + * Minimal MQTT unsubscribe options + */ +struct aws_protocol_adapter_unsubscribe_options { + struct aws_byte_cursor topic_filter; + uint32_t ack_timeout_seconds; +}; + +/* + * Minimal MQTT publish options + */ +struct aws_protocol_adapter_publish_options { + struct aws_byte_cursor topic; + struct aws_byte_cursor payload; + uint32_t ack_timeout_seconds; + + /* + * Invoked on success/failure of the publish itself. Our implementations use QoS1 which means that success + * will be on puback receipt. + */ + void (*completion_callback_fn)(int, void *); + + /* + * User data to pass in when invoking the completion callback + */ + void *user_data; +}; + +/* + * Describes the type of subscription event (relative to a topic filter) + */ +enum aws_protocol_adapter_subscription_event_type { + AWS_PASET_SUBSCRIBE, + AWS_PASET_UNSUBSCRIBE, +}; + +/* + * An event emitted by the protocol adapter when a subscribe or unsubscribe is completed by the adapted protocol + * client. + */ +struct aws_protocol_adapter_subscription_event { + struct aws_byte_cursor topic_filter; + enum aws_protocol_adapter_subscription_event_type event_type; + int error_code; + bool retryable; +}; + +/* + * An event emitted by the protocol adapter whenever a publish is received by the protocol client. This will + * potentially include messages that are completely unrelated to MQTT request-response. The topic is the first + * thing that should be checked for relevance. + */ +struct aws_protocol_adapter_incoming_publish_event { + struct aws_byte_cursor topic; + struct aws_byte_cursor payload; +}; + +enum aws_protocol_adapter_connection_event_type { + AWS_PACET_CONNECTED, + AWS_PACET_DISCONNECTED, +}; + +/* + * An event emitted by the protocol adapter whenever the protocol client's connection status changes + */ +struct aws_protocol_adapter_connection_event { + enum aws_protocol_adapter_connection_event_type event_type; + bool joined_session; +}; + +typedef void(aws_protocol_adapter_subscription_event_fn)( + const struct aws_protocol_adapter_subscription_event *event, + void *user_data); + +typedef void(aws_protocol_adapter_incoming_publish_fn)( + const struct aws_protocol_adapter_incoming_publish_event *publish, + void *user_data); + +typedef void(aws_protocol_adapter_terminate_callback_fn)(void *user_data); + +typedef void(aws_protocol_adapter_connection_event_fn)( + const struct aws_protocol_adapter_connection_event *event, + void *user_data); + +/* + * Set of callbacks invoked by the protocol adapter. These must all be set. + */ +struct aws_mqtt_protocol_adapter_options { + aws_protocol_adapter_subscription_event_fn *subscription_event_callback; + aws_protocol_adapter_incoming_publish_fn *incoming_publish_callback; + aws_protocol_adapter_terminate_callback_fn *terminate_callback; + aws_protocol_adapter_connection_event_fn *connection_event_callback; + + /* + * User data to pass into all singleton protocol adapter callbacks. Likely either the request-response client + * or the subscription manager component of the request-response client. + */ + void *user_data; +}; + +struct aws_mqtt_protocol_adapter_vtable { + + void (*aws_mqtt_protocol_adapter_destroy_fn)(void *); + + int (*aws_mqtt_protocol_adapter_subscribe_fn)(void *, struct aws_protocol_adapter_subscribe_options *); + + int (*aws_mqtt_protocol_adapter_unsubscribe_fn)(void *, struct aws_protocol_adapter_unsubscribe_options *); + + int (*aws_mqtt_protocol_adapter_publish_fn)(void *, struct aws_protocol_adapter_publish_options *); + + bool (*aws_mqtt_protocol_adapter_is_connected_fn)(void *); + + struct aws_event_loop *(*aws_mqtt_protocol_adapter_get_event_loop_fn)(void *); +}; + +struct aws_mqtt_protocol_adapter { + const struct aws_mqtt_protocol_adapter_vtable *vtable; + void *impl; +}; + +AWS_EXTERN_C_BEGIN + +/* + * Creates a new request-response protocol adapter from an MQTT311 client + */ +AWS_MQTT_API struct aws_mqtt_protocol_adapter *aws_mqtt_protocol_adapter_new_from_311( + struct aws_allocator *allocator, + struct aws_mqtt_protocol_adapter_options *options, + struct aws_mqtt_client_connection *connection); + +/* + * Creates a new request-response protocol adapter from an MQTT5 client + */ +AWS_MQTT_API struct aws_mqtt_protocol_adapter *aws_mqtt_protocol_adapter_new_from_5( + struct aws_allocator *allocator, + struct aws_mqtt_protocol_adapter_options *options, + struct aws_mqtt5_client *client); + +/* + * Destroys a request-response protocol adapter. Destruction is an asynchronous process and the caller must + * wait for the termination callback to be invoked before assuming that no further callbacks will be invoked. + */ +AWS_MQTT_API void aws_mqtt_protocol_adapter_destroy(struct aws_mqtt_protocol_adapter *adapter); + +/* + * Asks the adapted protocol client to perform an MQTT subscribe operation + */ +AWS_MQTT_API int aws_mqtt_protocol_adapter_subscribe( + struct aws_mqtt_protocol_adapter *adapter, + struct aws_protocol_adapter_subscribe_options *options); + +/* + * Asks the adapted protocol client to perform an MQTT unsubscribe operation + */ +AWS_MQTT_API int aws_mqtt_protocol_adapter_unsubscribe( + struct aws_mqtt_protocol_adapter *adapter, + struct aws_protocol_adapter_unsubscribe_options *options); + +/* + * Asks the adapted protocol client to perform an MQTT publish operation + */ +AWS_MQTT_API int aws_mqtt_protocol_adapter_publish( + struct aws_mqtt_protocol_adapter *adapter, + struct aws_protocol_adapter_publish_options *options); + +/* + * Synchronously checks the connection state of the adapted protocol client. May only be called from the + * protocol client's event loop. + */ +AWS_MQTT_API bool aws_mqtt_protocol_adapter_is_connected(struct aws_mqtt_protocol_adapter *adapter); + +/* + * Returns the event loop that the protocol client is bound to. + */ +AWS_MQTT_API struct aws_event_loop *aws_mqtt_protocol_adapter_get_event_loop(struct aws_mqtt_protocol_adapter *adapter); + +AWS_MQTT_API const char *aws_protocol_adapter_subscription_event_type_to_c_str( + enum aws_protocol_adapter_subscription_event_type type); + +AWS_MQTT_API const char *aws_protocol_adapter_connection_event_type_to_c_str( + enum aws_protocol_adapter_connection_event_type type); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_PRIVATE_REQUEST_RESPONSE_PROTOCOL_ADAPTER_H */ diff --git a/include/aws/mqtt/private/request-response/request_response_client.h b/include/aws/mqtt/private/request-response/request_response_client.h new file mode 100644 index 00000000..b64ae7fa --- /dev/null +++ b/include/aws/mqtt/private/request-response/request_response_client.h @@ -0,0 +1,23 @@ +#ifndef AWS_MQTT_PRIVATE_REQUEST_RESPONSE_REQUEST_RESPONSE_CLIENT_H +#define AWS_MQTT_PRIVATE_REQUEST_RESPONSE_REQUEST_RESPONSE_CLIENT_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include + +AWS_EXTERN_C_BEGIN + +struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_acquire_internal( + struct aws_mqtt_request_response_client *client); + +struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_release_internal( + struct aws_mqtt_request_response_client *client); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_PRIVATE_REQUEST_RESPONSE_REQUEST_RESPONSE_CLIENT_H */ \ No newline at end of file diff --git a/include/aws/mqtt/private/request-response/subscription_manager.h b/include/aws/mqtt/private/request-response/subscription_manager.h new file mode 100644 index 00000000..da70b4c9 --- /dev/null +++ b/include/aws/mqtt/private/request-response/subscription_manager.h @@ -0,0 +1,264 @@ +#ifndef AWS_MQTT_PRIVATE_REQUEST_RESPONSE_SUBSCRIPTION_MANAGER_H +#define AWS_MQTT_PRIVATE_REQUEST_RESPONSE_SUBSCRIPTION_MANAGER_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include + +struct aws_mqtt_protocol_adapter; +struct aws_protocol_adapter_connection_event; +struct aws_protocol_adapter_subscription_event; + +/* + * Describes a change to the state of a request operation subscription + */ +enum aws_rr_subscription_event_type { + + /* + * A request subscription subscribe succeeded + */ + ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + + /* + * A request subscription subscribe failed + */ + ARRSET_REQUEST_SUBSCRIBE_FAILURE, + + /* + * A previously successful request subscription has ended. + * + * Under normal circumstances this can happen when + * + * (1) failure to rejoin a session + */ + ARRSET_REQUEST_SUBSCRIPTION_ENDED, + + /* + * A streaming subscription subscribe succeeded + */ + ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED, + + /* + * The protocol client failed to rejoin a session containing a previously-established streaming subscription + */ + ARRSET_STREAMING_SUBSCRIPTION_LOST, + + /* + * A streaming subscription subscribe attempt resulted in an error or reason code that the client has determined + * will result in indefinite failures to subscribe. In this case, we stop attempting to resubscribe. + * + * Situations that can lead to this: + * (1) Permission failures + * (2) Invalid topic filter + */ + ARRSET_STREAMING_SUBSCRIPTION_HALTED, + + /* + * A subscription has lost its last listener and can be purged + * + * This event is global; operation_id will always be zero. + */ + ARRSET_SUBSCRIPTION_EMPTY, + + /* + * A subscription has been unsubscribed from + * + * This event is global; operation_id will always be zero. + */ + ARRSET_UNSUBSCRIBE_COMPLETE, +}; + +struct aws_rr_subscription_status_event { + enum aws_rr_subscription_event_type type; + struct aws_byte_cursor topic_filter; + uint64_t operation_id; +}; + +/* + * Invariant: despite being on the same thread, these callbacks must be queued as cross-thread tasks on the native + * request-response client. This allows us to iterate internal collections without worrying about external + * callers disrupting things by invoking APIs back on us. + */ +typedef void( + aws_rr_subscription_status_event_callback_fn)(const struct aws_rr_subscription_status_event *event, void *userdata); + +struct aws_rr_subscription_manager_options { + + /* + * Maximum number of request-response subscriptions allowed. Must be at least two. + */ + size_t max_request_response_subscriptions; + + /* + * Maximum number of streaming subscriptions allowed. + */ + size_t max_streaming_subscriptions; + + /* + * Ack timeout to use for all subscribe and unsubscribe operations + */ + uint32_t operation_timeout_seconds; + + aws_rr_subscription_status_event_callback_fn *subscription_status_callback; + void *userdata; +}; + +/* + * The subscription manager works with the request-response client to handle subscriptions in an eager manner. + * Subscription purges are checked with every client service call. Unsubscribe failures don't trigger anything special, + * we'll just try again next time we look for subscription space. Subscribes are attempted on idle subscriptions + * that still need them, either in response to a new operation listener or a connection resumption event. + * + * We only allow one subscribe or unsubscribe to be outstanding at once for a given topic. If an operation requires a + * subscription while an unsubscribe is in progress the operation is blocked until the unsubscribe resolves. + * + * These invariants are dropped during shutdown. In that case, we immediately send unsubscribes for everything + * that is not already unsubscribing. + */ +struct aws_rr_subscription_manager { + struct aws_allocator *allocator; + + struct aws_rr_subscription_manager_options config; + + /* non-owning reference; the client is responsible for destroying this asynchronously (listener detachment) */ + struct aws_mqtt_protocol_adapter *protocol_adapter; + + /* &aws_rr_subscription_record.topic_filter_cursor -> aws_rr_subscription_record * */ + struct aws_hash_table subscriptions; + + bool is_protocol_client_connected; +}; + +enum aws_rr_subscription_type { + ARRST_EVENT_STREAM, + ARRST_REQUEST_RESPONSE, +}; + +struct aws_rr_acquire_subscription_options { + struct aws_byte_cursor *topic_filters; + size_t topic_filter_count; + + uint64_t operation_id; + enum aws_rr_subscription_type type; +}; + +struct aws_rr_release_subscription_options { + struct aws_byte_cursor *topic_filters; + size_t topic_filter_count; + + uint64_t operation_id; +}; + +enum aws_acquire_subscription_result_type { + + /* + * All requested subscriptions already exist and are active. The operation can proceed to the next stage. + */ + AASRT_SUBSCRIBED, + + /* + * The requested subscriptions now exist but at least one is not yet active. The operation must wait for subscribes + * to complete as success or failure. + */ + AASRT_SUBSCRIBING, + + /* + * At least one subscription does not exist and there is no room for it currently. Room may open up in the future, + * so the operation should wait. + */ + AASRT_BLOCKED, + + /* + * At least one subscription does not exist and there is no room for it. Unless an event stream subscription gets + * closed, no room will be available in the future. The operation should be failed. + */ + AASRT_NO_CAPACITY, + + /* + * An internal failure occurred while trying to establish subscriptions. The operation should be failed. + */ + AASRT_FAILURE +}; + +AWS_EXTERN_C_BEGIN + +/* + * Initializes a subscription manager. Every native request-response client owns a single subscription manager. + */ +AWS_MQTT_API void aws_rr_subscription_manager_init( + struct aws_rr_subscription_manager *manager, + struct aws_allocator *allocator, + struct aws_mqtt_protocol_adapter *protocol_adapter, + const struct aws_rr_subscription_manager_options *options); + +/* + * Cleans up a subscription manager. This is done early in the native request-response client shutdown process. + * After this API is called, no other subscription manager APIs will be called by the request-response client (during + * the rest of the asynchronous shutdown process). + */ +AWS_MQTT_API void aws_rr_subscription_manager_clean_up(struct aws_rr_subscription_manager *manager); + +/* + * Requests the the subscription manager unsubscribe from all currently-unused subscriptions + */ +AWS_MQTT_API void aws_rr_subscription_manager_purge_unused(struct aws_rr_subscription_manager *manager); + +/* + * Signals to the subscription manager that the native request-response client is processing an operation that + * needs a subscription to a particular topic. Return value indicates to the request-response client how it should + * proceed with processing the operation. + */ +AWS_MQTT_API enum aws_acquire_subscription_result_type aws_rr_subscription_manager_acquire_subscription( + struct aws_rr_subscription_manager *manager, + const struct aws_rr_acquire_subscription_options *options); + +/* + * Signals to the subscription manager that the native request-response client operation no longer + * needs a subscription to a particular topic. + */ +AWS_MQTT_API void aws_rr_subscription_manager_release_subscription( + struct aws_rr_subscription_manager *manager, + const struct aws_rr_release_subscription_options *options); + +/* + * Notifies the subscription manager of a subscription status event. Invoked by the native request-response client + * that owns the subscription manager. The native request-response client also owns the protocol adapter that + * the subscription event originates from, so the control flow looks like: + * + * [Subscribe] + * subscription manager -> protocol adapter Subscribe -> protocol client Subscribe -> network... + * + * [Result] + * protocol client Suback/Timeout/Error -> protocol adapter -> native request-response client -> + * subscription manager (this API) + */ +AWS_MQTT_API void aws_rr_subscription_manager_on_protocol_adapter_subscription_event( + struct aws_rr_subscription_manager *manager, + const struct aws_protocol_adapter_subscription_event *event); + +/* + * Notifies the subscription manager of a connection status event. Invoked by the native request-response client + * that owns the subscription manager. The native request-response client also owns the protocol adapter that + * the connection event originates from. The control flow looks like: + * + * protocol client connect/disconnect -> protocol adapter -> native request-response client -> + * Subscription manager (this API) + */ +AWS_MQTT_API void aws_rr_subscription_manager_on_protocol_adapter_connection_event( + struct aws_rr_subscription_manager *manager, + const struct aws_protocol_adapter_connection_event *event); + +/* + * Checks subscription manager options for validity. + */ +AWS_MQTT_API bool aws_rr_subscription_manager_are_options_valid( + const struct aws_rr_subscription_manager_options *options); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_PRIVATE_REQUEST_RESPONSE_SUBSCRIPTION_MANAGER_H */ diff --git a/include/aws/mqtt/private/shared_constants.h b/include/aws/mqtt/private/shared.h similarity index 87% rename from include/aws/mqtt/private/shared_constants.h rename to include/aws/mqtt/private/shared.h index 0a835942..a1165d8e 100644 --- a/include/aws/mqtt/private/shared_constants.h +++ b/include/aws/mqtt/private/shared.h @@ -8,6 +8,8 @@ #include +#include + AWS_EXTERN_C_BEGIN AWS_MQTT_API extern const struct aws_byte_cursor *g_websocket_handshake_default_path; diff --git a/include/aws/mqtt/request-response/request_response_client.h b/include/aws/mqtt/request-response/request_response_client.h new file mode 100644 index 00000000..3edff934 --- /dev/null +++ b/include/aws/mqtt/request-response/request_response_client.h @@ -0,0 +1,274 @@ +#ifndef AWS_MQTT_REQUEST_RESPONSE_REQUEST_RESPONSE_CLIENT_H +#define AWS_MQTT_REQUEST_RESPONSE_REQUEST_RESPONSE_CLIENT_H + +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +struct aws_event_loop; +struct aws_mqtt_request_response_client; +struct aws_mqtt_client_connection; +struct aws_mqtt5_client; + +/* + * A response path is a pair of values - MQTT topic and a JSON path - that describe where a response to + * an MQTT-based request may arrive. For a given request type, there may be multiple response paths and each + * one is associated with a separate JSON schema for the response body. + */ +struct aws_mqtt_request_operation_response_path { + + /* + * MQTT topic that a response may arrive on. + */ + struct aws_byte_cursor topic; + + /* + * JSON path for finding correlation tokens within payloads that arrive on this path's topic. + */ + struct aws_byte_cursor correlation_token_json_path; +}; + +/* + * Callback signature for request-response completion. + * + * Invariants: + * If error_code is non-zero then response_topic and payload will be NULL. + * If response_topic and payload are not NULL then error_code will be 0. + * response_topic and payload are either both set or both not set. + */ +typedef void(aws_mqtt_request_operation_completion_fn)( + const struct aws_byte_cursor *response_topic, + const struct aws_byte_cursor *payload, + int error_code, + void *user_data); + +/* + * Configuration options for a request-response operation. + */ +struct aws_mqtt_request_operation_options { + + /* + * Set of topic filters that should be subscribed to in order to cover all possible response paths. Sometimes + * we can use wildcards to cut down on the subscriptions needed; sometimes we can't. + */ + struct aws_byte_cursor *subscription_topic_filters; + size_t subscription_topic_filter_count; + + /* + * Set of all possible response paths associated with this request type + */ + struct aws_mqtt_request_operation_response_path *response_paths; + size_t response_path_count; + + /* + * Topic to publish the request to once response subscriptions have been established. + */ + struct aws_byte_cursor publish_topic; + + /* + * Payload to publish in order to initiate the request + */ + struct aws_byte_cursor serialized_request; + + /* + * Correlation token embedded in the request that must be found in a response message. This can be the + * empty cursor to support certain services which don't use correlation tokens. In this case, the client + * only allows one request at a time to use the associated subscriptions; no concurrency is possible. There + * are some optimizations we could make here but for now, they're not worth the complexity. + */ + struct aws_byte_cursor correlation_token; + + /* + * Callback (and associated user data) to invoke when the request is completed. + */ + aws_mqtt_request_operation_completion_fn *completion_callback; + void *user_data; +}; + +/* + * Describes a change to the state of a streaming operation subscription + */ +enum aws_rr_streaming_subscription_event_type { + + /* + * The streaming operation is successfully subscribed to its topic (filter) + */ + ARRSSET_SUBSCRIPTION_ESTABLISHED, + + /* + * The streaming operation has temporarily lost its subscription to its topic (filter) + */ + ARRSSET_SUBSCRIPTION_LOST, + + /* + * The streaming operation has entered a terminal state where it has given up trying to subscribe + * to its topic (filter). This is always due to user error (bad topic filter or IoT Core permission policy). + */ + ARRSSET_SUBSCRIPTION_HALTED, +}; + +/* + * Callback signature for when the subscription status of a streaming operation changes. + */ +typedef void(aws_mqtt_streaming_operation_subscription_status_fn)( + enum aws_rr_streaming_subscription_event_type status, + int error_code, + void *user_data); + +/* + * Callback signature for when a publish arrives that matches a streaming operation's subscription + */ +typedef void(aws_mqtt_streaming_operation_incoming_publish_fn)(struct aws_byte_cursor payload, void *user_data); + +/* + * Callback signature for when a streaming operation is fully destroyed and no more events will be emitted. + */ +typedef void(aws_mqtt_streaming_operation_terminated_fn)(void *user_data); + +/* + * Configuration options for a streaming operation. + */ +struct aws_mqtt_streaming_operation_options { + + /* + * Topic filter that the streaming operation should listen on + */ + struct aws_byte_cursor topic_filter; + + /* + * Callback for subscription status events + */ + aws_mqtt_streaming_operation_subscription_status_fn *subscription_status_callback; + + /* + * Callback for publish messages that match the operation's topic filter + */ + aws_mqtt_streaming_operation_incoming_publish_fn *incoming_publish_callback; + + /* + * Callback for streaming operation final shutdown + */ + aws_mqtt_streaming_operation_terminated_fn *terminated_callback; + + /* + * Data passed to all streaming operation callbacks + */ + void *user_data; +}; + +typedef void(aws_mqtt_request_response_client_initialized_callback_fn)(void *user_data); +typedef void(aws_mqtt_request_response_client_terminated_callback_fn)(void *user_data); + +/* + * Request-response client configuration options + */ +struct aws_mqtt_request_response_client_options { + + /* + * Maximum number of subscriptions that the client will concurrently use for request-response operations + */ + size_t max_request_response_subscriptions; + + /* + * Maximum number of subscriptions that the client will concurrently use for streaming operations + */ + size_t max_streaming_subscriptions; + + /* + * Duration, in seconds, that a request-response operation will wait for completion before giving up + */ + uint32_t operation_timeout_seconds; + + /* + * Request-response client initialization is asynchronous. This callback is invoked when the client is fully + * initialized. + * + * Do not bind the initialized callback; it exists mostly for tests and should not be exposed + */ + aws_mqtt_request_response_client_initialized_callback_fn *initialized_callback; + + /* + * Callback invoked when the client's asynchronous destruction process has fully completed. + */ + aws_mqtt_request_response_client_terminated_callback_fn *terminated_callback; + + /* + * Arbitrary data to pass to the client callbacks + */ + void *user_data; +}; + +AWS_EXTERN_C_BEGIN + +/* + * Create a new request-response client that uses an MQTT311 client. + */ +AWS_MQTT_API struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_new_from_mqtt311_client( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *client, + const struct aws_mqtt_request_response_client_options *options); + +/* + * Create a new request-response client that uses an MQTT5 client. + */ +AWS_MQTT_API struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_new_from_mqtt5_client( + struct aws_allocator *allocator, + struct aws_mqtt5_client *client, + const struct aws_mqtt_request_response_client_options *options); + +/* + * Add a reference to a request-response client + */ +AWS_MQTT_API struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_acquire( + struct aws_mqtt_request_response_client *client); + +/* + * Remove a reference from a request-response client + */ +AWS_MQTT_API struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_release( + struct aws_mqtt_request_response_client *client); + +/* + * Submits a request operation to the client + */ +AWS_MQTT_API int aws_mqtt_request_response_client_submit_request( + struct aws_mqtt_request_response_client *client, + const struct aws_mqtt_request_operation_options *request_options); + +/* + * Creates a new streaming operation. Streaming operations start in an inactive state and must be + * activated before its subscription can be established. + */ +AWS_MQTT_API struct aws_mqtt_rr_client_operation *aws_mqtt_request_response_client_create_streaming_operation( + struct aws_mqtt_request_response_client *client, + const struct aws_mqtt_streaming_operation_options *streaming_options); + +/* + * Returns the event loop used by the request-response client's protocol client + */ +AWS_MQTT_API struct aws_event_loop *aws_mqtt_request_response_client_get_event_loop( + struct aws_mqtt_request_response_client *client); + +/* + * Initiates a streaming operation's subscription process. + */ +AWS_MQTT_API int aws_mqtt_rr_client_operation_activate(struct aws_mqtt_rr_client_operation *operation); + +/* + * Add a reference to a streaming operation. + */ +AWS_MQTT_API struct aws_mqtt_rr_client_operation *aws_mqtt_rr_client_operation_acquire( + struct aws_mqtt_rr_client_operation *operation); + +/* + * Remove a reference from a streaming operation + */ +AWS_MQTT_API struct aws_mqtt_rr_client_operation *aws_mqtt_rr_client_operation_release( + struct aws_mqtt_rr_client_operation *operation); + +AWS_EXTERN_C_END + +#endif /* AWS_MQTT_REQUEST_RESPONSE_REQUEST_RESPONSE_CLIENT_H */ diff --git a/include/aws/mqtt/v5/mqtt5_client.h b/include/aws/mqtt/v5/mqtt5_client.h index d04d2981..5cac371c 100644 --- a/include/aws/mqtt/v5/mqtt5_client.h +++ b/include/aws/mqtt/v5/mqtt5_client.h @@ -341,6 +341,7 @@ struct aws_mqtt5_publish_completion_options { aws_mqtt5_publish_completion_fn *completion_callback; void *completion_user_data; + /** Overrides the client's ack timeout with this value, for this operation only */ uint32_t ack_timeout_seconds_override; }; @@ -351,6 +352,7 @@ struct aws_mqtt5_subscribe_completion_options { aws_mqtt5_subscribe_completion_fn *completion_callback; void *completion_user_data; + /** Overrides the client's ack timeout with this value, for this operation only */ uint32_t ack_timeout_seconds_override; }; @@ -361,6 +363,7 @@ struct aws_mqtt5_unsubscribe_completion_options { aws_mqtt5_unsubscribe_completion_fn *completion_callback; void *completion_user_data; + /** Overrides the client's ack timeout with this value, for this operation only */ uint32_t ack_timeout_seconds_override; }; diff --git a/source/client.c b/source/client.c index c332c4da..8102ea24 100644 --- a/source/client.c +++ b/source/client.c @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include @@ -93,25 +93,6 @@ void mqtt_connection_set_state( connection->synced_data.state = state; } -struct request_timeout_wrapper; - -/* used for timeout task */ -struct request_timeout_task_arg { - uint16_t packet_id; - struct aws_mqtt_client_connection_311_impl *connection; - struct request_timeout_wrapper *task_arg_wrapper; -}; - -/* - * We want the timeout task to be able to destroy the forward reference from the operation's task arg structure - * to the timeout task. But the operation task arg structures don't have any data structure in common. So to allow - * the timeout to refer back to a zero-able forward pointer, we wrap a pointer to the timeout task and embed it - * in every operation's task arg that needs to create a timeout. - */ -struct request_timeout_wrapper { - struct request_timeout_task_arg *timeout_task_arg; -}; - static void s_request_timeout(struct aws_channel_task *channel_task, void *arg, enum aws_task_status status) { (void)channel_task; struct request_timeout_task_arg *timeout_task_arg = arg; @@ -139,8 +120,14 @@ static void s_request_timeout(struct aws_channel_task *channel_task, void *arg, static struct request_timeout_task_arg *s_schedule_timeout_task( struct aws_mqtt_client_connection_311_impl *connection, - uint16_t packet_id) { - /* schedule a timeout task to run, in case server consider the publish is not received */ + uint16_t packet_id, + uint64_t timeout_duration_in_ns) { + + if (timeout_duration_in_ns == UINT64_MAX || timeout_duration_in_ns == 0 || packet_id == 0) { + return NULL; + } + + /* schedule a timeout task to run, in case server never sends us an ack */ struct aws_channel_task *request_timeout_task = NULL; struct request_timeout_task_arg *timeout_task_arg = NULL; if (!aws_mem_acquire_many( @@ -161,7 +148,7 @@ static struct request_timeout_task_arg *s_schedule_timeout_task( aws_mem_release(connection->allocator, timeout_task_arg); return NULL; } - timestamp = aws_add_u64_saturating(timestamp, connection->operation_timeout_ns); + timestamp = aws_add_u64_saturating(timestamp, timeout_duration_in_ns); aws_channel_schedule_task_future(connection->slot->channel, request_timeout_task, timestamp); return timeout_task_arg; } @@ -259,6 +246,7 @@ static void s_mqtt_client_shutdown( (void)channel; struct aws_mqtt_client_connection_311_impl *connection = user_data; + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(connection->loop)); AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: Channel has been shutdown with error code %d", (void *)connection, error_code); @@ -404,6 +392,7 @@ static void s_mqtt_client_shutdown( "id=%p: Connection interrupted, calling callback and attempting reconnect", (void *)connection); MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_interrupted, error_code); + aws_mqtt311_callback_set_manager_on_connection_interrupted(&connection->callback_manager, error_code); /* In case user called disconnect from the on_interrupted callback */ bool stop_reconnect; @@ -442,6 +431,7 @@ static void s_mqtt_client_shutdown( (void *)connection); MQTT_CLIENT_CALL_CALLBACK(connection, on_disconnect); MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_closed, NULL); + aws_mqtt311_callback_set_manager_on_disconnect(&connection->callback_manager); break; case AWS_MQTT_CLIENT_STATE_DISCONNECTING: AWS_LOGF_DEBUG( @@ -450,6 +440,7 @@ static void s_mqtt_client_shutdown( (void *)connection); MQTT_CLIENT_CALL_CALLBACK(connection, on_disconnect); MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_closed, NULL); + aws_mqtt311_callback_set_manager_on_disconnect(&connection->callback_manager); break; case AWS_MQTT_CLIENT_STATE_CONNECTING: AWS_LOGF_TRACE( @@ -801,6 +792,8 @@ static void s_mqtt_client_connection_destroy_final(struct aws_mqtt_client_connec termination_handler_user_data = connection->on_termination_ud; } + aws_mqtt311_callback_set_manager_clean_up(&connection->callback_manager); + /* If the reconnect_task isn't freed, free it */ if (connection->reconnect_task) { aws_mem_release(connection->reconnect_task->allocator, connection->reconnect_task); @@ -1894,6 +1887,20 @@ static enum aws_mqtt_client_request_state s_subscribe_send(uint16_t packet_id, b aws_mem_release(message->allocator, message); } + /* TODO: timing should start from the message written into the socket, which is aws_io_message->on_completion + * invoked, but there are bugs in the websocket handler (and maybe also the h1 handler?) where we don't properly + * fire the on_completion callbacks. */ + struct request_timeout_task_arg *timeout_task_arg = + s_schedule_timeout_task(task_arg->connection, packet_id, task_arg->timeout_duration_in_ns); + if (timeout_task_arg) { + /* + * Set up mutual references between the operation task args and the timeout task args. Whoever runs first + * "wins", does its logic, and then breaks the connection between the two. + */ + task_arg->timeout_wrapper.timeout_task_arg = timeout_task_arg; + timeout_task_arg->task_arg_wrapper = &task_arg->timeout_wrapper; + } + if (!task_arg->tree_updated) { aws_mqtt_topic_tree_transaction_commit(&task_arg->connection->thread_data.subscriptions, &transaction); task_arg->tree_updated = true; @@ -1959,6 +1966,17 @@ static void s_subscribe_complete( error_code, task_arg->on_suback_ud); } + + /* + * If we have a forward pointer to a timeout task, then that means the timeout task has not run yet. So we should + * follow it and zero out the back pointer to us, because we're going away now. The timeout task will run later + * and be harmless (even vs. future operations with the same packet id) because it only cancels if it has a back + * pointer. + */ + if (task_arg->timeout_wrapper.timeout_task_arg) { + task_arg->timeout_wrapper.timeout_task_arg->task_arg_wrapper = NULL; + } + for (size_t i = 0; i < list_len; i++) { aws_array_list_get_at(&task_arg->topics, &topic, i); s_task_topic_release(topic); @@ -1991,6 +2009,7 @@ static uint16_t s_aws_mqtt_client_connection_311_subscribe_multiple( task_arg->connection = connection; task_arg->on_suback.multi = on_suback; task_arg->on_suback_ud = on_suback_ud; + task_arg->timeout_duration_in_ns = connection->operation_timeout_ns; const size_t num_topics = aws_array_list_length(topic_filters); @@ -2129,23 +2148,33 @@ static void s_subscribe_single_complete( error_code, task_arg->on_suback_ud); } + + /* + * If we have a forward pointer to a timeout task, then that means the timeout task has not run yet. So we should + * follow it and zero out the back pointer to us, because we're going away now. The timeout task will run later + * and be harmless (even vs. future operations with the same packet id) because it only cancels if it has a back + * pointer. + */ + if (task_arg->timeout_wrapper.timeout_task_arg) { + task_arg->timeout_wrapper.timeout_task_arg->task_arg_wrapper = NULL; + } + s_task_topic_release(topic); aws_array_list_clean_up(&task_arg->topics); aws_mqtt_packet_subscribe_clean_up(&task_arg->subscribe); aws_mem_release(task_arg->connection->allocator, task_arg); } -static uint16_t s_aws_mqtt_client_connection_311_subscribe( - void *impl, +uint16_t aws_mqtt_client_connection_311_subscribe( + struct aws_mqtt_client_connection_311_impl *connection, const struct aws_byte_cursor *topic_filter, enum aws_mqtt_qos qos, aws_mqtt_client_publish_received_fn *on_publish, void *on_publish_ud, aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, aws_mqtt_suback_fn *on_suback, - void *on_suback_ud) { - - struct aws_mqtt_client_connection_311_impl *connection = impl; + void *on_suback_ud, + uint64_t timeout_ns) { AWS_PRECONDITION(connection); @@ -2174,6 +2203,7 @@ static uint16_t s_aws_mqtt_client_connection_311_subscribe( task_arg->connection = connection; task_arg->on_suback.single = on_suback; task_arg->on_suback_ud = on_suback_ud; + task_arg->timeout_duration_in_ns = timeout_ns; /* It stores the pointer */ aws_array_list_init_static(&task_arg->topics, task_topic_storage, 1, sizeof(void *)); @@ -2249,6 +2279,29 @@ static uint16_t s_aws_mqtt_client_connection_311_subscribe( return 0; } +static uint16_t s_aws_mqtt_client_connection_311_subscribe( + void *impl, + const struct aws_byte_cursor *topic_filter, + enum aws_mqtt_qos qos, + aws_mqtt_client_publish_received_fn *on_publish, + void *on_publish_ud, + aws_mqtt_userdata_cleanup_fn *on_ud_cleanup, + aws_mqtt_suback_fn *on_suback, + void *on_suback_ud) { + + struct aws_mqtt_client_connection_311_impl *connection = impl; + return aws_mqtt_client_connection_311_subscribe( + connection, + topic_filter, + qos, + on_publish, + on_publish_ud, + on_ud_cleanup, + on_suback, + on_suback_ud, + connection->operation_timeout_ns); +} + /******************************************************************************* * Resubscribe ******************************************************************************/ @@ -2353,6 +2406,20 @@ static enum aws_mqtt_client_request_state s_resubscribe_send( aws_mem_release(message->allocator, message); } + /* TODO: timing should start from the message written into the socket, which is aws_io_message->on_completion + * invoked, but there are bugs in the websocket handler (and maybe also the h1 handler?) where we don't properly + * fire the on_completion callbacks. */ + struct request_timeout_task_arg *timeout_task_arg = + s_schedule_timeout_task(task_arg->connection, packet_id, task_arg->timeout_duration_in_ns); + if (timeout_task_arg) { + /* + * Set up mutual references between the operation task args and the timeout task args. Whoever runs first + * "wins", does its logic, and then breaks the connection between the two. + */ + task_arg->timeout_wrapper.timeout_task_arg = timeout_task_arg; + timeout_task_arg->task_arg_wrapper = &task_arg->timeout_wrapper; + } + return AWS_MQTT_CLIENT_REQUEST_ONGOING; handle_error: @@ -2416,6 +2483,16 @@ static void s_resubscribe_complete( clean_up: + /* + * If we have a forward pointer to a timeout task, then that means the timeout task has not run yet. So we should + * follow it and zero out the back pointer to us, because we're going away now. The timeout task will run later + * and be harmless (even vs. future operations with the same packet id) because it only cancels if it has a back + * pointer. + */ + if (task_arg->timeout_wrapper.timeout_task_arg) { + task_arg->timeout_wrapper.timeout_task_arg->task_arg_wrapper = NULL; + } + /* We need to cleanup the subscribe_task_topics, since they are not inserted into the topic tree by resubscribe. We * take the ownership to clean it up */ for (size_t i = 0; i < list_len; i++) { @@ -2445,6 +2522,7 @@ static uint16_t s_aws_mqtt_311_resubscribe_existing_topics( task_arg->connection = connection; task_arg->on_suback.multi = on_suback; task_arg->on_suback_ud = on_suback_ud; + task_arg->timeout_duration_in_ns = connection->operation_timeout_ns; /* Calculate the size of the packet. * The fixed header is 2 bytes and the packet ID is 2 bytes @@ -2505,6 +2583,7 @@ struct unsubscribe_task_arg { void *on_unsuback_ud; struct request_timeout_wrapper timeout_wrapper; + uint64_t timeout_duration_in_ns; }; static enum aws_mqtt_client_request_state s_unsubscribe_send( @@ -2596,18 +2675,17 @@ static enum aws_mqtt_client_request_state s_unsubscribe_send( /* TODO: timing should start from the message written into the socket, which is aws_io_message->on_completion * invoked, but there are bugs in the websocket handler (and maybe also the h1 handler?) where we don't properly * fire the on_completion callbacks. */ - struct request_timeout_task_arg *timeout_task_arg = s_schedule_timeout_task(task_arg->connection, packet_id); - if (!timeout_task_arg) { - return AWS_MQTT_CLIENT_REQUEST_ERROR; + struct request_timeout_task_arg *timeout_task_arg = + s_schedule_timeout_task(task_arg->connection, packet_id, task_arg->timeout_duration_in_ns); + if (timeout_task_arg) { + /* + * Set up mutual references between the operation task args and the timeout task args. Whoever runs first + * "wins", does its logic, and then breaks the connection between the two. + */ + task_arg->timeout_wrapper.timeout_task_arg = timeout_task_arg; + timeout_task_arg->task_arg_wrapper = &task_arg->timeout_wrapper; } - /* - * Set up mutual references between the operation task args and the timeout task args. Whoever runs first - * "wins", does its logic, and then breaks the connection between the two. - */ - task_arg->timeout_wrapper.timeout_task_arg = timeout_task_arg; - timeout_task_arg->task_arg_wrapper = &task_arg->timeout_wrapper; - if (!task_arg->tree_updated) { aws_mqtt_topic_tree_transaction_commit(&task_arg->connection->thread_data.subscriptions, &transaction); task_arg->tree_updated = true; @@ -2650,7 +2728,6 @@ static void s_unsubscribe_complete( */ if (task_arg->timeout_wrapper.timeout_task_arg) { task_arg->timeout_wrapper.timeout_task_arg->task_arg_wrapper = NULL; - task_arg->timeout_wrapper.timeout_task_arg = NULL; } if (task_arg->on_unsuback) { @@ -2662,13 +2739,12 @@ static void s_unsubscribe_complete( aws_mem_release(task_arg->connection->allocator, task_arg); } -static uint16_t s_aws_mqtt_client_connection_311_unsubscribe( - void *impl, +uint16_t aws_mqtt_client_connection_311_unsubscribe( + struct aws_mqtt_client_connection_311_impl *connection, const struct aws_byte_cursor *topic_filter, aws_mqtt_op_complete_fn *on_unsuback, - void *on_unsuback_ud) { - - struct aws_mqtt_client_connection_311_impl *connection = impl; + void *on_unsuback_ud, + uint64_t timeout_ns) { AWS_PRECONDITION(connection); @@ -2688,6 +2764,7 @@ static uint16_t s_aws_mqtt_client_connection_311_unsubscribe( task_arg->filter = aws_byte_cursor_from_string(task_arg->filter_string); task_arg->on_unsuback = on_unsuback; task_arg->on_unsuback_ud = on_unsuback_ud; + task_arg->timeout_duration_in_ns = timeout_ns; /* Calculate the size of the unsubscribe packet. * The fixed header is always 2 bytes, the packet ID is always 2 bytes @@ -2723,6 +2800,18 @@ static uint16_t s_aws_mqtt_client_connection_311_unsubscribe( return 0; } +static uint16_t s_aws_mqtt_client_connection_311_unsubscribe( + void *impl, + const struct aws_byte_cursor *topic_filter, + aws_mqtt_op_complete_fn *on_unsuback, + void *on_unsuback_ud) { + + struct aws_mqtt_client_connection_311_impl *connection = impl; + + return aws_mqtt_client_connection_311_unsubscribe( + connection, topic_filter, on_unsuback, on_unsuback_ud, connection->operation_timeout_ns); +} + /******************************************************************************* * Publish ******************************************************************************/ @@ -2742,6 +2831,7 @@ struct publish_task_arg { aws_mqtt_op_complete_fn *on_complete; void *userdata; + uint64_t timeout_duration_in_ns; struct request_timeout_wrapper timeout_wrapper; }; @@ -2876,15 +2966,13 @@ static enum aws_mqtt_client_request_state s_publish_send(uint16_t packet_id, boo goto write_payload_chunk; } } - if (!is_qos_0 && connection->operation_timeout_ns != UINT64_MAX) { - /* TODO: timing should start from the message written into the socket, which is aws_io_message->on_completion - * invoked, but there are bugs in the websocket handler (and maybe also the h1 handler?) where we don't properly - * fire fire the on_completion callbacks. */ - struct request_timeout_task_arg *timeout_task_arg = s_schedule_timeout_task(connection, packet_id); - if (!timeout_task_arg) { - return AWS_MQTT_CLIENT_REQUEST_ERROR; - } + /* TODO: timing should start from the message written into the socket, which is aws_io_message->on_completion + * invoked, but there are bugs in the websocket handler (and maybe also the h1 handler?) where we don't properly + * fire fire the on_completion callbacks. */ + struct request_timeout_task_arg *timeout_task_arg = + s_schedule_timeout_task(connection, packet_id, task_arg->timeout_duration_in_ns); + if (timeout_task_arg != NULL) { /* * Set up mutual references between the operation task args and the timeout task args. Whoever runs first * "wins", does its logic, and then breaks the connection between the two. @@ -2921,7 +3009,6 @@ static void s_publish_complete( */ if (task_arg->timeout_wrapper.timeout_task_arg != NULL) { task_arg->timeout_wrapper.timeout_task_arg->task_arg_wrapper = NULL; - task_arg->timeout_wrapper.timeout_task_arg = NULL; } aws_byte_buf_clean_up(&task_arg->payload_buf); @@ -2929,16 +3016,15 @@ static void s_publish_complete( aws_mem_release(connection->allocator, task_arg); } -static uint16_t s_aws_mqtt_client_connection_311_publish( - void *impl, +uint16_t aws_mqtt_client_connection_311_publish( + struct aws_mqtt_client_connection_311_impl *connection, const struct aws_byte_cursor *topic, enum aws_mqtt_qos qos, bool retain, const struct aws_byte_cursor *payload, aws_mqtt_op_complete_fn *on_complete, - void *userdata) { - - struct aws_mqtt_client_connection_311_impl *connection = impl; + void *userdata, + uint64_t timeout_ns) { AWS_PRECONDITION(connection); @@ -2962,6 +3048,7 @@ static uint16_t s_aws_mqtt_client_connection_311_publish( arg->topic = aws_byte_cursor_from_string(arg->topic_string); arg->qos = qos; arg->retain = retain; + arg->timeout_duration_in_ns = timeout_ns; struct aws_byte_cursor payload_cursor; AWS_ZERO_STRUCT(payload_cursor); @@ -3019,6 +3106,21 @@ static uint16_t s_aws_mqtt_client_connection_311_publish( return 0; } +static uint16_t s_aws_mqtt_client_connection_311_publish( + void *impl, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + bool retain, + const struct aws_byte_cursor *payload, + aws_mqtt_op_complete_fn *on_complete, + void *userdata) { + + struct aws_mqtt_client_connection_311_impl *connection = impl; + + return aws_mqtt_client_connection_311_publish( + connection, topic, qos, retain, payload, on_complete, userdata, connection->operation_timeout_ns); +} + /******************************************************************************* * Ping ******************************************************************************/ @@ -3220,6 +3322,18 @@ static void s_aws_mqtt_client_connection_311_release(void *impl) { aws_ref_count_release(&connection->ref_count); } +static enum aws_mqtt311_impl_type s_aws_mqtt_client_connection_311_get_impl(const void *impl) { + (void)impl; + + return AWS_MQTT311_IT_311_CONNECTION; +} + +static struct aws_event_loop *s_aws_mqtt_client_connection_311_get_event_loop(const void *impl) { + const struct aws_mqtt_client_connection_311_impl *connection = impl; + + return connection->loop; +} + static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_311_vtable = { .acquire_fn = s_aws_mqtt_client_connection_311_acquire, .release_fn = s_aws_mqtt_client_connection_311_release, @@ -3243,6 +3357,8 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_311 .unsubscribe_fn = s_aws_mqtt_client_connection_311_unsubscribe, .publish_fn = s_aws_mqtt_client_connection_311_publish, .get_stats_fn = s_aws_mqtt_client_connection_311_get_stats, + .get_impl_type = s_aws_mqtt_client_connection_311_get_impl, + .get_event_loop = s_aws_mqtt_client_connection_311_get_event_loop, }; static struct aws_mqtt_client_connection_vtable *s_aws_mqtt_client_connection_311_vtable_ptr = @@ -3344,6 +3460,8 @@ struct aws_mqtt_client_connection *aws_mqtt_client_connection_new(struct aws_mqt connection->handler.vtable = aws_mqtt_get_client_channel_vtable(); connection->handler.impl = connection; + aws_mqtt311_callback_set_manager_init(&connection->callback_manager, connection->allocator, &connection->base); + return &connection->base; failed_init_outstanding_requests_table: diff --git a/source/client_channel_handler.c b/source/client_channel_handler.c index 2719f41c..0fe344ec 100644 --- a/source/client_channel_handler.c +++ b/source/client_channel_handler.c @@ -263,6 +263,9 @@ static int s_packet_handler_connack(struct aws_byte_cursor message_cursor, void MQTT_CLIENT_CALL_CALLBACK_ARGS( connection, on_connection_success, connack.connect_return_code, connack.session_present); + aws_mqtt311_callback_set_manager_on_connection_success( + &connection->callback_manager, connack.connect_return_code, connack.session_present); + AWS_LOGF_TRACE(AWS_LS_MQTT_CLIENT, "id=%p: connection callback completed", (void *)connection); s_update_next_ping_time(connection); @@ -291,6 +294,9 @@ static int s_packet_handler_publish(struct aws_byte_cursor message_cursor, void MQTT_CLIENT_CALL_CALLBACK_ARGS(connection, on_any_publish, &publish.topic_name, &publish.payload, dup, qos, retain); + aws_mqtt311_callback_set_manager_on_publish_received( + &connection->callback_manager, &publish.topic_name, &publish.payload, dup, qos, retain); + AWS_LOGF_TRACE( AWS_LS_MQTT_CLIENT, "id=%p: publish received with msg id=%" PRIu16 " dup=%d qos=%d retain=%d payload-size=%zu topic=" PRInSTR, diff --git a/source/client_impl_shared.c b/source/client_impl_shared.c index 6f65eb88..019adc5c 100644 --- a/source/client_impl_shared.c +++ b/source/client_impl_shared.c @@ -204,6 +204,11 @@ int aws_mqtt_client_connection_get_stats( return (*connection->vtable->get_stats_fn)(connection->impl, stats); } +enum aws_mqtt311_impl_type aws_mqtt_client_connection_get_impl_type( + const struct aws_mqtt_client_connection *connection) { + return (*connection->vtable->get_impl_type)(connection->impl); +} + uint64_t aws_mqtt_hash_uint16_t(const void *item) { return *(uint16_t *)item; } @@ -218,3 +223,7 @@ bool aws_mqtt_byte_cursor_hash_equality(const void *a, const void *b) { return aws_byte_cursor_eq(a_cursor, b_cursor); } + +struct aws_event_loop *aws_mqtt_client_connection_get_event_loop(const struct aws_mqtt_client_connection *connection) { + return (*connection->vtable->get_event_loop)(connection->impl); +} diff --git a/source/mqtt.c b/source/mqtt.c index 9caeaa9b..252cbd49 100644 --- a/source/mqtt.c +++ b/source/mqtt.c @@ -236,6 +236,30 @@ bool aws_mqtt_is_valid_topic_filter(const struct aws_byte_cursor *topic_filter) AWS_DEFINE_ERROR_INFO_MQTT( AWS_ERROR_MQTT_ACK_REASON_CODE_FAILURE, "MQTT ack packet received with a failing reason code"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE, + "MQTT operation returned a failing reason code"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_REQUEST_RESPONSE_CLIENT_SHUT_DOWN, + "Request operation failed due to client shut down"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, + "Request operation failed due to timeout"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_REQUEST_RESPONSE_NO_SUBSCRIPTION_CAPACITY, + "Streaming request operation failed because there was no space for the subscription"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_REQUEST_RESPONSE_SUBSCRIBE_FAILURE, + "Request operation failed because the associated subscribe failed synchronously"), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_REQUEST_RESPONSE_INTERNAL_ERROR, + "Request operation failed due to a non-specific internal error within the client."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_REQUEST_RESPONSE_PUBLISH_FAILURE, + "Request-response operation failed because the associated publish failed synchronously."), + AWS_DEFINE_ERROR_INFO_MQTT( + AWS_ERROR_MQTT_REUQEST_RESPONSE_STREAM_ALREADY_ACTIVATED, + "Streaming operation activation failed because the operaation had already been activated."), }; /* clang-format on */ #undef AWS_DEFINE_ERROR_INFO_MQTT @@ -254,6 +278,7 @@ static struct aws_error_info_list s_error_list = { DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_CLIENT, "mqtt5-client", "MQTT5 client and connections"), DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_CANARY, "mqtt5-canary", "MQTT5 canary logging"), DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT5_TO_MQTT3_ADAPTER, "mqtt5-to-mqtt3-adapter", "MQTT5-To-MQTT3 adapter logging"), + DEFINE_LOG_SUBJECT_INFO(AWS_LS_MQTT_REQUEST_RESPONSE, "mqtt-request-response-systems", "MQTT request-response systems logging"), }; /* clang-format on */ diff --git a/source/mqtt311_listener.c b/source/mqtt311_listener.c new file mode 100644 index 00000000..69bec528 --- /dev/null +++ b/source/mqtt311_listener.c @@ -0,0 +1,329 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include + +#include + +static struct aws_event_loop *s_mqtt_client_connection_get_event_loop( + const struct aws_mqtt_client_connection *connection) { + AWS_FATAL_ASSERT(aws_mqtt_client_connection_get_impl_type(connection) == AWS_MQTT311_IT_311_CONNECTION); + + struct aws_mqtt_client_connection_311_impl *connection_impl = connection->impl; + + return connection_impl->loop; +} + +struct aws_mqtt311_listener { + struct aws_allocator *allocator; + + struct aws_ref_count ref_count; + + struct aws_mqtt311_listener_config config; + + uint64_t callback_set_id; + + struct aws_task initialize_task; + struct aws_task terminate_task; +}; + +static void s_mqtt311_listener_destroy(struct aws_mqtt311_listener *listener) { + + aws_mqtt_client_connection_release(listener->config.connection); + + aws_mqtt311_listener_termination_completion_fn *termination_callback = listener->config.termination_callback; + void *termination_callback_user_data = listener->config.termination_callback_user_data; + + aws_mem_release(listener->allocator, listener); + + if (termination_callback != NULL) { + (*termination_callback)(termination_callback_user_data); + } +} + +static void s_mqtt311_listener_initialize_task_fn(struct aws_task *task, void *arg, enum aws_task_status task_status) { + (void)task; + + struct aws_mqtt311_listener *listener = arg; + + if (task_status == AWS_TASK_STATUS_RUN_READY) { + struct aws_mqtt_client_connection_311_impl *connection_impl = listener->config.connection->impl; + listener->callback_set_id = aws_mqtt311_callback_set_manager_push_front( + &connection_impl->callback_manager, &listener->config.listener_callbacks); + AWS_LOGF_INFO( + AWS_LS_MQTT_GENERAL, + "id=%p: Mqtt311 Listener initialized, listener id=%p", + (void *)listener->config.connection, + (void *)listener); + aws_mqtt311_listener_release(listener); + } else { + s_mqtt311_listener_destroy(listener); + } +} + +static void s_mqtt311_listener_terminate_task_fn(struct aws_task *task, void *arg, enum aws_task_status task_status) { + (void)task; + + struct aws_mqtt311_listener *listener = arg; + + if (task_status == AWS_TASK_STATUS_RUN_READY) { + struct aws_mqtt_client_connection_311_impl *connection_impl = listener->config.connection->impl; + aws_mqtt311_callback_set_manager_remove(&connection_impl->callback_manager, listener->callback_set_id); + } + + AWS_LOGF_INFO( + AWS_LS_MQTT_GENERAL, + "id=%p: Mqtt311 Listener terminated, listener id=%p", + (void *)listener->config.connection, + (void *)listener); + + s_mqtt311_listener_destroy(listener); +} + +static void s_aws_mqtt311_listener_on_zero_ref_count(void *context) { + struct aws_mqtt311_listener *listener = context; + + aws_event_loop_schedule_task_now( + s_mqtt_client_connection_get_event_loop(listener->config.connection), &listener->terminate_task); +} + +struct aws_mqtt311_listener *aws_mqtt311_listener_new( + struct aws_allocator *allocator, + struct aws_mqtt311_listener_config *config) { + if (config->connection == NULL) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + if (aws_mqtt_client_connection_get_impl_type(config->connection) != AWS_MQTT311_IT_311_CONNECTION) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + struct aws_mqtt311_listener *listener = aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt311_listener)); + + listener->allocator = allocator; + listener->config = *config; + + aws_mqtt_client_connection_acquire(config->connection); + aws_ref_count_init(&listener->ref_count, listener, s_aws_mqtt311_listener_on_zero_ref_count); + + aws_task_init( + &listener->initialize_task, s_mqtt311_listener_initialize_task_fn, listener, "Mqtt311ListenerInitialize"); + aws_task_init( + &listener->terminate_task, s_mqtt311_listener_terminate_task_fn, listener, "Mqtt311ListenerTerminate"); + + aws_mqtt311_listener_acquire(listener); + aws_event_loop_schedule_task_now( + s_mqtt_client_connection_get_event_loop(config->connection), &listener->initialize_task); + + return listener; +} + +struct aws_mqtt311_listener *aws_mqtt311_listener_acquire(struct aws_mqtt311_listener *listener) { + if (listener != NULL) { + aws_ref_count_acquire(&listener->ref_count); + } + + return listener; +} + +struct aws_mqtt311_listener *aws_mqtt311_listener_release(struct aws_mqtt311_listener *listener) { + if (listener != NULL) { + aws_ref_count_release(&listener->ref_count); + } + + return NULL; +} + +struct aws_mqtt311_callback_set_entry { + struct aws_allocator *allocator; + + struct aws_linked_list_node node; + + uint64_t id; + + struct aws_mqtt311_callback_set callbacks; +}; + +void aws_mqtt311_callback_set_manager_init( + struct aws_mqtt311_callback_set_manager *manager, + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *connection) { + + manager->allocator = allocator; + manager->connection = connection; /* no need to ref count, this is assumed to be owned by the client connection */ + manager->next_callback_set_entry_id = 1; + + aws_linked_list_init(&manager->callback_set_entries); +} + +void aws_mqtt311_callback_set_manager_clean_up(struct aws_mqtt311_callback_set_manager *manager) { + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt311_callback_set_entry *entry = + AWS_CONTAINER_OF(node, struct aws_mqtt311_callback_set_entry, node); + node = aws_linked_list_next(node); + + aws_linked_list_remove(&entry->node); + aws_mem_release(entry->allocator, entry); + } +} + +static struct aws_mqtt311_callback_set_entry *s_new_311_callback_set_entry( + struct aws_mqtt311_callback_set_manager *manager, + struct aws_mqtt311_callback_set *callback_set) { + struct aws_mqtt311_callback_set_entry *entry = + aws_mem_calloc(manager->allocator, 1, sizeof(struct aws_mqtt311_callback_set_entry)); + + entry->allocator = manager->allocator; + entry->id = manager->next_callback_set_entry_id++; + entry->callbacks = *callback_set; + + AWS_LOGF_INFO( + AWS_LS_MQTT_GENERAL, + "id=%p: MQTT311 callback manager created new entry id=%" PRIu64, + (void *)manager->connection, + entry->id); + + return entry; +} + +uint64_t aws_mqtt311_callback_set_manager_push_front( + struct aws_mqtt311_callback_set_manager *manager, + struct aws_mqtt311_callback_set *callback_set) { + + AWS_FATAL_ASSERT( + aws_event_loop_thread_is_callers_thread(s_mqtt_client_connection_get_event_loop(manager->connection))); + + struct aws_mqtt311_callback_set_entry *entry = s_new_311_callback_set_entry(manager, callback_set); + + aws_linked_list_push_front(&manager->callback_set_entries, &entry->node); + + return entry->id; +} + +void aws_mqtt311_callback_set_manager_remove( + struct aws_mqtt311_callback_set_manager *manager, + uint64_t callback_set_id) { + + AWS_FATAL_ASSERT( + aws_event_loop_thread_is_callers_thread(s_mqtt_client_connection_get_event_loop(manager->connection))); + + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt311_callback_set_entry *entry = + AWS_CONTAINER_OF(node, struct aws_mqtt311_callback_set_entry, node); + node = aws_linked_list_next(node); + + if (entry->id == callback_set_id) { + aws_linked_list_remove(&entry->node); + + AWS_LOGF_INFO( + AWS_LS_MQTT_GENERAL, + "id=%p: MQTT311 callback manager removed entry id=%" PRIu64, + (void *)manager->connection, + entry->id); + aws_mem_release(entry->allocator, entry); + return; + } + } + AWS_LOGF_INFO( + AWS_LS_MQTT_GENERAL, + "id=%p: MQTT311 callback manager failed to remove entry id=%" PRIu64 ", callback set id not found.", + (void *)manager->connection, + callback_set_id); +} + +void aws_mqtt311_callback_set_manager_on_publish_received( + struct aws_mqtt311_callback_set_manager *manager, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain) { + + struct aws_mqtt_client_connection_311_impl *connection_impl = manager->connection->impl; + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(connection_impl->loop)); + + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt311_callback_set_entry *entry = + AWS_CONTAINER_OF(node, struct aws_mqtt311_callback_set_entry, node); + node = aws_linked_list_next(node); + + struct aws_mqtt311_callback_set *callback_set = &entry->callbacks; + if (callback_set->publish_received_handler != NULL) { + (*callback_set->publish_received_handler)( + manager->connection, topic, payload, dup, qos, retain, callback_set->user_data); + } + } +} + +void aws_mqtt311_callback_set_manager_on_connection_success( + struct aws_mqtt311_callback_set_manager *manager, + enum aws_mqtt_connect_return_code return_code, + bool rejoined_session) { + + struct aws_mqtt_client_connection_311_impl *connection_impl = manager->connection->impl; + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(connection_impl->loop)); + + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt311_callback_set_entry *entry = + AWS_CONTAINER_OF(node, struct aws_mqtt311_callback_set_entry, node); + node = aws_linked_list_next(node); + + struct aws_mqtt311_callback_set *callback_set = &entry->callbacks; + if (callback_set->connection_success_handler != NULL) { + (*callback_set->connection_success_handler)( + manager->connection, return_code, rejoined_session, callback_set->user_data); + } + } +} + +void aws_mqtt311_callback_set_manager_on_connection_interrupted( + struct aws_mqtt311_callback_set_manager *manager, + int error_code) { + + struct aws_mqtt_client_connection_311_impl *connection_impl = manager->connection->impl; + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(connection_impl->loop)); + + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt311_callback_set_entry *entry = + AWS_CONTAINER_OF(node, struct aws_mqtt311_callback_set_entry, node); + node = aws_linked_list_next(node); + + struct aws_mqtt311_callback_set *callback_set = &entry->callbacks; + if (callback_set->connection_interrupted_handler != NULL) { + (*callback_set->connection_interrupted_handler)(manager->connection, error_code, callback_set->user_data); + } + } +} + +void aws_mqtt311_callback_set_manager_on_disconnect(struct aws_mqtt311_callback_set_manager *manager) { + + struct aws_mqtt_client_connection_311_impl *connection_impl = manager->connection->impl; + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(connection_impl->loop)); + + struct aws_linked_list_node *node = aws_linked_list_begin(&manager->callback_set_entries); + while (node != aws_linked_list_end(&manager->callback_set_entries)) { + struct aws_mqtt311_callback_set_entry *entry = + AWS_CONTAINER_OF(node, struct aws_mqtt311_callback_set_entry, node); + node = aws_linked_list_next(node); + + struct aws_mqtt311_callback_set *callback_set = &entry->callbacks; + if (callback_set->disconnect_handler != NULL) { + (*callback_set->disconnect_handler)(manager->connection, callback_set->user_data); + } + } +} diff --git a/source/request-response/protocol_adapter.c b/source/request-response/protocol_adapter.c new file mode 100644 index 00000000..76aea9c4 --- /dev/null +++ b/source/request-response/protocol_adapter.c @@ -0,0 +1,964 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Basic API contract + * + * Invariant 1: Subscribe is only called from the RR subscription manager when going from 0 to 1 pending operations + * Invariant 2: Unsubscribe is only called from the RR subscription manager when there are 0 pending operations, not + * necessarily on the exact transition to zero though. + * + * Additional Notes + * + * Entries are not tracked with the exception of the eventstream impl which needs the stream handles to close. + * + * A subscribe failure does not trigger an unsubscribe, a status event. + * + * The sub manager is responsible for calling Unsubscribe on all its entries when shutting down + * (before releasing hold of the adapter). + * + * Retries, when appropriate, are the responsibility of the caller. + */ + +enum aws_mqtt_protocol_adapter_operation_type { + AMPAOT_SUBSCRIBE_UNSUBSCRIBE, + AMPAOT_PUBLISH, +}; + +struct aws_mqtt_protocol_adapter_sub_unsub_data { + struct aws_byte_buf topic_filter; +}; + +struct aws_mqtt_protocol_adapter_publish_data { + void (*completion_callback_fn)(int, void *); + void *user_data; +}; + +struct aws_mqtt_protocol_adapter_operation_userdata { + struct aws_allocator *allocator; + + struct aws_linked_list_node node; + void *adapter; + + enum aws_mqtt_protocol_adapter_operation_type operation_type; + + union { + struct aws_mqtt_protocol_adapter_sub_unsub_data sub_unsub_data; + struct aws_mqtt_protocol_adapter_publish_data publish_data; + } operation_data; +}; + +static struct aws_mqtt_protocol_adapter_operation_userdata *s_aws_mqtt_protocol_adapter_sub_unsub_data_new( + struct aws_allocator *allocator, + struct aws_byte_cursor topic_filter, + void *adapter) { + + struct aws_mqtt_protocol_adapter_operation_userdata *subscribe_data = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_protocol_adapter_operation_userdata)); + + subscribe_data->allocator = allocator; + subscribe_data->operation_type = AMPAOT_SUBSCRIBE_UNSUBSCRIBE; + subscribe_data->adapter = adapter; + aws_byte_buf_init_copy_from_cursor( + &subscribe_data->operation_data.sub_unsub_data.topic_filter, allocator, topic_filter); + + return subscribe_data; +} + +static struct aws_mqtt_protocol_adapter_operation_userdata *s_aws_mqtt_protocol_adapter_publish_data_new( + struct aws_allocator *allocator, + const struct aws_protocol_adapter_publish_options *publish_options, + void *adapter) { + + struct aws_mqtt_protocol_adapter_operation_userdata *publish_data = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_protocol_adapter_operation_userdata)); + + publish_data->allocator = allocator; + publish_data->operation_type = AMPAOT_PUBLISH; + publish_data->adapter = adapter; + + publish_data->operation_data.publish_data.completion_callback_fn = publish_options->completion_callback_fn; + publish_data->operation_data.publish_data.user_data = publish_options->user_data; + + return publish_data; +} + +static void s_aws_mqtt_protocol_adapter_operation_user_data_destroy( + struct aws_mqtt_protocol_adapter_operation_userdata *userdata) { + if (userdata == NULL) { + return; + } + + if (aws_linked_list_node_next_is_valid(&userdata->node) && aws_linked_list_node_prev_is_valid(&userdata->node)) { + aws_linked_list_remove(&userdata->node); + } + + if (userdata->operation_type == AMPAOT_SUBSCRIBE_UNSUBSCRIBE) { + aws_byte_buf_clean_up(&userdata->operation_data.sub_unsub_data.topic_filter); + } + + aws_mem_release(userdata->allocator, userdata); +} + +/*****************************************************************************************************************/ + +struct aws_mqtt_protocol_adapter_311_impl { + struct aws_allocator *allocator; + struct aws_mqtt_protocol_adapter base; + + struct aws_linked_list incomplete_operations; + struct aws_mqtt_protocol_adapter_options config; + + struct aws_event_loop *loop; + struct aws_mqtt_client_connection *connection; + struct aws_mqtt311_listener *listener; +}; + +static void s_aws_mqtt_protocol_adapter_311_destroy(void *impl) { + struct aws_mqtt_protocol_adapter_311_impl *adapter = impl; + + // all the real cleanup is done in the listener termination callback + aws_mqtt311_listener_release(adapter->listener); +} + +/* Subscribe */ + +static void s_protocol_adapter_311_subscribe_completion( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + int error_code, + void *userdata) { + (void)connection; + (void)topic; + (void)packet_id; + + struct aws_mqtt_protocol_adapter_operation_userdata *subscribe_data = userdata; + struct aws_mqtt_protocol_adapter_311_impl *adapter = subscribe_data->adapter; + + if (adapter == NULL) { + goto done; + } + + if (error_code == AWS_ERROR_SUCCESS) { + if (qos >= 128) { + error_code = AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE; + } + } + + struct aws_protocol_adapter_subscription_event subscribe_event = { + .topic_filter = aws_byte_cursor_from_buf(&subscribe_data->operation_data.sub_unsub_data.topic_filter), + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = error_code, + .retryable = true, + }; + + (*adapter->config.subscription_event_callback)(&subscribe_event, adapter->config.user_data); + +done: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(subscribe_data); +} + +int s_aws_mqtt_protocol_adapter_311_subscribe(void *impl, struct aws_protocol_adapter_subscribe_options *options) { + struct aws_mqtt_protocol_adapter_311_impl *adapter = impl; + struct aws_mqtt_client_connection_311_impl *connection_impl = adapter->connection->impl; + + struct aws_mqtt_protocol_adapter_operation_userdata *subscribe_data = + s_aws_mqtt_protocol_adapter_sub_unsub_data_new(adapter->allocator, options->topic_filter, adapter); + + aws_linked_list_push_back(&adapter->incomplete_operations, &subscribe_data->node); + + uint64_t timeout_nanos = + aws_timestamp_convert(options->ack_timeout_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + if (aws_mqtt_client_connection_311_subscribe( + connection_impl, + &options->topic_filter, + AWS_MQTT_QOS_AT_LEAST_ONCE, + NULL, + NULL, + NULL, + s_protocol_adapter_311_subscribe_completion, + subscribe_data, + timeout_nanos) == 0) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(subscribe_data); + + return AWS_OP_ERR; +} + +/* Unsubscribe */ + +static bool s_is_retryable_unsubscribe311(int error_code) { + return error_code == AWS_ERROR_MQTT_TIMEOUT; +} + +static void s_protocol_adapter_311_unsubscribe_completion( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + int error_code, + void *userdata) { + (void)connection; + (void)packet_id; + + struct aws_mqtt_protocol_adapter_operation_userdata *unsubscribe_data = userdata; + struct aws_mqtt_protocol_adapter_311_impl *adapter = unsubscribe_data->adapter; + + if (adapter == NULL) { + goto done; + } + + struct aws_protocol_adapter_subscription_event unsubscribe_event = { + .topic_filter = aws_byte_cursor_from_buf(&unsubscribe_data->operation_data.sub_unsub_data.topic_filter), + .event_type = AWS_PASET_UNSUBSCRIBE, + .error_code = error_code, + .retryable = s_is_retryable_unsubscribe311(error_code), + }; + + (*adapter->config.subscription_event_callback)(&unsubscribe_event, adapter->config.user_data); + +done: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(unsubscribe_data); +} + +int s_aws_mqtt_protocol_adapter_311_unsubscribe(void *impl, struct aws_protocol_adapter_unsubscribe_options *options) { + struct aws_mqtt_protocol_adapter_311_impl *adapter = impl; + struct aws_mqtt_client_connection_311_impl *connection_impl = adapter->connection->impl; + + struct aws_mqtt_protocol_adapter_operation_userdata *unsubscribe_data = + s_aws_mqtt_protocol_adapter_sub_unsub_data_new(adapter->allocator, options->topic_filter, adapter); + + aws_linked_list_push_back(&adapter->incomplete_operations, &unsubscribe_data->node); + + uint64_t timeout_nanos = + aws_timestamp_convert(options->ack_timeout_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + if (aws_mqtt_client_connection_311_unsubscribe( + connection_impl, + &options->topic_filter, + s_protocol_adapter_311_unsubscribe_completion, + unsubscribe_data, + timeout_nanos) == 0) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(unsubscribe_data); + + return AWS_OP_ERR; +} + +/* Publish */ + +static void s_protocol_adapter_311_publish_completion( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + int error_code, + void *userdata) { + + (void)connection; + (void)packet_id; + + struct aws_mqtt_protocol_adapter_operation_userdata *publish_data = userdata; + struct aws_mqtt_protocol_adapter_311_impl *adapter = publish_data->adapter; + + if (adapter == NULL) { + goto done; + } + + (*publish_data->operation_data.publish_data.completion_callback_fn)( + error_code, publish_data->operation_data.publish_data.user_data); + +done: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(publish_data); +} + +int s_aws_mqtt_protocol_adapter_311_publish(void *impl, struct aws_protocol_adapter_publish_options *options) { + struct aws_mqtt_protocol_adapter_311_impl *adapter = impl; + struct aws_mqtt_client_connection_311_impl *connection_impl = adapter->connection->impl; + + struct aws_mqtt_protocol_adapter_operation_userdata *publish_data = + s_aws_mqtt_protocol_adapter_publish_data_new(adapter->allocator, options, adapter); + + aws_linked_list_push_back(&adapter->incomplete_operations, &publish_data->node); + + uint64_t timeout_nanos = + aws_timestamp_convert(options->ack_timeout_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + if (aws_mqtt_client_connection_311_publish( + connection_impl, + &options->topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &options->payload, + s_protocol_adapter_311_publish_completion, + publish_data, + timeout_nanos) == 0) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(publish_data); + + return AWS_OP_ERR; +} + +static void s_protocol_adapter_mqtt311_listener_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + + (void)connection; + (void)dup; + (void)qos; + (void)retain; + + struct aws_mqtt_protocol_adapter_311_impl *adapter = userdata; + + struct aws_protocol_adapter_incoming_publish_event publish_event = { + .topic = *topic, + .payload = *payload, + }; + + (*adapter->config.incoming_publish_callback)(&publish_event, adapter->config.user_data); +} + +static void s_protocol_adapter_mqtt311_listener_connection_success( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + (void)connection; + (void)return_code; + + struct aws_mqtt_protocol_adapter_311_impl *adapter = userdata; + + if (adapter->config.connection_event_callback != NULL) { + struct aws_protocol_adapter_connection_event connection_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = session_present, + }; + + (*adapter->config.connection_event_callback)(&connection_event, adapter->config.user_data); + } +} + +static void s_protocol_adapter_mqtt311_emit_disconnect_event(struct aws_mqtt_protocol_adapter_311_impl *adapter) { + if (adapter->config.connection_event_callback != NULL) { + struct aws_protocol_adapter_connection_event connection_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + + (*adapter->config.connection_event_callback)(&connection_event, adapter->config.user_data); + } +} + +static void s_protocol_adapter_mqtt311_listener_connection_interrupted( + struct aws_mqtt_client_connection *connection, + int error_code, + void *userdata) { + (void)connection; + (void)error_code; + + s_protocol_adapter_mqtt311_emit_disconnect_event(userdata); +} + +static void s_aws_mqtt_protocol_adapter_311_disconnect_fn( + struct aws_mqtt_client_connection *connection, + void *userdata) { + (void)connection; + + s_protocol_adapter_mqtt311_emit_disconnect_event(userdata); +} + +static bool s_aws_mqtt_protocol_adapter_311_is_connected(void *impl) { + struct aws_mqtt_protocol_adapter_311_impl *adapter = impl; + struct aws_mqtt_client_connection_311_impl *connection_impl = adapter->connection->impl; + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(connection_impl->loop)); + + mqtt_connection_lock_synced_data(connection_impl); + enum aws_mqtt_client_connection_state current_state = connection_impl->synced_data.state; + mqtt_connection_unlock_synced_data(connection_impl); + + return current_state == AWS_MQTT_CLIENT_STATE_CONNECTED; +} + +static void s_release_incomplete_operations(struct aws_linked_list *incomplete_operations) { + struct aws_linked_list dummy_list; + aws_linked_list_init(&dummy_list); + aws_linked_list_swap_contents(incomplete_operations, &dummy_list); + + while (!aws_linked_list_empty(&dummy_list)) { + struct aws_linked_list_node *head = aws_linked_list_pop_front(&dummy_list); + struct aws_mqtt_protocol_adapter_operation_userdata *userdata = + AWS_CONTAINER_OF(head, struct aws_mqtt_protocol_adapter_operation_userdata, node); + + userdata->adapter = NULL; + + if (userdata->operation_type == AMPAOT_PUBLISH) { + struct aws_mqtt_protocol_adapter_publish_data *publish_data = &userdata->operation_data.publish_data; + if (publish_data->completion_callback_fn != NULL) { + (*userdata->operation_data.publish_data.completion_callback_fn)( + AWS_ERROR_MQTT_REQUEST_RESPONSE_CLIENT_SHUT_DOWN, publish_data->user_data); + } + } + } +} + +static void s_protocol_adapter_mqtt311_listener_termination_callback(void *user_data) { + struct aws_mqtt_protocol_adapter_311_impl *adapter = user_data; + struct aws_mqtt_client_connection_311_impl *impl = adapter->connection->impl; + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(impl->loop)); + + s_release_incomplete_operations(&adapter->incomplete_operations); + + aws_mqtt_client_connection_release(adapter->connection); + + aws_protocol_adapter_terminate_callback_fn *terminate_callback = adapter->config.terminate_callback; + void *terminate_user_data = adapter->config.user_data; + + aws_mem_release(adapter->allocator, adapter); + + if (terminate_callback) { + (*terminate_callback)(terminate_user_data); + } +} + +static struct aws_event_loop *s_aws_mqtt_protocol_adapter_311_get_event_loop(void *impl) { + struct aws_mqtt_protocol_adapter_311_impl *adapter = impl; + + return adapter->loop; +} + +static struct aws_mqtt_protocol_adapter_vtable s_protocol_adapter_mqtt311_vtable = { + .aws_mqtt_protocol_adapter_destroy_fn = s_aws_mqtt_protocol_adapter_311_destroy, + .aws_mqtt_protocol_adapter_subscribe_fn = s_aws_mqtt_protocol_adapter_311_subscribe, + .aws_mqtt_protocol_adapter_unsubscribe_fn = s_aws_mqtt_protocol_adapter_311_unsubscribe, + .aws_mqtt_protocol_adapter_publish_fn = s_aws_mqtt_protocol_adapter_311_publish, + .aws_mqtt_protocol_adapter_is_connected_fn = s_aws_mqtt_protocol_adapter_311_is_connected, + .aws_mqtt_protocol_adapter_get_event_loop_fn = s_aws_mqtt_protocol_adapter_311_get_event_loop, +}; + +struct aws_mqtt_protocol_adapter *aws_mqtt_protocol_adapter_new_from_311( + struct aws_allocator *allocator, + struct aws_mqtt_protocol_adapter_options *options, + struct aws_mqtt_client_connection *connection) { + + if (options == NULL || connection == NULL) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + if (aws_mqtt_client_connection_get_impl_type(connection) != AWS_MQTT311_IT_311_CONNECTION) { + struct aws_mqtt_client_connection_5_impl *adapter_impl = connection->impl; + return aws_mqtt_protocol_adapter_new_from_5(allocator, options, adapter_impl->client); + } + + struct aws_mqtt_client_connection_311_impl *impl = connection->impl; + + struct aws_mqtt_protocol_adapter_311_impl *adapter = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_protocol_adapter_311_impl)); + + adapter->allocator = allocator; + adapter->base.impl = adapter; + adapter->base.vtable = &s_protocol_adapter_mqtt311_vtable; + aws_linked_list_init(&adapter->incomplete_operations); + adapter->config = *options; + adapter->loop = impl->loop; + adapter->connection = aws_mqtt_client_connection_acquire(connection); + + struct aws_mqtt311_listener_config listener_options = { + .connection = connection, + .listener_callbacks = + { + .publish_received_handler = s_protocol_adapter_mqtt311_listener_publish_received, + .connection_success_handler = s_protocol_adapter_mqtt311_listener_connection_success, + .connection_interrupted_handler = s_protocol_adapter_mqtt311_listener_connection_interrupted, + .disconnect_handler = s_aws_mqtt_protocol_adapter_311_disconnect_fn, + .user_data = adapter, + }, + .termination_callback = s_protocol_adapter_mqtt311_listener_termination_callback, + .termination_callback_user_data = adapter, + }; + + adapter->listener = aws_mqtt311_listener_new(allocator, &listener_options); + + return &adapter->base; +} + +/******************************************************************************************************************/ + +struct aws_mqtt_protocol_adapter_5_impl { + struct aws_allocator *allocator; + struct aws_mqtt_protocol_adapter base; + struct aws_linked_list incomplete_operations; + struct aws_mqtt_protocol_adapter_options config; + + struct aws_event_loop *loop; + struct aws_mqtt5_client *client; + struct aws_mqtt5_listener *listener; +}; + +static void s_aws_mqtt_protocol_adapter_5_destroy(void *impl) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = impl; + + // all the real cleanup is done in the listener termination callback + aws_mqtt5_listener_release(adapter->listener); +} + +/* Subscribe */ + +static bool s_is_retryable_subscribe(enum aws_mqtt5_suback_reason_code reason_code, int error_code) { + if (error_code == AWS_ERROR_MQTT5_PACKET_VALIDATION || error_code == AWS_ERROR_MQTT5_SUBSCRIBE_OPTIONS_VALIDATION) { + return false; + } else if (error_code != AWS_ERROR_SUCCESS) { + return true; + } + + switch (reason_code) { + case AWS_MQTT5_SARC_GRANTED_QOS_0: + case AWS_MQTT5_SARC_GRANTED_QOS_1: + case AWS_MQTT5_SARC_GRANTED_QOS_2: + case AWS_MQTT5_SARC_UNSPECIFIED_ERROR: + case AWS_MQTT5_SARC_PACKET_IDENTIFIER_IN_USE: + case AWS_MQTT5_SARC_IMPLEMENTATION_SPECIFIC_ERROR: + case AWS_MQTT5_SARC_QUOTA_EXCEEDED: + return true; + + default: + return false; + } +} + +static void s_protocol_adapter_5_subscribe_completion( + const struct aws_mqtt5_packet_suback_view *suback, + int error_code, + void *complete_ctx) { + struct aws_mqtt_protocol_adapter_operation_userdata *subscribe_data = complete_ctx; + struct aws_mqtt_protocol_adapter_5_impl *adapter = subscribe_data->adapter; + + if (adapter == NULL) { + goto done; + } + + enum aws_mqtt5_suback_reason_code reason_code = AWS_MQTT5_SARC_GRANTED_QOS_0; + if (suback != NULL && suback->reason_code_count > 0) { + reason_code = suback->reason_codes[0]; + } + bool is_retryable = s_is_retryable_subscribe(reason_code, error_code); + + if (error_code == AWS_ERROR_SUCCESS) { + if (suback == NULL || suback->reason_code_count != 1 || suback->reason_codes[0] >= 128) { + error_code = AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE; + } + } + + struct aws_protocol_adapter_subscription_event subscribe_event = { + .topic_filter = aws_byte_cursor_from_buf(&subscribe_data->operation_data.sub_unsub_data.topic_filter), + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = error_code, + .retryable = is_retryable, + }; + + (*adapter->config.subscription_event_callback)(&subscribe_event, adapter->config.user_data); + +done: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(subscribe_data); +} + +int s_aws_mqtt_protocol_adapter_5_subscribe(void *impl, struct aws_protocol_adapter_subscribe_options *options) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = impl; + + struct aws_mqtt_protocol_adapter_operation_userdata *subscribe_data = + s_aws_mqtt_protocol_adapter_sub_unsub_data_new(adapter->allocator, options->topic_filter, adapter); + + aws_linked_list_push_back(&adapter->incomplete_operations, &subscribe_data->node); + + struct aws_mqtt5_subscription_view subscription_view = { + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .topic_filter = options->topic_filter, + }; + + struct aws_mqtt5_packet_subscribe_view subscribe_view = { + .subscriptions = &subscription_view, + .subscription_count = 1, + }; + + struct aws_mqtt5_subscribe_completion_options completion_options = { + .ack_timeout_seconds_override = options->ack_timeout_seconds, + .completion_callback = s_protocol_adapter_5_subscribe_completion, + .completion_user_data = subscribe_data, + }; + + if (aws_mqtt5_client_subscribe(adapter->client, &subscribe_view, &completion_options)) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(subscribe_data); + + return AWS_OP_ERR; +} + +/* Unsubscribe */ + +static bool s_is_retryable_unsubscribe5(enum aws_mqtt5_unsuback_reason_code reason_code, int error_code) { + if (error_code == AWS_ERROR_MQTT5_PACKET_VALIDATION || + error_code == AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION) { + return false; + } else if (error_code == AWS_ERROR_MQTT_TIMEOUT) { + return true; + } + + switch (reason_code) { + case AWS_MQTT5_UARC_UNSPECIFIED_ERROR: + case AWS_MQTT5_UARC_IMPLEMENTATION_SPECIFIC_ERROR: + return true; + + default: + return false; + } +} + +static void s_protocol_adapter_5_unsubscribe_completion( + const struct aws_mqtt5_packet_unsuback_view *unsuback, + int error_code, + void *complete_ctx) { + struct aws_mqtt_protocol_adapter_operation_userdata *unsubscribe_data = complete_ctx; + struct aws_mqtt_protocol_adapter_5_impl *adapter = unsubscribe_data->adapter; + + if (adapter == NULL) { + goto done; + } + + enum aws_mqtt5_unsuback_reason_code reason_code = AWS_MQTT5_UARC_SUCCESS; + if (unsuback != NULL && unsuback->reason_code_count > 0) { + reason_code = unsuback->reason_codes[0]; + } + + bool is_retryable = s_is_retryable_unsubscribe5(reason_code, error_code); + + if (error_code == AWS_ERROR_SUCCESS) { + if (unsuback == NULL || unsuback->reason_code_count != 1 || unsuback->reason_codes[0] >= 128) { + error_code = AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE; + } + } + + struct aws_protocol_adapter_subscription_event unsubscribe_event = { + .topic_filter = aws_byte_cursor_from_buf(&unsubscribe_data->operation_data.sub_unsub_data.topic_filter), + .event_type = AWS_PASET_UNSUBSCRIBE, + .error_code = error_code, + .retryable = is_retryable, + }; + + (*adapter->config.subscription_event_callback)(&unsubscribe_event, adapter->config.user_data); + +done: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(unsubscribe_data); +} + +int s_aws_mqtt_protocol_adapter_5_unsubscribe(void *impl, struct aws_protocol_adapter_unsubscribe_options *options) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = impl; + + struct aws_mqtt_protocol_adapter_operation_userdata *unsubscribe_data = + s_aws_mqtt_protocol_adapter_sub_unsub_data_new(adapter->allocator, options->topic_filter, adapter); + + aws_linked_list_push_back(&adapter->incomplete_operations, &unsubscribe_data->node); + + struct aws_mqtt5_packet_unsubscribe_view unsubscribe_view = { + .topic_filters = &options->topic_filter, + .topic_filter_count = 1, + }; + + struct aws_mqtt5_unsubscribe_completion_options completion_options = { + .ack_timeout_seconds_override = options->ack_timeout_seconds, + .completion_callback = s_protocol_adapter_5_unsubscribe_completion, + .completion_user_data = unsubscribe_data, + }; + + if (aws_mqtt5_client_unsubscribe(adapter->client, &unsubscribe_view, &completion_options)) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(unsubscribe_data); + + return AWS_OP_ERR; +} + +/* Publish */ + +static void s_protocol_adapter_5_publish_completion( + enum aws_mqtt5_packet_type packet_type, + const void *packet, + int error_code, + void *complete_ctx) { + struct aws_mqtt_protocol_adapter_operation_userdata *publish_data = complete_ctx; + struct aws_mqtt_protocol_adapter_5_impl *adapter = publish_data->adapter; + + if (adapter == NULL) { + goto done; + } + + if (error_code == AWS_ERROR_SUCCESS && packet_type == AWS_MQTT5_PT_PUBACK) { + const struct aws_mqtt5_packet_puback_view *puback = packet; + if (puback->reason_code >= 128) { + error_code = AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE; + } + } + + (*publish_data->operation_data.publish_data.completion_callback_fn)( + error_code, publish_data->operation_data.publish_data.user_data); + +done: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(publish_data); +} + +int s_aws_mqtt_protocol_adapter_5_publish(void *impl, struct aws_protocol_adapter_publish_options *options) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = impl; + struct aws_mqtt_protocol_adapter_operation_userdata *publish_data = + s_aws_mqtt_protocol_adapter_publish_data_new(adapter->allocator, options, adapter); + + aws_linked_list_push_back(&adapter->incomplete_operations, &publish_data->node); + + struct aws_mqtt5_packet_publish_view publish_view = { + .topic = options->topic, .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, .payload = options->payload}; + + struct aws_mqtt5_publish_completion_options completion_options = { + .ack_timeout_seconds_override = options->ack_timeout_seconds, + .completion_callback = s_protocol_adapter_5_publish_completion, + .completion_user_data = publish_data, + }; + + if (aws_mqtt5_client_publish(adapter->client, &publish_view, &completion_options)) { + goto error; + } + + return AWS_OP_SUCCESS; + +error: + + s_aws_mqtt_protocol_adapter_operation_user_data_destroy(publish_data); + + return AWS_OP_ERR; +} + +static bool s_aws_mqtt_protocol_adapter_5_is_connected(void *impl) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = impl; + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(adapter->client->loop)); + + enum aws_mqtt5_client_state current_state = adapter->client->current_state; + + return current_state == AWS_MCS_CONNECTED; +} + +static bool s_protocol_adapter_mqtt5_listener_publish_received( + const struct aws_mqtt5_packet_publish_view *publish, + void *user_data) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = user_data; + + struct aws_protocol_adapter_incoming_publish_event publish_event = { + .topic = publish->topic, + .payload = publish->payload, + }; + + (*adapter->config.incoming_publish_callback)(&publish_event, adapter->config.user_data); + + return false; +} + +static void s_protocol_adapter_mqtt5_lifecycle_event_callback(const struct aws_mqtt5_client_lifecycle_event *event) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = event->user_data; + + switch (event->event_type) { + case AWS_MQTT5_CLET_CONNECTION_SUCCESS: { + struct aws_protocol_adapter_connection_event connection_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = event->settings->rejoined_session, + }; + + (*adapter->config.connection_event_callback)(&connection_event, adapter->config.user_data); + break; + } + case AWS_MQTT5_CLET_DISCONNECTION: { + struct aws_protocol_adapter_connection_event connection_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + + (*adapter->config.connection_event_callback)(&connection_event, adapter->config.user_data); + break; + } + default: + break; + } +} + +static void s_protocol_adapter_mqtt5_listener_termination_callback(void *user_data) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = user_data; + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(adapter->client->loop)); + + s_release_incomplete_operations(&adapter->incomplete_operations); + + aws_mqtt5_client_release(adapter->client); + + aws_protocol_adapter_terminate_callback_fn *terminate_callback = adapter->config.terminate_callback; + void *terminate_user_data = adapter->config.user_data; + + aws_mem_release(adapter->allocator, adapter); + + if (terminate_callback) { + (*terminate_callback)(terminate_user_data); + } +} + +static struct aws_event_loop *s_aws_mqtt_protocol_adapter_5_get_event_loop(void *impl) { + struct aws_mqtt_protocol_adapter_5_impl *adapter = impl; + + return adapter->loop; +} + +static struct aws_mqtt_protocol_adapter_vtable s_protocol_adapter_mqtt5_vtable = { + .aws_mqtt_protocol_adapter_destroy_fn = s_aws_mqtt_protocol_adapter_5_destroy, + .aws_mqtt_protocol_adapter_subscribe_fn = s_aws_mqtt_protocol_adapter_5_subscribe, + .aws_mqtt_protocol_adapter_unsubscribe_fn = s_aws_mqtt_protocol_adapter_5_unsubscribe, + .aws_mqtt_protocol_adapter_publish_fn = s_aws_mqtt_protocol_adapter_5_publish, + .aws_mqtt_protocol_adapter_is_connected_fn = s_aws_mqtt_protocol_adapter_5_is_connected, + .aws_mqtt_protocol_adapter_get_event_loop_fn = s_aws_mqtt_protocol_adapter_5_get_event_loop}; + +struct aws_mqtt_protocol_adapter *aws_mqtt_protocol_adapter_new_from_5( + struct aws_allocator *allocator, + struct aws_mqtt_protocol_adapter_options *options, + struct aws_mqtt5_client *client) { + + if (options == NULL || client == NULL) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + struct aws_mqtt_protocol_adapter_5_impl *adapter = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_protocol_adapter_5_impl)); + + adapter->allocator = allocator; + adapter->base.impl = adapter; + adapter->base.vtable = &s_protocol_adapter_mqtt5_vtable; + aws_linked_list_init(&adapter->incomplete_operations); + adapter->config = *options; + adapter->loop = client->loop; + adapter->client = aws_mqtt5_client_acquire(client); + + struct aws_mqtt5_listener_config listener_options = { + .client = client, + .listener_callbacks = + { + .listener_publish_received_handler = s_protocol_adapter_mqtt5_listener_publish_received, + .listener_publish_received_handler_user_data = adapter, + .lifecycle_event_handler = s_protocol_adapter_mqtt5_lifecycle_event_callback, + .lifecycle_event_handler_user_data = adapter, + }, + .termination_callback = s_protocol_adapter_mqtt5_listener_termination_callback, + .termination_callback_user_data = adapter, + }; + + adapter->listener = aws_mqtt5_listener_new(allocator, &listener_options); + + return &adapter->base; +} + +void aws_mqtt_protocol_adapter_destroy(struct aws_mqtt_protocol_adapter *adapter) { + (*adapter->vtable->aws_mqtt_protocol_adapter_destroy_fn)(adapter->impl); +} + +int aws_mqtt_protocol_adapter_subscribe( + struct aws_mqtt_protocol_adapter *adapter, + struct aws_protocol_adapter_subscribe_options *options) { + return (*adapter->vtable->aws_mqtt_protocol_adapter_subscribe_fn)(adapter->impl, options); +} + +int aws_mqtt_protocol_adapter_unsubscribe( + struct aws_mqtt_protocol_adapter *adapter, + struct aws_protocol_adapter_unsubscribe_options *options) { + return (*adapter->vtable->aws_mqtt_protocol_adapter_unsubscribe_fn)(adapter->impl, options); +} + +int aws_mqtt_protocol_adapter_publish( + struct aws_mqtt_protocol_adapter *adapter, + struct aws_protocol_adapter_publish_options *options) { + return (*adapter->vtable->aws_mqtt_protocol_adapter_publish_fn)(adapter->impl, options); +} + +bool aws_mqtt_protocol_adapter_is_connected(struct aws_mqtt_protocol_adapter *adapter) { + return (*adapter->vtable->aws_mqtt_protocol_adapter_is_connected_fn)(adapter->impl); +} + +struct aws_event_loop *aws_mqtt_protocol_adapter_get_event_loop(struct aws_mqtt_protocol_adapter *adapter) { + return (*adapter->vtable->aws_mqtt_protocol_adapter_get_event_loop_fn)(adapter->impl); +} + +const char *aws_protocol_adapter_subscription_event_type_to_c_str( + enum aws_protocol_adapter_subscription_event_type type) { + switch (type) { + case AWS_PASET_SUBSCRIBE: + return "Subscribe"; + + case AWS_PASET_UNSUBSCRIBE: + return "Unsubscribe"; + } + + return "Unknown"; +} + +const char *aws_protocol_adapter_connection_event_type_to_c_str(enum aws_protocol_adapter_connection_event_type type) { + switch (type) { + case AWS_PACET_CONNECTED: + return "Connected"; + + case AWS_PACET_DISCONNECTED: + return "Disconnected"; + } + + return "Unknown"; +} diff --git a/source/request-response/request_response_client.c b/source/request-response/request_response_client.c new file mode 100644 index 00000000..eeb4ffc8 --- /dev/null +++ b/source/request-response/request_response_client.c @@ -0,0 +1,2276 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define MQTT_RR_CLIENT_OPERATION_TABLE_DEFAULT_SIZE 50 +#define MQTT_RR_CLIENT_RESPONSE_TABLE_DEFAULT_SIZE 50 + +struct aws_mqtt_request_operation_storage { + struct aws_mqtt_request_operation_options options; + + struct aws_array_list operation_response_paths; + struct aws_array_list subscription_topic_filters; + + struct aws_byte_buf operation_data; +}; + +struct aws_mqtt_streaming_operation_storage { + struct aws_mqtt_streaming_operation_options options; + + struct aws_byte_buf operation_data; + + struct aws_atomic_var activated; +}; + +enum aws_mqtt_request_response_operation_type { + AWS_MRROT_REQUEST, + AWS_MRROT_STREAMING, +}; + +enum aws_mqtt_request_response_operation_state { + /* creation -> in event loop enqueue */ + AWS_MRROS_NONE, + + /* in event loop queue -> non blocked response from subscription manager */ + AWS_MRROS_QUEUED, + + /* subscribing response from sub manager -> subscription success/failure event */ + AWS_MRROS_PENDING_SUBSCRIPTION, + + /* (request only) subscription success -> (publish failure OR correlated response received) */ + AWS_MRROS_PENDING_RESPONSE, + + /* (streaming only) subscription success -> (operation finished OR subscription ended event) */ + AWS_MRROS_SUBSCRIBED, + + /* (streaming only) (subscription failure OR subscription ended) -> operation close/terminate */ + AWS_MRROS_TERMINAL, + + /* (request only) the operation's destroy task has been scheduled but not yet executed */ + AWS_MRROS_PENDING_DESTROY, +}; + +const char *s_aws_mqtt_request_response_operation_state_to_c_str(enum aws_mqtt_request_response_operation_state state) { + switch (state) { + case AWS_MRROS_NONE: + return "NONE"; + + case AWS_MRROS_QUEUED: + return "QUEUED"; + + case AWS_MRROS_PENDING_SUBSCRIPTION: + return "PENDING_SUBSCRIPTION"; + + case AWS_MRROS_PENDING_RESPONSE: + return "PENDING_RESPONSE"; + + case AWS_MRROS_SUBSCRIBED: + return "SUBSCRIBED"; + + case AWS_MRROS_TERMINAL: + return "TERMINAL"; + + case AWS_MRROS_PENDING_DESTROY: + return "PENDING_DESTROY"; + + default: + return "Unknown"; + } +} + +const char *s_aws_acquire_subscription_result_type(enum aws_acquire_subscription_result_type result) { + switch (result) { + case AASRT_SUBSCRIBED: + return "SUBSCRIBED"; + + case AASRT_SUBSCRIBING: + return "SUBSCRIBING"; + + case AASRT_BLOCKED: + return "BLOCKED"; + + case AASRT_NO_CAPACITY: + return "NO_CAPACITY"; + + case AASRT_FAILURE: + return "FAILURE"; + + default: + return "Unknown"; + } +} + +/* + +Client Tables/Lookups + + (operations: authoritative operation container) + 1. &operation.id -> &operation // added on in-thread enqueue, removed on operation completion/destruction + + (request_response_paths: response path topic -> Correlation token extraction info) + 2. &topic -> &{topic, topic_buffer, correlation token json path buffer} // ref-counted, per-message-path add on + request dequeue into subscribing/subscribed state, decref/removed on operation completion/destruction + + (operations_by_correlation_tokens: correlationToken -> request operation) + 3. &operation.correlation token -> (request) &operation // added on request dequeue into subscribing/subscribed +state, removed on operation completion/destruction + + (streaming_operation_subscription_lists: streaming subscription filter -> list of all operations using that filter) + 4. &topic_filter -> &{topic_filter, linked_list} // added on request dequeue into subscribing/subscribed state, + removed from list on operation completion/destruction also checked for empty and removed from table + +*/ + +/* + * This is the (key and) value in hash table (4) above. + */ +struct aws_rr_operation_list_topic_filter_entry { + struct aws_allocator *allocator; + + struct aws_byte_cursor topic_filter_cursor; + struct aws_byte_buf topic_filter; + + struct aws_linked_list operations; +}; + +static struct aws_rr_operation_list_topic_filter_entry *s_aws_rr_operation_list_topic_filter_entry_new( + struct aws_allocator *allocator, + struct aws_byte_cursor topic_filter) { + struct aws_rr_operation_list_topic_filter_entry *entry = + aws_mem_calloc(allocator, 1, sizeof(struct aws_rr_operation_list_topic_filter_entry)); + + entry->allocator = allocator; + aws_byte_buf_init_copy_from_cursor(&entry->topic_filter, allocator, topic_filter); + entry->topic_filter_cursor = aws_byte_cursor_from_buf(&entry->topic_filter); + + aws_linked_list_init(&entry->operations); + + return entry; +} + +static void s_aws_rr_operation_list_topic_filter_entry_destroy(struct aws_rr_operation_list_topic_filter_entry *entry) { + if (entry == NULL) { + return; + } + + aws_byte_buf_clean_up(&entry->topic_filter); + + aws_mem_release(entry->allocator, entry); +} + +static void s_aws_rr_operation_list_topic_filter_entry_hash_element_destroy(void *value) { + s_aws_rr_operation_list_topic_filter_entry_destroy(value); +} + +struct aws_rr_response_path_entry { + struct aws_allocator *allocator; + + size_t ref_count; + + struct aws_byte_cursor topic_cursor; + struct aws_byte_buf topic; + + struct aws_byte_buf correlation_token_json_path; +}; + +static struct aws_rr_response_path_entry *s_aws_rr_response_path_entry_new( + struct aws_allocator *allocator, + struct aws_byte_cursor topic, + struct aws_byte_cursor correlation_token_json_path) { + struct aws_rr_response_path_entry *entry = aws_mem_calloc(allocator, 1, sizeof(struct aws_rr_response_path_entry)); + + entry->allocator = allocator; + entry->ref_count = 1; + aws_byte_buf_init_copy_from_cursor(&entry->topic, allocator, topic); + entry->topic_cursor = aws_byte_cursor_from_buf(&entry->topic); + + aws_byte_buf_init_copy_from_cursor(&entry->correlation_token_json_path, allocator, correlation_token_json_path); + + return entry; +} + +static void s_aws_rr_response_path_entry_destroy(struct aws_rr_response_path_entry *entry) { + if (entry == NULL) { + return; + } + + aws_byte_buf_clean_up(&entry->topic); + aws_byte_buf_clean_up(&entry->correlation_token_json_path); + + aws_mem_release(entry->allocator, entry); +} + +static void s_aws_rr_response_path_table_hash_element_destroy(void *value) { + s_aws_rr_response_path_entry_destroy(value); +} + +struct aws_mqtt_rr_client_operation { + struct aws_allocator *allocator; + + /* + * Operation ref-counting is a bit tricky and un-intuitive because it differs based on the type of operation. + * + * Streaming operations are managed by the user, and so the ref count is their responsibility to drop to zero. + * Dropping a streaming operation's ref count to zero schedules a task on the client event loop to destroy the + * operation. It is expected that the binding client will track (with proper synchronization) all unclosed + * streaming operations and safely close them for the user when close is called on the binding client. + * + * Request operations are managed by the client, and so the ref count is dropped to zero when either the + * operation completes normally (success or failure) or when the client is shutdown due to its external ref + * count dropping to zero. In all cases, this event happens naturally on the client event loop. + * + * So the summary is: + * + * (1) Streaming operation clean up is initiated by the user calling dec ref on the streaming operation + * (2) Request operation clean up is initiated by normal completion or client shutdown invoking dec ref. + * + * The upshot is that client shutdown dec-refs request operations but not streaming operations. + */ + struct aws_ref_count ref_count; + + struct aws_mqtt_request_response_client *client_internal_ref; + + uint64_t id; + + enum aws_mqtt_request_response_operation_type type; + + union { + struct aws_mqtt_streaming_operation_storage streaming_storage; + struct aws_mqtt_request_operation_storage request_storage; + } storage; + + uint64_t timeout_timepoint_ns; + struct aws_priority_queue_node priority_queue_node; + + /* Sometimes this is client->operation_queue, other times it is an entry in the client's topic_filter table */ + struct aws_linked_list_node node; + + enum aws_mqtt_request_response_operation_state state; + + size_t pending_subscriptions; + + bool in_client_tables; + + struct aws_task submit_task; + struct aws_task destroy_task; +}; + +/*******************************************************************************************/ + +/* Tracks the current state of the request-response client */ +enum aws_request_response_client_state { + + /* cross-thread initialization has not completed and all protocol adapter callbacks are ignored */ + AWS_RRCS_UNINITIALIZED, + + /* Normal operating state for the client. */ + AWS_RRCS_ACTIVE, + + /* asynchronously shutting down, no more servicing will be done and all protocol adapter callbacks are ignored */ + AWS_RRCS_SHUTTING_DOWN, +}; + +/* + * Request-Response Client Notes + * + * Ref-counting/Shutdown + * + * The request-response client uses a double ref-count pattern. + * + * External references represent user references. When the external reference reaches zero, the client's asynchronous + * shutdown process is started. + * + * Internal references block final destruction. Asynchronous shutdown will not complete until all internal references + * are dropped. In addition to one long-lived internal reference (the protocol client adapter's back reference to + * the request-response client), all event loop tasks that target the request-response client hold an internal + * reference between task submission and task completion. This ensures that the task always has a valid reference + * to the client, even if we're trying to shut down at the same time. + * + * + * Initialization + * + * Initialization is complicated by the fact that the subscription manager needs to be initialized from the + * event loop thread that the client/protocol adapter/protocol client are all seated on. To do this safely, + * we add an uninitialized state that ignores all callbacks and we schedule a task on initial construction to do + * the event-loop-only initialization. Once that initialization completes on the event loop thread, we move + * the client into an active state where it will process operations and protocol adapter callbacks. + */ +struct aws_mqtt_request_response_client { + struct aws_allocator *allocator; + + struct aws_ref_count external_ref_count; + struct aws_ref_count internal_ref_count; + + struct aws_mqtt_request_response_client_options config; + + struct aws_mqtt_protocol_adapter *client_adapter; + + struct aws_rr_subscription_manager subscription_manager; + + struct aws_event_loop *loop; + + struct aws_task initialize_task; + struct aws_task external_shutdown_task; + struct aws_task internal_shutdown_task; + + uint64_t scheduled_service_timepoint_ns; + struct aws_task service_task; + + enum aws_request_response_client_state state; + + struct aws_atomic_var next_id; + + struct aws_linked_list operation_queue; + + /* &operation->id -> &operation */ + struct aws_hash_table operations; + + /* + * heap of operation pointers where the timeout is the sort value. Elements are added to this on operation + * submission and removed on operation timeout/completion/termination. Request-response operations have actual + * timeouts, while streaming operations have UINT64_MAX timeouts. + */ + struct aws_priority_queue operations_by_timeout; + + /* + * Map from cursor (topic filter) -> list of streaming operations using that filter + */ + struct aws_hash_table streaming_operation_subscription_lists; + + /* + * Map from cursor (topic) -> request response path (topic, correlation token json path) + * + * We don't garbage collect this table over the course of normal client operation. We only clean it up + * when the client is shutting down. + */ + struct aws_hash_table request_response_paths; + + /* + * Map from cursor (correlation token) -> request operation + */ + struct aws_hash_table operations_by_correlation_tokens; +}; + +struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_acquire_internal( + struct aws_mqtt_request_response_client *client) { + if (client != NULL) { + aws_ref_count_acquire(&client->internal_ref_count); + } + + return client; +} + +struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_release_internal( + struct aws_mqtt_request_response_client *client) { + if (client != NULL) { + aws_ref_count_release(&client->internal_ref_count); + } + + return NULL; +} + +static void s_aws_rr_client_on_zero_internal_ref_count(void *context) { + struct aws_mqtt_request_response_client *client = context; + + /* Both ref counts are zero, but it's still safest to schedule final destruction, not invoke it directly */ + aws_event_loop_schedule_task_now(client->loop, &client->internal_shutdown_task); +} + +static void s_aws_rr_client_on_zero_external_ref_count(void *context) { + struct aws_mqtt_request_response_client *client = context; + + /* Start the asynchronous shutdown process */ + aws_event_loop_schedule_task_now(client->loop, &client->external_shutdown_task); +} + +static void s_mqtt_request_response_client_final_destroy(struct aws_mqtt_request_response_client *client) { + aws_mqtt_request_response_client_terminated_callback_fn *terminate_callback = client->config.terminated_callback; + void *user_data = client->config.user_data; + + AWS_FATAL_ASSERT(aws_hash_table_get_entry_count(&client->operations) == 0); + aws_hash_table_clean_up(&client->operations); + + aws_priority_queue_clean_up(&client->operations_by_timeout); + aws_hash_table_clean_up(&client->streaming_operation_subscription_lists); + aws_hash_table_clean_up(&client->request_response_paths); + aws_hash_table_clean_up(&client->operations_by_correlation_tokens); + + aws_mem_release(client->allocator, client); + + if (terminate_callback != NULL) { + (*terminate_callback)(user_data); + } +} + +static void s_mqtt_request_response_client_internal_shutdown_task_fn( + struct aws_task *task, + void *arg, + enum aws_task_status task_status) { + (void)task; + (void)task_status; + + struct aws_mqtt_request_response_client *client = arg; + + /* All internal and external refs are gone; it is safe to clean up synchronously. */ + s_mqtt_request_response_client_final_destroy(client); +} + +static void s_remove_operation_from_timeout_queue(struct aws_mqtt_rr_client_operation *operation) { + struct aws_mqtt_request_response_client *client = operation->client_internal_ref; + + if (aws_priority_queue_node_is_in_queue(&operation->priority_queue_node)) { + struct aws_mqtt_rr_client_operation *queued_operation = NULL; + aws_priority_queue_remove(&client->operations_by_timeout, &queued_operation, &operation->priority_queue_node); + } +} + +static void s_change_operation_state( + struct aws_mqtt_rr_client_operation *operation, + enum aws_mqtt_request_response_operation_state new_state) { + enum aws_mqtt_request_response_operation_state old_state = operation->state; + if (old_state == new_state) { + return; + } + + operation->state = new_state; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response operation %" PRIu64 " changing state from %s to %s", + (void *)operation->client_internal_ref, + operation->id, + s_aws_mqtt_request_response_operation_state_to_c_str(old_state), + s_aws_mqtt_request_response_operation_state_to_c_str(new_state)); +} + +static void s_complete_request_operation_with_failure(struct aws_mqtt_rr_client_operation *operation, int error_code) { + AWS_FATAL_ASSERT(operation->type == AWS_MRROT_REQUEST); + AWS_FATAL_ASSERT(error_code != AWS_ERROR_SUCCESS); + + if (operation->state == AWS_MRROS_PENDING_DESTROY) { + return; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response operation %" PRIu64 " failed with error code %d(%s)", + (void *)operation->client_internal_ref, + operation->id, + error_code, + aws_error_debug_str(error_code)); + + aws_mqtt_request_operation_completion_fn *completion_callback = + operation->storage.request_storage.options.completion_callback; + void *user_data = operation->storage.request_storage.options.user_data; + + if (completion_callback != NULL) { + (*completion_callback)(NULL, NULL, error_code, user_data); + } + + s_change_operation_state(operation, AWS_MRROS_PENDING_DESTROY); + + aws_mqtt_rr_client_operation_release(operation); +} + +static void s_streaming_operation_emit_streaming_subscription_event( + struct aws_mqtt_rr_client_operation *operation, + enum aws_rr_streaming_subscription_event_type event_type, + int error_code) { + aws_mqtt_streaming_operation_subscription_status_fn *subscription_status_callback = + operation->storage.streaming_storage.options.subscription_status_callback; + + if (subscription_status_callback != NULL) { + void *user_data = operation->storage.streaming_storage.options.user_data; + (*subscription_status_callback)(event_type, error_code, user_data); + } +} + +static void s_halt_streaming_operation_with_failure(struct aws_mqtt_rr_client_operation *operation, int error_code) { + AWS_FATAL_ASSERT(operation->type == AWS_MRROT_STREAMING); + AWS_FATAL_ASSERT(error_code != AWS_ERROR_SUCCESS); + + if (operation->state == AWS_MRROS_PENDING_DESTROY || operation->state == AWS_MRROS_TERMINAL) { + return; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: streaming operation %" PRIu64 " halted with error code %d(%s)", + (void *)operation->client_internal_ref, + operation->id, + error_code, + aws_error_debug_str(error_code)); + + s_streaming_operation_emit_streaming_subscription_event(operation, ARRSSET_SUBSCRIPTION_HALTED, error_code); + + s_change_operation_state(operation, AWS_MRROS_TERMINAL); +} + +static void s_request_response_fail_operation(struct aws_mqtt_rr_client_operation *operation, int error_code) { + if (operation->type == AWS_MRROT_STREAMING) { + s_halt_streaming_operation_with_failure(operation, error_code); + } else { + s_complete_request_operation_with_failure(operation, error_code); + } +} + +static int s_rr_client_clean_up_operation(void *context, struct aws_hash_element *elem) { + (void)context; + struct aws_mqtt_rr_client_operation *operation = elem->value; + + s_request_response_fail_operation(operation, AWS_ERROR_MQTT_REQUEST_RESPONSE_CLIENT_SHUT_DOWN); + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +static void s_mqtt_request_response_client_external_shutdown_task_fn( + struct aws_task *task, + void *arg, + enum aws_task_status task_status) { + (void)task; + + AWS_FATAL_ASSERT(task_status != AWS_TASK_STATUS_CANCELED); + + struct aws_mqtt_request_response_client *client = arg; + + /* stop handling adapter event callbacks */ + client->state = AWS_RRCS_SHUTTING_DOWN; + + if (client->scheduled_service_timepoint_ns != 0) { + aws_event_loop_cancel_task(client->loop, &client->service_task); + client->scheduled_service_timepoint_ns = 0; + } + + aws_rr_subscription_manager_clean_up(&client->subscription_manager); + + if (client->client_adapter != NULL) { + aws_mqtt_protocol_adapter_destroy(client->client_adapter); + } + + /* + * It is a client invariant that when external shutdown starts, it must be the case that there are no in-flight + * operations with un-executed submit tasks. This means it safe to assume that all tracked request operations are + * either in the process of cleaning up already (state == AWS_MRROS_PENDING_DESTROY) or can be + * completed now (state != AWS_MRROS_PENDING_DESTROY). Non-terminal streaming operations are moved into + * a terminal state and emit an appropriate failure/ended event. + * + * Actual operation destruction and client ref-count release is done by a scheduled task + * on the operation that is triggered by dec-refing it (assuming streaming operations get closed by the binding + * client). + */ + aws_hash_table_foreach(&client->operations, s_rr_client_clean_up_operation, NULL); + + aws_ref_count_release(&client->internal_ref_count); +} + +static void s_mqtt_request_response_client_wake_service(struct aws_mqtt_request_response_client *client) { + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(client->loop)); + + if (client->state != AWS_RRCS_ACTIVE) { + return; + } + + if (client->scheduled_service_timepoint_ns == 0 || now < client->scheduled_service_timepoint_ns) { + if (now < client->scheduled_service_timepoint_ns) { + aws_event_loop_cancel_task(client->loop, &client->service_task); + } + + client->scheduled_service_timepoint_ns = now; + aws_event_loop_schedule_task_now(client->loop, &client->service_task); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, "id=%p: request-response client service task woke", (void *)client); + } +} + +struct aws_rrc_incomplete_publish { + struct aws_allocator *allocator; + + struct aws_mqtt_request_response_client *rr_client; + + uint64_t operation_id; +}; + +static void s_aws_rrc_incomplete_publish_destroy(struct aws_rrc_incomplete_publish *user_data) { + if (user_data == NULL) { + return; + } + + aws_mqtt_request_response_client_release_internal(user_data->rr_client); + + aws_mem_release(user_data->allocator, user_data); +} + +static void s_on_request_publish_completion(int error_code, void *userdata) { + struct aws_rrc_incomplete_publish *publish_user_data = userdata; + + if (error_code != AWS_ERROR_SUCCESS) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response operation %" PRIu64 " failed publish step due to error %d(%s)", + (void *)publish_user_data->rr_client, + publish_user_data->operation_id, + error_code, + aws_error_debug_str(error_code)); + + struct aws_hash_element *element = NULL; + if (aws_hash_table_find( + &publish_user_data->rr_client->operations, &publish_user_data->operation_id, &element) == + AWS_OP_SUCCESS && + element != NULL) { + struct aws_mqtt_rr_client_operation *operation = element->value; + s_complete_request_operation_with_failure(operation, AWS_ERROR_MQTT_REQUEST_RESPONSE_PUBLISH_FAILURE); + } + } + + s_aws_rrc_incomplete_publish_destroy(publish_user_data); +} + +static void s_make_mqtt_request( + struct aws_mqtt_request_response_client *client, + struct aws_mqtt_rr_client_operation *operation) { + (void)client; + + AWS_FATAL_ASSERT(operation->type == AWS_MRROT_REQUEST); + + struct aws_mqtt_request_operation_options *request_options = &operation->storage.request_storage.options; + + struct aws_rrc_incomplete_publish *publish_user_data = + aws_mem_calloc(client->allocator, 1, sizeof(struct aws_rrc_incomplete_publish)); + publish_user_data->allocator = client->allocator; + publish_user_data->rr_client = aws_mqtt_request_response_client_acquire_internal(client); + publish_user_data->operation_id = operation->id; + + struct aws_protocol_adapter_publish_options publish_options = { + .topic = request_options->publish_topic, + .payload = request_options->serialized_request, + .ack_timeout_seconds = client->config.operation_timeout_seconds, + .completion_callback_fn = s_on_request_publish_completion, + .user_data = publish_user_data, + }; + + if (aws_mqtt_protocol_adapter_publish(client->client_adapter, &publish_options)) { + int error_code = aws_last_error(); + + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response operation %" PRIu64 " synchronously failed publish step due to error %d(%s)", + (void *)publish_user_data->rr_client, + publish_user_data->operation_id, + error_code, + aws_error_debug_str(error_code)); + s_complete_request_operation_with_failure(operation, AWS_ERROR_MQTT_REQUEST_RESPONSE_PUBLISH_FAILURE); + goto error; + } + + return; + +error: + + s_aws_rrc_incomplete_publish_destroy(publish_user_data); +} + +struct aws_rr_subscription_status_event_task { + struct aws_allocator *allocator; + + struct aws_task task; + + struct aws_mqtt_request_response_client *rr_client; + + enum aws_rr_subscription_event_type type; + struct aws_byte_buf topic_filter; + uint64_t operation_id; +}; + +static void s_aws_rr_subscription_status_event_task_delete(struct aws_rr_subscription_status_event_task *task) { + if (task == NULL) { + return; + } + + aws_byte_buf_clean_up(&task->topic_filter); + aws_mqtt_request_response_client_release_internal(task->rr_client); + + aws_mem_release(task->allocator, task); +} + +static void s_on_request_operation_subscription_status_event( + struct aws_mqtt_rr_client_operation *operation, + struct aws_byte_cursor topic_filter, + enum aws_rr_subscription_event_type event_type) { + (void)topic_filter; + + switch (event_type) { + case ARRSET_REQUEST_SUBSCRIBE_FAILURE: + case ARRSET_REQUEST_SUBSCRIPTION_ENDED: + s_complete_request_operation_with_failure(operation, AWS_ERROR_MQTT_REQUEST_RESPONSE_SUBSCRIBE_FAILURE); + break; + + case ARRSET_REQUEST_SUBSCRIBE_SUCCESS: + if (operation->state == AWS_MRROS_PENDING_SUBSCRIPTION) { + --operation->pending_subscriptions; + if (operation->pending_subscriptions == 0) { + s_change_operation_state(operation, AWS_MRROS_PENDING_RESPONSE); + s_make_mqtt_request(operation->client_internal_ref, operation); + } + } + break; + + default: + AWS_FATAL_ASSERT(false); + } +} + +static void s_on_streaming_operation_subscription_status_event( + struct aws_mqtt_rr_client_operation *operation, + struct aws_byte_cursor topic_filter, + enum aws_rr_subscription_event_type event_type) { + (void)topic_filter; + + switch (event_type) { + case ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED: + if (operation->state == AWS_MRROS_PENDING_SUBSCRIPTION) { + s_change_operation_state(operation, AWS_MRROS_SUBSCRIBED); + } + + s_streaming_operation_emit_streaming_subscription_event( + operation, ARRSSET_SUBSCRIPTION_ESTABLISHED, AWS_ERROR_SUCCESS); + break; + + case ARRSET_STREAMING_SUBSCRIPTION_LOST: + s_streaming_operation_emit_streaming_subscription_event( + operation, ARRSSET_SUBSCRIPTION_LOST, AWS_ERROR_SUCCESS); + break; + + case ARRSET_STREAMING_SUBSCRIPTION_HALTED: + s_halt_streaming_operation_with_failure(operation, AWS_ERROR_MQTT_REQUEST_RESPONSE_SUBSCRIBE_FAILURE); + break; + + default: + AWS_FATAL_ASSERT(false); + } +} + +static void s_handle_subscription_status_event_task(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_rr_subscription_status_event_task *event_task = arg; + + if (status == AWS_TASK_STATUS_CANCELED) { + goto done; + } + + if (event_task->type == ARRSET_UNSUBSCRIBE_COMPLETE || event_task->type == ARRSET_SUBSCRIPTION_EMPTY) { + s_mqtt_request_response_client_wake_service(event_task->rr_client); + goto done; + } + + struct aws_hash_element *element = NULL; + if (aws_hash_table_find(&event_task->rr_client->operations, &event_task->operation_id, &element) || + element == NULL) { + goto done; + } + + struct aws_mqtt_rr_client_operation *operation = element->value; + + switch (event_task->type) { + case ARRSET_REQUEST_SUBSCRIBE_SUCCESS: + case ARRSET_REQUEST_SUBSCRIBE_FAILURE: + case ARRSET_REQUEST_SUBSCRIPTION_ENDED: + s_on_request_operation_subscription_status_event( + operation, aws_byte_cursor_from_buf(&event_task->topic_filter), event_task->type); + break; + + case ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED: + case ARRSET_STREAMING_SUBSCRIPTION_LOST: + case ARRSET_STREAMING_SUBSCRIPTION_HALTED: + s_on_streaming_operation_subscription_status_event( + operation, aws_byte_cursor_from_buf(&event_task->topic_filter), event_task->type); + break; + + default: + break; + } + +done: + + s_aws_rr_subscription_status_event_task_delete(event_task); +} + +static struct aws_rr_subscription_status_event_task *s_aws_rr_subscription_status_event_task_new( + struct aws_allocator *allocator, + struct aws_mqtt_request_response_client *rr_client, + const struct aws_rr_subscription_status_event *event) { + struct aws_rr_subscription_status_event_task *task = + aws_mem_calloc(allocator, 1, sizeof(struct aws_rr_subscription_status_event_task)); + + task->allocator = allocator; + task->type = event->type; + task->operation_id = event->operation_id; + task->rr_client = aws_mqtt_request_response_client_acquire_internal(rr_client); + + aws_byte_buf_init_copy_from_cursor(&task->topic_filter, allocator, event->topic_filter); + + aws_task_init(&task->task, s_handle_subscription_status_event_task, task, "SubscriptionStatusEventTask"); + + return task; +} + +static void s_aws_rr_client_subscription_status_event_callback( + const struct aws_rr_subscription_status_event *event, + void *userdata) { + (void)event; + (void)userdata; + + /* + * We must be on the event loop, but it's safer overall to process this event as a top-level event loop task. The + * subscription manager assumes that we won't call APIs on it while iterating subscription records and listeners. + * + * These tasks hold an internal reference while they exist. + */ + + struct aws_mqtt_request_response_client *rr_client = userdata; + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(rr_client->loop)); + AWS_FATAL_ASSERT(rr_client->state != AWS_RRCS_SHUTTING_DOWN); + + struct aws_rr_subscription_status_event_task *task = + s_aws_rr_subscription_status_event_task_new(rr_client->allocator, rr_client, event); + + aws_event_loop_schedule_task_now(rr_client->loop, &task->task); +} + +static void s_aws_rr_client_protocol_adapter_subscription_event_callback( + const struct aws_protocol_adapter_subscription_event *event, + void *user_data) { + struct aws_mqtt_request_response_client *rr_client = user_data; + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(rr_client->loop)); + + if (rr_client->state != AWS_RRCS_ACTIVE) { + return; + } + + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(&rr_client->subscription_manager, event); +} + +static void s_apply_publish_to_streaming_operation_list( + struct aws_rr_operation_list_topic_filter_entry *entry, + const struct aws_protocol_adapter_incoming_publish_event *publish_event) { + AWS_FATAL_ASSERT(entry != NULL); + + struct aws_linked_list_node *node = aws_linked_list_begin(&entry->operations); + while (node != aws_linked_list_end(&entry->operations)) { + struct aws_mqtt_rr_client_operation *operation = + AWS_CONTAINER_OF(node, struct aws_mqtt_rr_client_operation, node); + node = aws_linked_list_next(node); + + if (operation->type != AWS_MRROT_STREAMING) { + continue; + } + + if (operation->state == AWS_MRROS_PENDING_DESTROY || operation->state == AWS_MRROS_TERMINAL) { + continue; + } + + aws_mqtt_streaming_operation_incoming_publish_fn *incoming_publish_callback = + operation->storage.streaming_storage.options.incoming_publish_callback; + if (!incoming_publish_callback) { + continue; + } + + void *user_data = operation->storage.streaming_storage.options.user_data; + (*incoming_publish_callback)(publish_event->payload, user_data); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on topic '" PRInSTR + "' routed to streaming operation %" PRIu64, + (void *)operation->client_internal_ref, + AWS_BYTE_CURSOR_PRI(publish_event->topic), + operation->id); + } +} + +static void s_complete_operation_with_correlation_token( + struct aws_mqtt_request_response_client *rr_client, + struct aws_byte_cursor correlation_token, + const struct aws_protocol_adapter_incoming_publish_event *publish_event) { + struct aws_hash_element *hash_element = NULL; + + if (aws_hash_table_find(&rr_client->operations_by_correlation_tokens, &correlation_token, &hash_element)) { + return; + } + + if (hash_element == NULL) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on response path topic '" PRInSTR + "' and correlation token '" PRInSTR "' does not have an originating request entry", + (void *)rr_client, + AWS_BYTE_CURSOR_PRI(publish_event->topic), + AWS_BYTE_CURSOR_PRI(correlation_token)); + return; + } + + struct aws_mqtt_rr_client_operation *operation = hash_element->value; + AWS_FATAL_ASSERT(operation->type == AWS_MRROT_REQUEST); + + if (operation->state == AWS_MRROS_PENDING_DESTROY) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response operation %" PRIu64 " cannot be completed, already in pending destruction state", + (void *)operation->client_internal_ref, + operation->id); + return; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response operation %" PRIu64 " completed successfully", + (void *)operation->client_internal_ref, + operation->id); + + aws_mqtt_request_operation_completion_fn *completion_callback = + operation->storage.request_storage.options.completion_callback; + void *user_data = operation->storage.request_storage.options.user_data; + + if (completion_callback != NULL) { + (*completion_callback)(&publish_event->topic, &publish_event->payload, AWS_ERROR_SUCCESS, user_data); + } + + s_change_operation_state(operation, AWS_MRROS_PENDING_DESTROY); + + aws_mqtt_rr_client_operation_release(operation); +} + +static void s_apply_publish_to_response_path_entry( + struct aws_mqtt_request_response_client *rr_client, + struct aws_rr_response_path_entry *entry, + const struct aws_protocol_adapter_incoming_publish_event *publish_event) { + + struct aws_json_value *json_payload = NULL; + + struct aws_byte_cursor correlation_token; + AWS_ZERO_STRUCT(correlation_token); + struct aws_byte_cursor correlation_token_json_path = aws_byte_cursor_from_buf(&entry->correlation_token_json_path); + if (correlation_token_json_path.len > 0) { + json_payload = aws_json_value_new_from_string(rr_client->allocator, publish_event->payload); + if (json_payload == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on response path topic '" PRInSTR + "' could not be deserialized into JSON", + (void *)rr_client, + AWS_BYTE_CURSOR_PRI(publish_event->topic)); + return; + } + + struct aws_byte_cursor segment; + AWS_ZERO_STRUCT(segment); + + struct aws_json_value *correlation_token_entry = json_payload; + while (aws_byte_cursor_next_split(&correlation_token_json_path, '.', &segment)) { + if (!aws_json_value_is_object(correlation_token_entry)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on response path topic '" PRInSTR + "' unable to walk correlation token path '" PRInSTR "'", + (void *)rr_client, + AWS_BYTE_CURSOR_PRI(publish_event->topic), + AWS_BYTE_CURSOR_PRI(correlation_token_json_path)); + goto done; + } + + correlation_token_entry = aws_json_value_get_from_object(correlation_token_entry, segment); + if (correlation_token_entry == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on response path topic '" PRInSTR + "' could not find path segment '" PRInSTR "'", + (void *)rr_client, + AWS_BYTE_CURSOR_PRI(publish_event->topic), + AWS_BYTE_CURSOR_PRI(segment)); + goto done; + } + } + + if (!aws_json_value_is_string(correlation_token_entry)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on response path topic '" PRInSTR + "' token entry is not a string", + (void *)rr_client, + AWS_BYTE_CURSOR_PRI(publish_event->topic)); + goto done; + } + + if (aws_json_value_get_string(correlation_token_entry, &correlation_token)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on response path topic '" PRInSTR + "' failed to extract string from token entry", + (void *)rr_client, + AWS_BYTE_CURSOR_PRI(publish_event->topic)); + goto done; + } + } + + s_complete_operation_with_correlation_token(rr_client, correlation_token, publish_event); + +done: + + if (json_payload != NULL) { + aws_json_value_destroy(json_payload); + } +} + +static void s_aws_rr_client_protocol_adapter_incoming_publish_callback( + const struct aws_protocol_adapter_incoming_publish_event *publish_event, + void *user_data) { + + struct aws_mqtt_request_response_client *rr_client = user_data; + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(rr_client->loop)); + + if (rr_client->state != AWS_RRCS_ACTIVE) { + return; + } + + /* Streaming operation handling */ + struct aws_hash_element *subscription_filter_element = NULL; + if (aws_hash_table_find( + &rr_client->streaming_operation_subscription_lists, &publish_event->topic, &subscription_filter_element) == + AWS_OP_SUCCESS && + subscription_filter_element != NULL) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on topic '" PRInSTR "' matches streaming topic", + (void *)rr_client, + AWS_BYTE_CURSOR_PRI(publish_event->topic)); + + s_apply_publish_to_streaming_operation_list(subscription_filter_element->value, publish_event); + } + + /* Request-Response handling */ + struct aws_hash_element *response_path_element = NULL; + if (aws_hash_table_find(&rr_client->request_response_paths, &publish_event->topic, &response_path_element) == + AWS_OP_SUCCESS && + response_path_element != NULL) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client incoming publish on topic '" PRInSTR "' matches response path", + (void *)rr_client, + AWS_BYTE_CURSOR_PRI(publish_event->topic)); + + s_apply_publish_to_response_path_entry(rr_client, response_path_element->value, publish_event); + } +} + +static void s_aws_rr_client_protocol_adapter_terminate_callback(void *user_data) { + struct aws_mqtt_request_response_client *rr_client = user_data; + + /* release the internal ref count "held" by the protocol adapter's existence */ + aws_ref_count_release(&rr_client->internal_ref_count); +} + +static void s_aws_rr_client_protocol_adapter_connection_event_callback( + const struct aws_protocol_adapter_connection_event *event, + void *user_data) { + struct aws_mqtt_request_response_client *rr_client = user_data; + + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(rr_client->loop)); + + if (rr_client->state != AWS_RRCS_ACTIVE) { + return; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client applying connection event to subscription manager", + (void *)rr_client); + + aws_rr_subscription_manager_on_protocol_adapter_connection_event(&rr_client->subscription_manager, event); +} + +static int s_compare_rr_operation_timeouts(const void *a, const void *b) { + const struct aws_mqtt_rr_client_operation **operation_a_ptr = (void *)a; + const struct aws_mqtt_rr_client_operation *operation_a = *operation_a_ptr; + + const struct aws_mqtt_rr_client_operation **operation_b_ptr = (void *)b; + const struct aws_mqtt_rr_client_operation *operation_b = *operation_b_ptr; + + if (operation_a->timeout_timepoint_ns < operation_b->timeout_timepoint_ns) { + return -1; + } else if (operation_a->timeout_timepoint_ns > operation_b->timeout_timepoint_ns) { + return 1; + } else { + return 0; + } +} + +static struct aws_mqtt_request_response_client *s_aws_mqtt_request_response_client_new( + struct aws_allocator *allocator, + const struct aws_mqtt_request_response_client_options *options, + struct aws_event_loop *loop) { + struct aws_rr_subscription_manager_options sm_options = { + .max_request_response_subscriptions = options->max_request_response_subscriptions, + .max_streaming_subscriptions = options->max_streaming_subscriptions, + .operation_timeout_seconds = options->operation_timeout_seconds, + }; + + /* + * We can't initialize the subscription manager until we're running on the event loop, so make sure that + * initialize can't fail by checking its options for validity now. + */ + if (!aws_rr_subscription_manager_are_options_valid(&sm_options)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, "(static) request response client creation failed - invalid client options"); + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + struct aws_mqtt_request_response_client *rr_client = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_request_response_client)); + + rr_client->allocator = allocator; + rr_client->config = *options; + rr_client->loop = loop; + rr_client->state = AWS_RRCS_UNINITIALIZED; + + aws_hash_table_init( + &rr_client->operations, + allocator, + MQTT_RR_CLIENT_OPERATION_TABLE_DEFAULT_SIZE, + aws_hash_uint64_t_by_identity, + aws_hash_compare_uint64_t_eq, + NULL, + NULL); + + aws_priority_queue_init_dynamic( + &rr_client->operations_by_timeout, + allocator, + 100, + sizeof(struct aws_mqtt_rr_client_operation *), + s_compare_rr_operation_timeouts); + + aws_hash_table_init( + &rr_client->streaming_operation_subscription_lists, + allocator, + MQTT_RR_CLIENT_OPERATION_TABLE_DEFAULT_SIZE, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_aws_rr_operation_list_topic_filter_entry_hash_element_destroy); + + aws_hash_table_init( + &rr_client->request_response_paths, + allocator, + MQTT_RR_CLIENT_RESPONSE_TABLE_DEFAULT_SIZE, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_aws_rr_response_path_table_hash_element_destroy); + + aws_hash_table_init( + &rr_client->operations_by_correlation_tokens, + allocator, + MQTT_RR_CLIENT_OPERATION_TABLE_DEFAULT_SIZE, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + NULL); + + aws_linked_list_init(&rr_client->operation_queue); + + aws_task_init( + &rr_client->external_shutdown_task, + s_mqtt_request_response_client_external_shutdown_task_fn, + rr_client, + "mqtt_rr_client_external_shutdown"); + + aws_task_init( + &rr_client->internal_shutdown_task, + s_mqtt_request_response_client_internal_shutdown_task_fn, + rr_client, + "mqtt_rr_client_internal_shutdown"); + + /* The initial external ref belongs to the caller */ + aws_ref_count_init(&rr_client->external_ref_count, rr_client, s_aws_rr_client_on_zero_external_ref_count); + + /* The initial internal ref belongs to ourselves (the external ref count shutdown task) */ + aws_ref_count_init(&rr_client->internal_ref_count, rr_client, s_aws_rr_client_on_zero_internal_ref_count); + + aws_atomic_store_int(&rr_client->next_id, 1); + + return rr_client; +} + +static void s_aws_rr_client_init_subscription_manager( + struct aws_mqtt_request_response_client *rr_client, + struct aws_allocator *allocator) { + AWS_FATAL_ASSERT(aws_event_loop_thread_is_callers_thread(rr_client->loop)); + + struct aws_rr_subscription_manager_options subscription_manager_options = { + .operation_timeout_seconds = rr_client->config.operation_timeout_seconds, + .max_request_response_subscriptions = rr_client->config.max_request_response_subscriptions, + .max_streaming_subscriptions = rr_client->config.max_streaming_subscriptions, + .subscription_status_callback = s_aws_rr_client_subscription_status_event_callback, + .userdata = rr_client, + }; + + aws_rr_subscription_manager_init( + &rr_client->subscription_manager, allocator, rr_client->client_adapter, &subscription_manager_options); +} + +static void s_check_for_operation_timeouts(struct aws_mqtt_request_response_client *client) { + uint64_t now = 0; + aws_high_res_clock_get_ticks(&now); + + struct aws_priority_queue *timeout_queue = &client->operations_by_timeout; + + bool done = aws_priority_queue_size(timeout_queue) == 0; + while (!done) { + struct aws_mqtt_rr_client_operation **next_operation_by_timeout_ptr = NULL; + aws_priority_queue_top(timeout_queue, (void **)&next_operation_by_timeout_ptr); + AWS_FATAL_ASSERT(next_operation_by_timeout_ptr != NULL); + struct aws_mqtt_rr_client_operation *next_operation_by_timeout = *next_operation_by_timeout_ptr; + AWS_FATAL_ASSERT(next_operation_by_timeout != NULL); + + // If the current top of the heap hasn't timed out than nothing has + if (next_operation_by_timeout->timeout_timepoint_ns > now) { + break; + } + + /* Ack timeout for this operation has been reached */ + aws_priority_queue_pop(timeout_queue, &next_operation_by_timeout); + + s_request_response_fail_operation(next_operation_by_timeout, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT); + + done = aws_priority_queue_size(timeout_queue) == 0; + } +} + +static uint64_t s_mqtt_request_response_client_get_next_service_time(struct aws_mqtt_request_response_client *client) { + if (aws_priority_queue_size(&client->operations_by_timeout) > 0) { + struct aws_mqtt_rr_client_operation **next_operation_by_timeout_ptr = NULL; + aws_priority_queue_top(&client->operations_by_timeout, (void **)&next_operation_by_timeout_ptr); + AWS_FATAL_ASSERT(next_operation_by_timeout_ptr != NULL); + struct aws_mqtt_rr_client_operation *next_operation_by_timeout = *next_operation_by_timeout_ptr; + AWS_FATAL_ASSERT(next_operation_by_timeout != NULL); + + return next_operation_by_timeout->timeout_timepoint_ns; + } + + return UINT64_MAX; +} + +static int s_add_streaming_operation_to_subscription_topic_filter_table( + struct aws_mqtt_request_response_client *client, + struct aws_mqtt_rr_client_operation *operation) { + + struct aws_byte_cursor topic_filter_cursor = operation->storage.streaming_storage.options.topic_filter; + + struct aws_hash_element *element = NULL; + if (aws_hash_table_find(&client->streaming_operation_subscription_lists, &topic_filter_cursor, &element)) { + return aws_raise_error(AWS_ERROR_MQTT_REQUEST_RESPONSE_INTERNAL_ERROR); + } + + struct aws_rr_operation_list_topic_filter_entry *entry = NULL; + if (element == NULL) { + entry = s_aws_rr_operation_list_topic_filter_entry_new(client->allocator, topic_filter_cursor); + aws_hash_table_put(&client->streaming_operation_subscription_lists, &entry->topic_filter_cursor, entry, NULL); + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client adding topic filter '" PRInSTR "' to streaming subscriptions table", + (void *)client, + AWS_BYTE_CURSOR_PRI(topic_filter_cursor)); + } else { + entry = element->value; + } + + AWS_FATAL_ASSERT(entry != NULL); + + if (aws_linked_list_node_is_in_list(&operation->node)) { + aws_linked_list_remove(&operation->node); + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client adding streaming operation %" PRIu64 + " to streaming subscription table with topic_filter '" PRInSTR "'", + (void *)client, + operation->id, + AWS_BYTE_CURSOR_PRI(topic_filter_cursor)); + + aws_linked_list_push_back(&entry->operations, &operation->node); + + return AWS_OP_SUCCESS; +} + +static int s_add_request_operation_to_response_path_table( + struct aws_mqtt_request_response_client *client, + struct aws_mqtt_rr_client_operation *operation) { + + struct aws_array_list *paths = &operation->storage.request_storage.operation_response_paths; + size_t path_count = aws_array_list_length(paths); + for (size_t i = 0; i < path_count; ++i) { + struct aws_mqtt_request_operation_response_path path; + aws_array_list_get_at(paths, &path, i); + + struct aws_hash_element *element = NULL; + if (aws_hash_table_find(&client->request_response_paths, &path.topic, &element)) { + return aws_raise_error(AWS_ERROR_MQTT_REQUEST_RESPONSE_INTERNAL_ERROR); + } + + if (element != NULL) { + struct aws_rr_response_path_entry *entry = element->value; + ++entry->ref_count; + continue; + } + + struct aws_rr_response_path_entry *entry = + s_aws_rr_response_path_entry_new(client->allocator, path.topic, path.correlation_token_json_path); + if (aws_hash_table_put(&client->request_response_paths, &entry->topic_cursor, entry, NULL)) { + return aws_raise_error(AWS_ERROR_MQTT_REQUEST_RESPONSE_INTERNAL_ERROR); + } + } + + return AWS_OP_SUCCESS; +} + +static int s_add_request_operation_to_correlation_token_table( + struct aws_mqtt_request_response_client *client, + struct aws_mqtt_rr_client_operation *operation) { + + return aws_hash_table_put( + &client->operations_by_correlation_tokens, + &operation->storage.request_storage.options.correlation_token, + operation, + NULL); +} + +static int s_add_in_progress_operation_to_tracking_tables( + struct aws_mqtt_request_response_client *client, + struct aws_mqtt_rr_client_operation *operation) { + if (operation->type == AWS_MRROT_STREAMING) { + if (s_add_streaming_operation_to_subscription_topic_filter_table(client, operation)) { + return AWS_OP_ERR; + } + } else { + if (s_add_request_operation_to_response_path_table(client, operation)) { + return AWS_OP_ERR; + } + + if (s_add_request_operation_to_correlation_token_table(client, operation)) { + return AWS_OP_ERR; + } + } + + operation->in_client_tables = true; + + return AWS_OP_SUCCESS; +} + +static void s_handle_operation_subscribe_result( + struct aws_mqtt_request_response_client *client, + struct aws_mqtt_rr_client_operation *operation, + enum aws_acquire_subscription_result_type subscribe_result) { + if (subscribe_result == AASRT_FAILURE || subscribe_result == AASRT_NO_CAPACITY) { + int error_code = (subscribe_result == AASRT_NO_CAPACITY) + ? AWS_ERROR_MQTT_REQUEST_RESPONSE_NO_SUBSCRIPTION_CAPACITY + : AWS_ERROR_MQTT_REQUEST_RESPONSE_SUBSCRIBE_FAILURE; + s_request_response_fail_operation(operation, error_code); + return; + } + + if (s_add_in_progress_operation_to_tracking_tables(client, operation)) { + s_request_response_fail_operation(operation, AWS_ERROR_MQTT_REQUEST_RESPONSE_INTERNAL_ERROR); + return; + } + + if (subscribe_result == AASRT_SUBSCRIBING) { + s_change_operation_state(operation, AWS_MRROS_PENDING_SUBSCRIPTION); + return; + } + + if (operation->type == AWS_MRROT_STREAMING) { + s_change_operation_state(operation, AWS_MRROS_SUBSCRIBED); + s_streaming_operation_emit_streaming_subscription_event( + operation, ARRSSET_SUBSCRIPTION_ESTABLISHED, AWS_ERROR_SUCCESS); + } else { + s_make_mqtt_request(client, operation); + } +} + +static enum aws_rr_subscription_type s_rr_operation_type_to_subscription_type( + enum aws_mqtt_request_response_operation_type type) { + if (type == AWS_MRROT_REQUEST) { + return ARRST_REQUEST_RESPONSE; + } + + return ARRST_EVENT_STREAM; +} + +static bool s_can_operation_dequeue( + struct aws_mqtt_request_response_client *client, + struct aws_mqtt_rr_client_operation *operation) { + if (operation->type != AWS_MRROT_REQUEST) { + return true; + } + + struct aws_hash_element *token_element = NULL; + if (aws_hash_table_find( + &client->operations_by_correlation_tokens, + &operation->storage.request_storage.options.correlation_token, + &token_element)) { + return false; + } + + return token_element == NULL; +} + +static struct aws_byte_cursor *s_aws_mqtt_rr_operation_get_subscription_topic_filters( + struct aws_mqtt_rr_client_operation *operation) { + if (operation->type == AWS_MRROT_STREAMING) { + return &operation->storage.streaming_storage.options.topic_filter; + } else { + return operation->storage.request_storage.options.subscription_topic_filters; + } +} + +static size_t s_aws_mqtt_rr_operation_get_subscription_topic_filter_count( + struct aws_mqtt_rr_client_operation *operation) { + if (operation->type == AWS_MRROT_STREAMING) { + return 1; + } else { + return operation->storage.request_storage.options.subscription_topic_filter_count; + } +} + +static void s_process_queued_operations(struct aws_mqtt_request_response_client *client) { + aws_rr_subscription_manager_purge_unused(&client->subscription_manager); + + while (!aws_linked_list_empty(&client->operation_queue)) { + struct aws_linked_list_node *head = aws_linked_list_front(&client->operation_queue); + struct aws_mqtt_rr_client_operation *head_operation = + AWS_CONTAINER_OF(head, struct aws_mqtt_rr_client_operation, node); + + if (!s_can_operation_dequeue(client, head_operation)) { + break; + } + + struct aws_rr_acquire_subscription_options subscribe_options = { + .topic_filters = s_aws_mqtt_rr_operation_get_subscription_topic_filters(head_operation), + .topic_filter_count = s_aws_mqtt_rr_operation_get_subscription_topic_filter_count(head_operation), + .operation_id = head_operation->id, + .type = s_rr_operation_type_to_subscription_type(head_operation->type), + }; + + enum aws_acquire_subscription_result_type subscribe_result = + aws_rr_subscription_manager_acquire_subscription(&client->subscription_manager, &subscribe_options); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client intake, queued operation %" PRIu64 + " yielded acquire subscription result: %s", + (void *)client, + head_operation->id, + s_aws_acquire_subscription_result_type(subscribe_result)); + + if (subscribe_result == AASRT_BLOCKED) { + break; + } + + aws_linked_list_pop_front(&client->operation_queue); + s_handle_operation_subscribe_result(client, head_operation, subscribe_result); + } +} + +static void s_mqtt_request_response_service_task_fn( + struct aws_task *task, + void *arg, + enum aws_task_status task_status) { + (void)task; + + if (task_status == AWS_TASK_STATUS_CANCELED) { + return; + } + + struct aws_mqtt_request_response_client *client = arg; + client->scheduled_service_timepoint_ns = 0; + + if (client->state == AWS_RRCS_ACTIVE) { + + // timeouts + s_check_for_operation_timeouts(client); + + // operation queue + s_process_queued_operations(client); + + // schedule next service + client->scheduled_service_timepoint_ns = s_mqtt_request_response_client_get_next_service_time(client); + aws_event_loop_schedule_task_future( + client->loop, &client->service_task, client->scheduled_service_timepoint_ns); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client service, next timepoint: %" PRIu64, + (void *)client, + client->scheduled_service_timepoint_ns); + } +} + +static void s_mqtt_request_response_client_initialize_task_fn( + struct aws_task *task, + void *arg, + enum aws_task_status task_status) { + (void)task; + + AWS_FATAL_ASSERT(task_status != AWS_TASK_STATUS_CANCELED); + + struct aws_mqtt_request_response_client *client = arg; + + if (client->state == AWS_RRCS_UNINITIALIZED) { + s_aws_rr_client_init_subscription_manager(client, client->allocator); + + client->state = AWS_RRCS_ACTIVE; + + aws_task_init(&client->service_task, s_mqtt_request_response_service_task_fn, client, "mqtt_rr_client_service"); + + aws_event_loop_schedule_task_future(client->loop, &client->service_task, UINT64_MAX); + client->scheduled_service_timepoint_ns = UINT64_MAX; + } + + if (client->config.initialized_callback != NULL) { + (*client->config.initialized_callback)(client->config.user_data); + } + + /* give up the internal ref we held while the task was pending */ + aws_ref_count_release(&client->internal_ref_count); +} + +static void s_setup_cross_thread_initialization(struct aws_mqtt_request_response_client *rr_client) { + /* now that it exists, 1 internal ref belongs to protocol adapter termination */ + aws_ref_count_acquire(&rr_client->internal_ref_count); + + /* 1 internal ref belongs to the initialize task until it runs */ + aws_ref_count_acquire(&rr_client->internal_ref_count); + + aws_task_init( + &rr_client->initialize_task, + s_mqtt_request_response_client_initialize_task_fn, + rr_client, + "mqtt_rr_client_initialize"); + aws_event_loop_schedule_task_now(rr_client->loop, &rr_client->initialize_task); +} + +struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_new_from_mqtt311_client( + struct aws_allocator *allocator, + struct aws_mqtt_client_connection *client, + const struct aws_mqtt_request_response_client_options *options) { + + struct aws_mqtt_request_response_client *rr_client = + s_aws_mqtt_request_response_client_new(allocator, options, aws_mqtt_client_connection_get_event_loop(client)); + + if (rr_client == NULL) { + return NULL; + } + + struct aws_mqtt_protocol_adapter_options adapter_options = { + .subscription_event_callback = s_aws_rr_client_protocol_adapter_subscription_event_callback, + .incoming_publish_callback = s_aws_rr_client_protocol_adapter_incoming_publish_callback, + .terminate_callback = s_aws_rr_client_protocol_adapter_terminate_callback, + .connection_event_callback = s_aws_rr_client_protocol_adapter_connection_event_callback, + .user_data = rr_client, + }; + + rr_client->client_adapter = aws_mqtt_protocol_adapter_new_from_311(rr_client->allocator, &adapter_options, client); + if (rr_client->client_adapter == NULL) { + goto error; + } + + s_setup_cross_thread_initialization(rr_client); + + return rr_client; + +error: + + /* even on construction failures we still need to walk through the async shutdown process */ + aws_mqtt_request_response_client_release(rr_client); + + return NULL; +} + +struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_new_from_mqtt5_client( + struct aws_allocator *allocator, + struct aws_mqtt5_client *client, + const struct aws_mqtt_request_response_client_options *options) { + + struct aws_mqtt_request_response_client *rr_client = + s_aws_mqtt_request_response_client_new(allocator, options, client->loop); + + if (rr_client == NULL) { + return NULL; + } + + struct aws_mqtt_protocol_adapter_options adapter_options = { + .subscription_event_callback = s_aws_rr_client_protocol_adapter_subscription_event_callback, + .incoming_publish_callback = s_aws_rr_client_protocol_adapter_incoming_publish_callback, + .terminate_callback = s_aws_rr_client_protocol_adapter_terminate_callback, + .connection_event_callback = s_aws_rr_client_protocol_adapter_connection_event_callback, + .user_data = rr_client, + }; + + rr_client->client_adapter = aws_mqtt_protocol_adapter_new_from_5(rr_client->allocator, &adapter_options, client); + if (rr_client->client_adapter == NULL) { + goto error; + } + + s_setup_cross_thread_initialization(rr_client); + + return rr_client; + +error: + + /* even on construction failures we still need to walk through the async shutdown process */ + aws_mqtt_request_response_client_release(rr_client); + + return NULL; +} + +struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_acquire( + struct aws_mqtt_request_response_client *client) { + if (client != NULL) { + aws_ref_count_acquire(&client->external_ref_count); + } + + return client; +} + +struct aws_mqtt_request_response_client *aws_mqtt_request_response_client_release( + struct aws_mqtt_request_response_client *client) { + if (client != NULL) { + aws_ref_count_release(&client->external_ref_count); + } + + return NULL; +} + +///////////////////////////////////////////////// + +static bool s_are_request_operation_options_valid( + const struct aws_mqtt_request_response_client *client, + const struct aws_mqtt_request_operation_options *request_options) { + if (request_options == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_REQUEST_RESPONSE, "(%p) rr client - NULL request options", (void *)client); + return false; + } + + if (request_options->response_path_count == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "(%p) rr client request options - no response paths supplied", + (void *)client); + return false; + } + + for (size_t i = 0; i < request_options->response_path_count; ++i) { + const struct aws_mqtt_request_operation_response_path *path = &request_options->response_paths[i]; + if (!aws_mqtt_is_valid_topic(&path->topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "(%p) rr client request options - " PRInSTR " is not a valid topic", + (void *)client, + AWS_BYTE_CURSOR_PRI(path->topic)); + return false; + } + } + + if (!aws_mqtt_is_valid_topic(&request_options->publish_topic)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "(%p) rr client request options - " PRInSTR " is not a valid topic", + (void *)client, + AWS_BYTE_CURSOR_PRI(request_options->publish_topic)); + return false; + } + + if (request_options->subscription_topic_filter_count == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "(%p) rr client request options - no subscription topic filters supplied", + (void *)client); + return false; + } + + for (size_t i = 0; i < request_options->subscription_topic_filter_count; ++i) { + const struct aws_byte_cursor subscription_topic_filter = request_options->subscription_topic_filters[i]; + if (!aws_mqtt_is_valid_topic_filter(&subscription_topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "(%p) rr client request options - " PRInSTR " is not a valid subscription topic filter", + (void *)client, + AWS_BYTE_CURSOR_PRI(subscription_topic_filter)); + return false; + } + } + + if (request_options->serialized_request.len == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, "(%p) rr client request options - empty request payload", (void *)client); + return false; + } + + return true; +} + +static bool s_are_streaming_operation_options_valid( + struct aws_mqtt_request_response_client *client, + const struct aws_mqtt_streaming_operation_options *streaming_options) { + if (streaming_options == NULL) { + AWS_LOGF_ERROR(AWS_LS_MQTT_REQUEST_RESPONSE, "(%p) rr client - NULL streaming options", (void *)client); + return false; + } + + if (!aws_mqtt_is_valid_topic_filter(&streaming_options->topic_filter)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "(%p) rr client streaming options - " PRInSTR " is not a valid topic filter", + (void *)client, + AWS_BYTE_CURSOR_PRI(streaming_options->topic_filter)); + return false; + } + + return true; +} + +static uint64_t s_aws_mqtt_request_response_client_allocate_operation_id( + struct aws_mqtt_request_response_client *client) { + return aws_atomic_fetch_add(&client->next_id, 1); +} + +static void s_mqtt_rr_client_submit_operation(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + + struct aws_mqtt_rr_client_operation *operation = arg; + struct aws_mqtt_request_response_client *client = operation->client_internal_ref; + + if (status == AWS_TASK_STATUS_CANCELED) { + goto done; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client, queuing operation %" PRIu64, + (void *)client, + operation->id); + + // add appropriate client table entries + aws_hash_table_put(&client->operations, &operation->id, operation, NULL); + + // add to timeout priority queue + if (operation->type == AWS_MRROT_REQUEST) { + aws_priority_queue_push_ref( + &client->operations_by_timeout, (void *)&operation, &operation->priority_queue_node); + } + + // enqueue + aws_linked_list_push_back(&operation->client_internal_ref->operation_queue, &operation->node); + + s_change_operation_state(operation, AWS_MRROS_QUEUED); + + s_mqtt_request_response_client_wake_service(operation->client_internal_ref); + +done: + + /* + * We hold a second reference to the operation during submission. This ensures that even if a streaming operation + * is immediately dec-refed by the creator (before submission completes), the operation will not get destroyed. + * + * It is now safe and correct to release that reference. + * + * After this, streaming operation lifetime is completely user-driven, while request operation lifetime is + * completely client-internal. + */ + aws_mqtt_rr_client_operation_release(operation); +} + +static void s_aws_mqtt_streaming_operation_storage_clean_up(struct aws_mqtt_streaming_operation_storage *storage) { + aws_byte_buf_clean_up(&storage->operation_data); +} + +static void s_aws_mqtt_request_operation_storage_clean_up(struct aws_mqtt_request_operation_storage *storage) { + aws_array_list_clean_up(&storage->operation_response_paths); + aws_array_list_clean_up(&storage->subscription_topic_filters); + aws_byte_buf_clean_up(&storage->operation_data); +} + +static void s_remove_operation_from_client_tables(struct aws_mqtt_rr_client_operation *operation) { + if (operation->type != AWS_MRROT_REQUEST) { + return; + } + + if (!operation->in_client_tables) { + return; + } + + struct aws_mqtt_request_response_client *client = operation->client_internal_ref; + + aws_hash_table_remove( + &client->operations_by_correlation_tokens, + &operation->storage.request_storage.options.correlation_token, + NULL, + NULL); + + struct aws_array_list *paths = &operation->storage.request_storage.operation_response_paths; + size_t path_count = aws_array_list_length(paths); + for (size_t i = 0; i < path_count; ++i) { + struct aws_mqtt_request_operation_response_path path; + aws_array_list_get_at(paths, &path, i); + + struct aws_hash_element *element = NULL; + if (aws_hash_table_find(&client->request_response_paths, &path.topic, &element) || element == NULL) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: internal state error removing reference to response path for topic " PRInSTR, + (void *)client, + AWS_BYTE_CURSOR_PRI(path.topic)); + continue; + } + + struct aws_rr_response_path_entry *entry = element->value; + --entry->ref_count; + + if (entry->ref_count == 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: removing last reference to response path for topic " PRInSTR, + (void *)client, + AWS_BYTE_CURSOR_PRI(path.topic)); + aws_hash_table_remove(&client->request_response_paths, &path.topic, NULL, NULL); + } else { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: removing reference to response path for topic " PRInSTR ", %zu references remain", + (void *)client, + AWS_BYTE_CURSOR_PRI(path.topic), + entry->ref_count); + } + } +} + +static void s_mqtt_rr_client_destroy_operation(struct aws_task *task, void *arg, enum aws_task_status status) { + (void)task; + (void)status; + + struct aws_mqtt_rr_client_operation *operation = arg; + struct aws_mqtt_request_response_client *client = operation->client_internal_ref; + + aws_hash_table_remove(&client->operations, &operation->id, NULL, NULL); + s_remove_operation_from_timeout_queue(operation); + + if (aws_linked_list_node_is_in_list(&operation->node)) { + aws_linked_list_remove(&operation->node); + } + + if (client->state != AWS_RRCS_SHUTTING_DOWN) { + struct aws_rr_release_subscription_options release_options = { + .topic_filters = s_aws_mqtt_rr_operation_get_subscription_topic_filters(operation), + .topic_filter_count = s_aws_mqtt_rr_operation_get_subscription_topic_filter_count(operation), + .operation_id = operation->id, + }; + aws_rr_subscription_manager_release_subscription(&client->subscription_manager, &release_options); + } + + s_remove_operation_from_client_tables(operation); + + aws_mqtt_request_response_client_release_internal(operation->client_internal_ref); + + if (operation->type == AWS_MRROT_STREAMING) { + s_aws_mqtt_streaming_operation_storage_clean_up(&operation->storage.streaming_storage); + } else { + s_aws_mqtt_request_operation_storage_clean_up(&operation->storage.request_storage); + } + + aws_mqtt_streaming_operation_terminated_fn *terminated_callback = NULL; + void *terminated_user_data = NULL; + if (operation->type == AWS_MRROT_STREAMING) { + terminated_callback = operation->storage.streaming_storage.options.terminated_callback; + terminated_user_data = operation->storage.streaming_storage.options.user_data; + } + + aws_mem_release(operation->allocator, operation); + + if (terminated_callback != NULL) { + (*terminated_callback)(terminated_user_data); + } +} + +static void s_on_mqtt_rr_client_operation_zero_ref_count(void *context) { + struct aws_mqtt_rr_client_operation *operation = context; + + aws_event_loop_schedule_task_now(operation->client_internal_ref->loop, &operation->destroy_task); +} + +static void s_aws_mqtt_rr_client_operation_init_shared( + struct aws_mqtt_rr_client_operation *operation, + struct aws_mqtt_request_response_client *client) { + operation->allocator = client->allocator; + aws_ref_count_init(&operation->ref_count, operation, s_on_mqtt_rr_client_operation_zero_ref_count); + + operation->client_internal_ref = aws_mqtt_request_response_client_acquire_internal(client); + operation->id = s_aws_mqtt_request_response_client_allocate_operation_id(client); + s_change_operation_state(operation, AWS_MRROS_NONE); + + aws_task_init( + &operation->submit_task, + s_mqtt_rr_client_submit_operation, + operation, + "MQTTRequestResponseClientOperationSubmit"); + aws_task_init( + &operation->destroy_task, + s_mqtt_rr_client_destroy_operation, + operation, + "MQTTRequestResponseClientOperationDestroy"); +} + +void s_aws_mqtt_request_operation_storage_init_from_options( + struct aws_mqtt_request_operation_storage *storage, + struct aws_allocator *allocator, + const struct aws_mqtt_request_operation_options *request_options) { + + size_t bytes_needed = 0; + bytes_needed += request_options->publish_topic.len; + bytes_needed += request_options->serialized_request.len; + bytes_needed += request_options->correlation_token.len; + + for (size_t i = 0; i < request_options->subscription_topic_filter_count; ++i) { + const struct aws_byte_cursor *subscription_topic_filter = &request_options->subscription_topic_filters[i]; + + bytes_needed += subscription_topic_filter->len; + } + + for (size_t i = 0; i < request_options->response_path_count; ++i) { + const struct aws_mqtt_request_operation_response_path *response_path = &request_options->response_paths[i]; + + bytes_needed += response_path->topic.len; + bytes_needed += response_path->correlation_token_json_path.len; + } + + storage->options = *request_options; + + aws_byte_buf_init(&storage->operation_data, allocator, bytes_needed); + aws_array_list_init_dynamic( + &storage->operation_response_paths, + allocator, + request_options->response_path_count, + sizeof(struct aws_mqtt_request_operation_response_path)); + aws_array_list_init_dynamic( + &storage->subscription_topic_filters, + allocator, + request_options->subscription_topic_filter_count, + sizeof(struct aws_byte_cursor)); + + AWS_FATAL_ASSERT( + aws_byte_buf_append_and_update(&storage->operation_data, &storage->options.publish_topic) == AWS_OP_SUCCESS); + AWS_FATAL_ASSERT( + aws_byte_buf_append_and_update(&storage->operation_data, &storage->options.serialized_request) == + AWS_OP_SUCCESS); + AWS_FATAL_ASSERT( + aws_byte_buf_append_and_update(&storage->operation_data, &storage->options.correlation_token) == + AWS_OP_SUCCESS); + + for (size_t i = 0; i < request_options->subscription_topic_filter_count; ++i) { + struct aws_byte_cursor subscription_topic_filter = request_options->subscription_topic_filters[i]; + + AWS_FATAL_ASSERT( + aws_byte_buf_append_and_update(&storage->operation_data, &subscription_topic_filter) == AWS_OP_SUCCESS); + + aws_array_list_push_back(&storage->subscription_topic_filters, &subscription_topic_filter); + } + + storage->options.subscription_topic_filters = storage->subscription_topic_filters.data; + + for (size_t i = 0; i < request_options->response_path_count; ++i) { + struct aws_mqtt_request_operation_response_path response_path = request_options->response_paths[i]; + + AWS_FATAL_ASSERT( + aws_byte_buf_append_and_update(&storage->operation_data, &response_path.topic) == AWS_OP_SUCCESS); + AWS_FATAL_ASSERT( + aws_byte_buf_append_and_update(&storage->operation_data, &response_path.correlation_token_json_path) == + AWS_OP_SUCCESS); + + aws_array_list_push_back(&storage->operation_response_paths, &response_path); + } + + storage->options.response_paths = storage->operation_response_paths.data; +} + +static void s_log_request_response_operation( + struct aws_mqtt_rr_client_operation *operation, + struct aws_mqtt_request_response_client *client) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT_REQUEST_RESPONSE, AWS_LL_DEBUG); + if (log_handle == NULL) { + return; + } + + struct aws_mqtt_request_operation_options *options = &operation->storage.request_storage.options; + + for (size_t i = 0; i < options->subscription_topic_filter_count; ++i) { + struct aws_byte_cursor subscription_topic_filter = options->subscription_topic_filters[i]; + + AWS_LOGUF( + log_handle, + AWS_LL_DEBUG, + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client operation %" PRIu64 " - subscription topic filter %zu topic '" PRInSTR "'", + (void *)client, + operation->id, + i, + AWS_BYTE_CURSOR_PRI(subscription_topic_filter)); + } + + AWS_LOGUF( + log_handle, + AWS_LL_DEBUG, + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client operation %" PRIu64 " - correlation token: '" PRInSTR "'", + (void *)client, + operation->id, + AWS_BYTE_CURSOR_PRI(options->correlation_token)); + + AWS_LOGUF( + log_handle, + AWS_LL_DEBUG, + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client operation %" PRIu64 " - publish topic: '" PRInSTR "'", + (void *)client, + operation->id, + AWS_BYTE_CURSOR_PRI(options->publish_topic)); + + AWS_LOGUF( + log_handle, + AWS_LL_DEBUG, + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client operation %" PRIu64 " - %zu response paths:", + (void *)client, + operation->id, + options->response_path_count); + + for (size_t i = 0; i < options->response_path_count; ++i) { + struct aws_mqtt_request_operation_response_path *response_path = &options->response_paths[i]; + + AWS_LOGUF( + log_handle, + AWS_LL_DEBUG, + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client operation %" PRIu64 " - response path %zu topic '" PRInSTR "'", + (void *)client, + operation->id, + i, + AWS_BYTE_CURSOR_PRI(response_path->topic)); + + AWS_LOGUF( + log_handle, + AWS_LL_DEBUG, + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client operation %" PRIu64 " - response path %zu correlation token path '" PRInSTR + "'", + (void *)client, + operation->id, + i, + AWS_BYTE_CURSOR_PRI(response_path->correlation_token_json_path)); + } +} + +int aws_mqtt_request_response_client_submit_request( + struct aws_mqtt_request_response_client *client, + const struct aws_mqtt_request_operation_options *request_options) { + + if (client == NULL) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + if (!s_are_request_operation_options_valid(client, request_options)) { + /* all failure cases have logged the problem already */ + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } + + uint64_t now = 0; + if (aws_high_res_clock_get_ticks(&now)) { + return aws_raise_error(AWS_ERROR_CLOCK_FAILURE); + } + + struct aws_allocator *allocator = client->allocator; + struct aws_mqtt_rr_client_operation *operation = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_rr_client_operation)); + operation->allocator = allocator; + operation->type = AWS_MRROT_REQUEST; + operation->timeout_timepoint_ns = + now + + aws_timestamp_convert(client->config.operation_timeout_seconds, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL); + operation->pending_subscriptions = request_options->subscription_topic_filter_count; + + s_aws_mqtt_request_operation_storage_init_from_options( + &operation->storage.request_storage, allocator, request_options); + s_aws_mqtt_rr_client_operation_init_shared(operation, client); + + AWS_LOGF_INFO( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client - submitting request-response operation with id %" PRIu64, + (void *)client, + operation->id); + + s_log_request_response_operation(operation, client); + + /* + * We hold a second reference to the operation during submission. This ensures that even if a streaming operation + * is immediately dec-refed by the creator (before submission runs), the operation will not get destroyed. + */ + aws_mqtt_rr_client_operation_acquire(operation); + + aws_event_loop_schedule_task_now(client->loop, &operation->submit_task); + + return AWS_OP_SUCCESS; +} + +void s_aws_mqtt_streaming_operation_storage_init_from_options( + struct aws_mqtt_streaming_operation_storage *storage, + struct aws_allocator *allocator, + const struct aws_mqtt_streaming_operation_options *streaming_options) { + size_t bytes_needed = streaming_options->topic_filter.len; + + storage->options = *streaming_options; + aws_byte_buf_init(&storage->operation_data, allocator, bytes_needed); + + AWS_FATAL_ASSERT( + aws_byte_buf_append_and_update(&storage->operation_data, &storage->options.topic_filter) == AWS_OP_SUCCESS); + + aws_atomic_init_int(&storage->activated, 0); +} + +static void s_log_streaming_operation( + struct aws_mqtt_rr_client_operation *operation, + struct aws_mqtt_request_response_client *client) { + struct aws_logger *log_handle = aws_logger_get_conditional(AWS_LS_MQTT_REQUEST_RESPONSE, AWS_LL_DEBUG); + if (log_handle == NULL) { + return; + } + + struct aws_mqtt_streaming_operation_options *options = &operation->storage.streaming_storage.options; + + AWS_LOGUF( + log_handle, + AWS_LL_DEBUG, + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client streaming operation %" PRIu64 ": topic filter: '" PRInSTR "'", + (void *)client, + operation->id, + AWS_BYTE_CURSOR_PRI(options->topic_filter)); +} + +struct aws_mqtt_rr_client_operation *aws_mqtt_request_response_client_create_streaming_operation( + struct aws_mqtt_request_response_client *client, + const struct aws_mqtt_streaming_operation_options *streaming_options) { + + if (client == NULL) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + if (!s_are_streaming_operation_options_valid(client, streaming_options)) { + /* all failure cases have logged the problem already */ + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + return NULL; + } + + struct aws_allocator *allocator = client->allocator; + struct aws_mqtt_rr_client_operation *operation = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_rr_client_operation)); + operation->allocator = allocator; + operation->type = AWS_MRROT_STREAMING; + operation->timeout_timepoint_ns = UINT64_MAX; + operation->pending_subscriptions = 1; + + s_aws_mqtt_streaming_operation_storage_init_from_options( + &operation->storage.streaming_storage, allocator, streaming_options); + s_aws_mqtt_rr_client_operation_init_shared(operation, client); + + AWS_LOGF_INFO( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client - submitting streaming operation with id %" PRIu64, + (void *)client, + operation->id); + + s_log_streaming_operation(operation, client); + + return operation; +} + +int aws_mqtt_rr_client_operation_activate(struct aws_mqtt_rr_client_operation *operation) { + struct aws_atomic_var *activated = &operation->storage.streaming_storage.activated; + size_t unactivated = 0; + if (!aws_atomic_compare_exchange_int(activated, &unactivated, 1)) { + return aws_raise_error(AWS_ERROR_MQTT_REUQEST_RESPONSE_STREAM_ALREADY_ACTIVATED); + } + + struct aws_mqtt_request_response_client *rr_client = operation->client_internal_ref; + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "id=%p: request-response client - activating streaming operation with id %" PRIu64, + (void *)rr_client, + operation->id); + + /* + * We hold a second reference to the operation during submission. This ensures that even if a streaming operation + * is immediately dec-refed by the creator (before submission runs), the operation will not get destroyed. + */ + aws_mqtt_rr_client_operation_acquire(operation); + + aws_event_loop_schedule_task_now(rr_client->loop, &operation->submit_task); + + return AWS_OP_SUCCESS; +} + +struct aws_mqtt_rr_client_operation *aws_mqtt_rr_client_operation_acquire( + struct aws_mqtt_rr_client_operation *operation) { + if (operation != NULL) { + aws_ref_count_acquire(&operation->ref_count); + } + + return operation; +} + +struct aws_mqtt_rr_client_operation *aws_mqtt_rr_client_operation_release( + struct aws_mqtt_rr_client_operation *operation) { + if (operation != NULL) { + aws_ref_count_release(&operation->ref_count); + } + + return NULL; +} + +struct aws_event_loop *aws_mqtt_request_response_client_get_event_loop( + struct aws_mqtt_request_response_client *client) { + + return aws_mqtt_protocol_adapter_get_event_loop(client->client_adapter); +} diff --git a/source/request-response/subscription_manager.c b/source/request-response/subscription_manager.c new file mode 100644 index 00000000..88cc5428 --- /dev/null +++ b/source/request-response/subscription_manager.c @@ -0,0 +1,822 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include +#include +#include + +#include + +enum aws_rr_subscription_status_type { + ARRSST_SUBSCRIBED, + ARRSST_NOT_SUBSCRIBED, +}; + +/* + * Invariant: subscriptions can only transition from nothing -> {subscribing, unsubscribing} + * + * In particular, the logic blocks subscribing while unsubscribing and unsubscribing while subscribing (unless + * shutting down). + */ +enum aws_rr_subscription_pending_action_type { + ARRSPAT_NOTHING, + ARRSPAT_SUBSCRIBING, + ARRSPAT_UNSUBSCRIBING, +}; + +struct aws_rr_subscription_listener { + struct aws_allocator *allocator; + uint64_t operation_id; +}; + +static uint64_t s_aws_hash_subscription_listener(const void *item) { + const struct aws_rr_subscription_listener *listener = item; + + return listener->operation_id; +} + +static bool s_aws_subscription_listener_hash_equality(const void *a, const void *b) { + const struct aws_rr_subscription_listener *a_listener = a; + const struct aws_rr_subscription_listener *b_listener = b; + + return a_listener->operation_id == b_listener->operation_id; +} + +static void s_aws_subscription_listener_destroy(void *element) { + struct aws_rr_subscription_listener *listener = element; + + aws_mem_release(listener->allocator, listener); +} + +struct aws_rr_subscription_record { + struct aws_allocator *allocator; + + struct aws_byte_buf topic_filter; + struct aws_byte_cursor topic_filter_cursor; + + struct aws_hash_table listeners; + + enum aws_rr_subscription_status_type status; + enum aws_rr_subscription_pending_action_type pending_action; + + enum aws_rr_subscription_type type; + + /* + * A poisoned record represents a subscription that we will never try to subscribe to because a previous + * attempt resulted in a failure that we judge to be "terminal." Terminal failures include permission failures + * and validation failures. To remove a poisoned record, all listeners must be removed. For request-response + * operations this will happen naturally. For streaming operations, the operation must be closed by the user (in + * response to the user-facing event we emit on the streaming operation when the failure that poisons the + * record occurs). + */ + bool poisoned; +}; + +static void s_aws_rr_subscription_record_destroy(void *element) { + struct aws_rr_subscription_record *record = element; + + aws_byte_buf_clean_up(&record->topic_filter); + aws_hash_table_clean_up(&record->listeners); + + aws_mem_release(record->allocator, record); +} + +static struct aws_rr_subscription_record *s_aws_rr_subscription_new( + struct aws_allocator *allocator, + struct aws_byte_cursor topic_filter, + enum aws_rr_subscription_type type) { + struct aws_rr_subscription_record *record = aws_mem_calloc(allocator, 1, sizeof(struct aws_rr_subscription_record)); + record->allocator = allocator; + + aws_byte_buf_init_copy_from_cursor(&record->topic_filter, allocator, topic_filter); + record->topic_filter_cursor = aws_byte_cursor_from_buf(&record->topic_filter); + + aws_hash_table_init( + &record->listeners, + allocator, + 4, + s_aws_hash_subscription_listener, + s_aws_subscription_listener_hash_equality, + NULL, + s_aws_subscription_listener_destroy); + + record->status = ARRSST_NOT_SUBSCRIBED; + record->pending_action = ARRSPAT_NOTHING; + + record->type = type; + + return record; +} + +static void s_subscription_record_unsubscribe( + struct aws_rr_subscription_manager *manager, + struct aws_rr_subscription_record *record, + bool shutdown) { + + bool currently_subscribed = record->status == ARRSST_SUBSCRIBED; + bool currently_subscribing = record->pending_action == ARRSPAT_SUBSCRIBING; + bool currently_unsubscribing = record->pending_action == ARRSPAT_UNSUBSCRIBING; + + /* + * The difference between a shutdown unsubscribe and a normal unsubscribe is that on a shutdown we will "chase" + * a pending subscribe with an unsubscribe (breaking the invariant of never having multiple MQTT operations + * pending on a subscription). + */ + bool should_unsubscribe = currently_subscribed && !currently_unsubscribing; + if (shutdown) { + should_unsubscribe = should_unsubscribe || currently_subscribing; + } + + if (!should_unsubscribe) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - subscription ('" PRInSTR + "') has no listeners but is not in a state that allows unsubscribe yet", + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor)); + return; + } + + struct aws_protocol_adapter_unsubscribe_options unsubscribe_options = { + .topic_filter = record->topic_filter_cursor, + .ack_timeout_seconds = manager->config.operation_timeout_seconds, + }; + + if (aws_mqtt_protocol_adapter_unsubscribe(manager->protocol_adapter, &unsubscribe_options)) { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - sync unsubscribe failure for ('" PRInSTR "'), ec %d(%s)", + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor), + error_code, + aws_error_debug_str(error_code)); + return; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - unsubscribe submitted for ('" PRInSTR "')", + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor)); + + record->pending_action = ARRSPAT_UNSUBSCRIBING; +} + +/* Only called by the request-response client when shutting down */ +static int s_rr_subscription_clean_up_foreach_wrap(void *context, struct aws_hash_element *elem) { + struct aws_rr_subscription_manager *manager = context; + struct aws_rr_subscription_record *subscription = elem->value; + + s_subscription_record_unsubscribe(manager, subscription, true); + s_aws_rr_subscription_record_destroy(subscription); + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE | AWS_COMMON_HASH_TABLE_ITER_DELETE; +} + +static struct aws_rr_subscription_record *s_get_subscription_record( + struct aws_rr_subscription_manager *manager, + struct aws_byte_cursor topic_filter) { + struct aws_rr_subscription_record *subscription = NULL; + struct aws_hash_element *element = NULL; + if (aws_hash_table_find(&manager->subscriptions, &topic_filter, &element)) { + return NULL; + } + + if (element != NULL) { + subscription = element->value; + } + + return subscription; +} + +struct aws_subscription_stats { + size_t request_response_subscriptions; + size_t event_stream_subscriptions; + size_t unsubscribing_event_stream_subscriptions; +}; + +static int s_rr_subscription_count_foreach_wrap(void *context, struct aws_hash_element *elem) { + const struct aws_rr_subscription_record *subscription = elem->value; + struct aws_subscription_stats *stats = context; + + if (subscription->type == ARRST_EVENT_STREAM) { + ++stats->event_stream_subscriptions; + if (subscription->pending_action == ARRSPAT_UNSUBSCRIBING) { + ++stats->unsubscribing_event_stream_subscriptions; + } + } else { + ++stats->request_response_subscriptions; + } + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +static void s_get_subscription_stats( + struct aws_rr_subscription_manager *manager, + struct aws_subscription_stats *stats) { + AWS_ZERO_STRUCT(*stats); + + aws_hash_table_foreach(&manager->subscriptions, s_rr_subscription_count_foreach_wrap, stats); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager current stats: %d event stream sub records, %d request-response sub " + "records, %d unsubscribing event stream subscriptions", + (int)stats->event_stream_subscriptions, + (int)stats->request_response_subscriptions, + (int)stats->unsubscribing_event_stream_subscriptions); +} + +static void s_remove_listener_from_subscription_record( + struct aws_rr_subscription_manager *manager, + struct aws_byte_cursor topic_filter, + uint64_t operation_id) { + struct aws_rr_subscription_record *record = s_get_subscription_record(manager, topic_filter); + if (record == NULL) { + return; + } + + struct aws_rr_subscription_listener listener = { + .operation_id = operation_id, + }; + + aws_hash_table_remove(&record->listeners, &listener, NULL, NULL); + + size_t listener_count = aws_hash_table_get_entry_count(&record->listeners); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - removed listener %" PRIu64 " from subscription ('" PRInSTR + "'), %zu listeners left", + operation_id, + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor), + listener_count); + + if (listener_count == 0) { + struct aws_rr_subscription_status_event event = { + .type = ARRSET_SUBSCRIPTION_EMPTY, + .topic_filter = record->topic_filter_cursor, + .operation_id = 0, + }; + + (*manager->config.subscription_status_callback)(&event, manager->config.userdata); + } +} + +static void s_add_listener_to_subscription_record(struct aws_rr_subscription_record *record, uint64_t operation_id) { + struct aws_rr_subscription_listener *listener = + aws_mem_calloc(record->allocator, 1, sizeof(struct aws_rr_subscription_listener)); + listener->allocator = record->allocator; + listener->operation_id = operation_id; + + aws_hash_table_put(&record->listeners, listener, listener, NULL); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - added listener %" PRIu64 " to subscription ('" PRInSTR + "'), %zu listeners total", + operation_id, + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor), + aws_hash_table_get_entry_count(&record->listeners)); +} + +static int s_rr_subscription_purge_unused_subscriptions_wrapper(void *context, struct aws_hash_element *elem) { + struct aws_rr_subscription_record *record = elem->value; + struct aws_rr_subscription_manager *manager = context; + + if (aws_hash_table_get_entry_count(&record->listeners) == 0) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - checking subscription ('" PRInSTR "') for removal", + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor)); + + if (manager->is_protocol_client_connected) { + s_subscription_record_unsubscribe(manager, record, false); + } + + if (record->status == ARRSST_NOT_SUBSCRIBED && record->pending_action == ARRSPAT_NOTHING) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - deleting subscription ('" PRInSTR "')", + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor)); + + s_aws_rr_subscription_record_destroy(record); + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE | AWS_COMMON_HASH_TABLE_ITER_DELETE; + } + } + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +void aws_rr_subscription_manager_purge_unused(struct aws_rr_subscription_manager *manager) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, "request-response subscription manager - purging unused subscriptions"); + aws_hash_table_foreach(&manager->subscriptions, s_rr_subscription_purge_unused_subscriptions_wrapper, manager); +} + +static const char *s_rr_subscription_event_type_to_c_str(enum aws_rr_subscription_event_type type) { + switch (type) { + case ARRSET_REQUEST_SUBSCRIBE_SUCCESS: + return "RequestSubscribeSuccess"; + + case ARRSET_REQUEST_SUBSCRIBE_FAILURE: + return "RequestSubscribeFailure"; + + case ARRSET_REQUEST_SUBSCRIPTION_ENDED: + return "RequestSubscriptionEnded"; + + case ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED: + return "StreamingSubscriptionEstablished"; + + case ARRSET_STREAMING_SUBSCRIPTION_LOST: + return "StreamingSubscriptionLost"; + + case ARRSET_STREAMING_SUBSCRIPTION_HALTED: + return "StreamingSubscriptionHalted"; + + case ARRSET_UNSUBSCRIBE_COMPLETE: + return "UnsubscribeComplete"; + + case ARRSET_SUBSCRIPTION_EMPTY: + return "SubscriptionEmpty"; + } + + return "Unknown"; +} + +static bool s_subscription_type_matches_event_type( + enum aws_rr_subscription_type subscription_type, + enum aws_rr_subscription_event_type event_type) { + switch (event_type) { + case ARRSET_REQUEST_SUBSCRIBE_SUCCESS: + case ARRSET_REQUEST_SUBSCRIBE_FAILURE: + case ARRSET_REQUEST_SUBSCRIPTION_ENDED: + return subscription_type == ARRST_REQUEST_RESPONSE; + + case ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED: + case ARRSET_STREAMING_SUBSCRIPTION_LOST: + case ARRSET_STREAMING_SUBSCRIPTION_HALTED: + return subscription_type == ARRST_EVENT_STREAM; + + default: + return true; + } +} + +static void s_emit_subscription_event( + const struct aws_rr_subscription_manager *manager, + const struct aws_rr_subscription_record *record, + enum aws_rr_subscription_event_type type) { + + AWS_FATAL_ASSERT(s_subscription_type_matches_event_type(record->type, type)); + + for (struct aws_hash_iter iter = aws_hash_iter_begin(&record->listeners); !aws_hash_iter_done(&iter); + aws_hash_iter_next(&iter)) { + + struct aws_rr_subscription_listener *listener = iter.element.value; + struct aws_rr_subscription_status_event event = { + .type = type, + .topic_filter = record->topic_filter_cursor, + .operation_id = listener->operation_id, + }; + + (*manager->config.subscription_status_callback)(&event, manager->config.userdata); + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - subscription event for ('" PRInSTR + "'), type: %s, operation: %" PRIu64 "", + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor), + s_rr_subscription_event_type_to_c_str(type), + listener->operation_id); + } +} + +static int s_rr_activate_idle_subscription( + struct aws_rr_subscription_manager *manager, + struct aws_rr_subscription_record *record) { + int result = AWS_OP_SUCCESS; + + if (record->poisoned) { + /* + * Don't try and establish poisoned subscriptions. This is not an error or a loggable event, it just means + * we hit a "try and make subscriptions" event when a poisoned subscription still hadn't been fully released. + */ + return AWS_OP_SUCCESS; + } + + if (manager->is_protocol_client_connected && aws_hash_table_get_entry_count(&record->listeners) > 0) { + if (record->status == ARRSST_NOT_SUBSCRIBED && record->pending_action == ARRSPAT_NOTHING) { + struct aws_protocol_adapter_subscribe_options subscribe_options = { + .topic_filter = record->topic_filter_cursor, + .ack_timeout_seconds = manager->config.operation_timeout_seconds, + }; + + result = aws_mqtt_protocol_adapter_subscribe(manager->protocol_adapter, &subscribe_options); + if (result == AWS_OP_SUCCESS) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - initiating subscribe operation for ('" PRInSTR "')", + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor)); + record->pending_action = ARRSPAT_SUBSCRIBING; + } else { + int error_code = aws_last_error(); + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - synchronous failure subscribing to ('" PRInSTR + "'), ec %d(%s)", + AWS_BYTE_CURSOR_PRI(record->topic_filter_cursor), + error_code, + aws_error_debug_str(error_code)); + + if (record->type == ARRST_REQUEST_RESPONSE) { + s_emit_subscription_event(manager, record, ARRSET_REQUEST_SUBSCRIBE_FAILURE); + } else { + record->poisoned = true; + s_emit_subscription_event(manager, record, ARRSET_STREAMING_SUBSCRIPTION_HALTED); + } + } + } + } + + return result; +} + +enum aws_acquire_subscription_result_type aws_rr_subscription_manager_acquire_subscription( + struct aws_rr_subscription_manager *manager, + const struct aws_rr_acquire_subscription_options *options) { + + if (options->topic_filter_count == 0) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire_subscription for operation %" PRIu64 + " with no topic filters", + options->operation_id); + return AASRT_FAILURE; + } + + /* + * Check for poisoned or mismatched records. This has precedence over the following unsubscribing check, + * and so we put them in separate loops + */ + for (size_t i = 0; i < options->topic_filter_count; ++i) { + struct aws_byte_cursor topic_filter = options->topic_filters[i]; + struct aws_rr_subscription_record *existing_record = s_get_subscription_record(manager, topic_filter); + if (existing_record == NULL) { + continue; + } + + if (existing_record->poisoned) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for ('" PRInSTR "'), operation %" PRIu64 + " failed - existing subscription is poisoned and has not been released", + AWS_BYTE_CURSOR_PRI(topic_filter), + options->operation_id); + return AASRT_FAILURE; + } + + if (existing_record->type != options->type) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for ('" PRInSTR "'), operation %" PRIu64 + " failed - conflicts with subscription type of existing subscription", + AWS_BYTE_CURSOR_PRI(topic_filter), + options->operation_id); + return AASRT_FAILURE; + } + } + + /* blocked if an existing record is unsubscribing; also compute how many subscriptions are needed */ + size_t subscriptions_needed = 0; + for (size_t i = 0; i < options->topic_filter_count; ++i) { + struct aws_byte_cursor topic_filter = options->topic_filters[i]; + struct aws_rr_subscription_record *existing_record = s_get_subscription_record(manager, topic_filter); + if (existing_record != NULL) { + if (existing_record->pending_action == ARRSPAT_UNSUBSCRIBING) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for ('" PRInSTR + "'), operation %" PRIu64 " blocked - existing subscription is unsubscribing", + AWS_BYTE_CURSOR_PRI(topic_filter), + options->operation_id); + return AASRT_BLOCKED; + } + } else { + ++subscriptions_needed; + } + } + + /* Check for space and fail or block as appropriate */ + if (subscriptions_needed > 0) { + /* how much of the budget are we using? */ + struct aws_subscription_stats stats; + s_get_subscription_stats(manager, &stats); + + if (options->type == ARRST_REQUEST_RESPONSE) { + if (subscriptions_needed > + manager->config.max_request_response_subscriptions - stats.request_response_subscriptions) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for request operation %" PRIu64 + " blocked - no room currently", + options->operation_id); + return AASRT_BLOCKED; + } + } else { + /* + * Streaming subscriptions have more complicated space-checking logic. Under certain conditions, we may + * block rather than failing + */ + if (subscriptions_needed + stats.event_stream_subscriptions > manager->config.max_streaming_subscriptions) { + if (subscriptions_needed + stats.event_stream_subscriptions <= + manager->config.max_streaming_subscriptions + stats.unsubscribing_event_stream_subscriptions) { + /* If enough subscriptions are in the process of going away then wait in the blocked state */ + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for streaming operation %" PRIu64 + " blocked - no room currently", + options->operation_id); + return AASRT_BLOCKED; + } else { + /* Otherwise, there's no hope, fail */ + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for operation %" PRIu64 + " failed - no room", + options->operation_id); + return AASRT_NO_CAPACITY; + } + } + } + } + + bool is_fully_subscribed = true; + for (size_t i = 0; i < options->topic_filter_count; ++i) { + struct aws_byte_cursor topic_filter = options->topic_filters[i]; + struct aws_rr_subscription_record *existing_record = s_get_subscription_record(manager, topic_filter); + + if (existing_record == NULL) { + existing_record = s_aws_rr_subscription_new(manager->allocator, topic_filter, options->type); + aws_hash_table_put(&manager->subscriptions, &existing_record->topic_filter_cursor, existing_record, NULL); + } + + s_add_listener_to_subscription_record(existing_record, options->operation_id); + if (existing_record->status != ARRSST_SUBSCRIBED) { + is_fully_subscribed = false; + } + } + + if (is_fully_subscribed) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for operation %" PRIu64 + " fully subscribed - all required subscriptions are active", + options->operation_id); + return AASRT_SUBSCRIBED; + } + + for (size_t i = 0; i < options->topic_filter_count; ++i) { + struct aws_byte_cursor topic_filter = options->topic_filters[i]; + struct aws_rr_subscription_record *existing_record = s_get_subscription_record(manager, topic_filter); + + if (s_rr_activate_idle_subscription(manager, existing_record)) { + AWS_LOGF_ERROR( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for operation %" PRIu64 + " failed - synchronous subscribe failure", + options->operation_id); + return AASRT_FAILURE; + } + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - acquire subscription for operation %" PRIu64 + " subscribing - waiting on one or more subscribes to complete", + options->operation_id); + + return AASRT_SUBSCRIBING; +} + +void aws_rr_subscription_manager_release_subscription( + struct aws_rr_subscription_manager *manager, + const struct aws_rr_release_subscription_options *options) { + for (size_t i = 0; i < options->topic_filter_count; ++i) { + struct aws_byte_cursor topic_filter = options->topic_filters[i]; + s_remove_listener_from_subscription_record(manager, topic_filter, options->operation_id); + } +} + +static void s_handle_protocol_adapter_request_subscription_event( + struct aws_rr_subscription_manager *manager, + struct aws_rr_subscription_record *record, + const struct aws_protocol_adapter_subscription_event *event) { + if (event->event_type == AWS_PASET_SUBSCRIBE) { + AWS_FATAL_ASSERT(record->pending_action == ARRSPAT_SUBSCRIBING); + record->pending_action = ARRSPAT_NOTHING; + + if (event->error_code == AWS_ERROR_SUCCESS) { + record->status = ARRSST_SUBSCRIBED; + s_emit_subscription_event(manager, record, ARRSET_REQUEST_SUBSCRIBE_SUCCESS); + } else { + s_emit_subscription_event(manager, record, ARRSET_REQUEST_SUBSCRIBE_FAILURE); + } + } else { + AWS_FATAL_ASSERT(event->event_type == AWS_PASET_UNSUBSCRIBE); + AWS_FATAL_ASSERT(record->pending_action == ARRSPAT_UNSUBSCRIBING); + record->pending_action = ARRSPAT_NOTHING; + + if (event->error_code == AWS_ERROR_SUCCESS) { + record->status = ARRSST_NOT_SUBSCRIBED; + + struct aws_rr_subscription_status_event unsubscribe_event = { + .type = ARRSET_UNSUBSCRIBE_COMPLETE, + .topic_filter = record->topic_filter_cursor, + .operation_id = 0, + }; + + (*manager->config.subscription_status_callback)(&unsubscribe_event, manager->config.userdata); + } + } +} + +static void s_handle_protocol_adapter_streaming_subscription_event( + struct aws_rr_subscription_manager *manager, + struct aws_rr_subscription_record *record, + const struct aws_protocol_adapter_subscription_event *event) { + if (event->event_type == AWS_PASET_SUBSCRIBE) { + AWS_FATAL_ASSERT(record->pending_action == ARRSPAT_SUBSCRIBING); + record->pending_action = ARRSPAT_NOTHING; + + if (event->error_code == AWS_ERROR_SUCCESS) { + record->status = ARRSST_SUBSCRIBED; + s_emit_subscription_event(manager, record, ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED); + } else { + if (event->retryable) { + s_rr_activate_idle_subscription(manager, record); + } else { + record->poisoned = true; + s_emit_subscription_event(manager, record, ARRSET_STREAMING_SUBSCRIPTION_HALTED); + } + } + } else { + AWS_FATAL_ASSERT(event->event_type == AWS_PASET_UNSUBSCRIBE); + AWS_FATAL_ASSERT(record->pending_action == ARRSPAT_UNSUBSCRIBING); + record->pending_action = ARRSPAT_NOTHING; + + if (event->error_code == AWS_ERROR_SUCCESS) { + record->status = ARRSST_NOT_SUBSCRIBED; + + struct aws_rr_subscription_status_event unsubscribe_event = { + .type = ARRSET_UNSUBSCRIBE_COMPLETE, + .topic_filter = record->topic_filter_cursor, + .operation_id = 0, + }; + + (*manager->config.subscription_status_callback)(&unsubscribe_event, manager->config.userdata); + } + } +} + +void aws_rr_subscription_manager_on_protocol_adapter_subscription_event( + struct aws_rr_subscription_manager *manager, + const struct aws_protocol_adapter_subscription_event *event) { + struct aws_rr_subscription_record *record = s_get_subscription_record(manager, event->topic_filter); + if (record == NULL) { + return; + } + + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - received a protocol adapter subscription event for ('" PRInSTR + "'), type %s, error_code %d(%s)", + AWS_BYTE_CURSOR_PRI(event->topic_filter), + aws_protocol_adapter_subscription_event_type_to_c_str(event->event_type), + event->error_code, + aws_error_debug_str(event->error_code)); + + if (record->type == ARRST_REQUEST_RESPONSE) { + s_handle_protocol_adapter_request_subscription_event(manager, record, event); + } else { + s_handle_protocol_adapter_streaming_subscription_event(manager, record, event); + } +} + +static int s_rr_activate_idle_subscriptions_wrapper(void *context, struct aws_hash_element *elem) { + struct aws_rr_subscription_record *record = elem->value; + struct aws_rr_subscription_manager *manager = context; + + s_rr_activate_idle_subscription(manager, record); + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +static void s_activate_idle_subscriptions(struct aws_rr_subscription_manager *manager) { + aws_hash_table_foreach(&manager->subscriptions, s_rr_activate_idle_subscriptions_wrapper, manager); +} + +static int s_apply_session_lost_wrapper(void *context, struct aws_hash_element *elem) { + struct aws_rr_subscription_record *record = elem->value; + struct aws_rr_subscription_manager *manager = context; + + if (record->status == ARRSST_SUBSCRIBED) { + record->status = ARRSST_NOT_SUBSCRIBED; + + if (record->type == ARRST_REQUEST_RESPONSE) { + s_emit_subscription_event(manager, record, ARRSET_REQUEST_SUBSCRIPTION_ENDED); + + if (record->pending_action != ARRSPAT_UNSUBSCRIBING) { + s_aws_rr_subscription_record_destroy(record); + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE | AWS_COMMON_HASH_TABLE_ITER_DELETE; + } + } else { + s_emit_subscription_event(manager, record, ARRSET_STREAMING_SUBSCRIPTION_LOST); + } + } + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +static int s_apply_streaming_resubscribe_wrapper(void *context, struct aws_hash_element *elem) { + struct aws_rr_subscription_record *record = elem->value; + struct aws_rr_subscription_manager *manager = context; + + if (record->type == ARRST_EVENT_STREAM) { + s_rr_activate_idle_subscription(manager, record); + } + + return AWS_COMMON_HASH_TABLE_ITER_CONTINUE; +} + +static void s_apply_session_lost(struct aws_rr_subscription_manager *manager) { + aws_hash_table_foreach(&manager->subscriptions, s_apply_session_lost_wrapper, manager); + aws_hash_table_foreach(&manager->subscriptions, s_apply_streaming_resubscribe_wrapper, manager); +} + +void aws_rr_subscription_manager_on_protocol_adapter_connection_event( + struct aws_rr_subscription_manager *manager, + const struct aws_protocol_adapter_connection_event *event) { + + if (event->event_type == AWS_PACET_CONNECTED) { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - received a protocol adapter connection event, joined_session: " + "%d", + (int)(event->joined_session ? 1 : 0)); + + manager->is_protocol_client_connected = true; + if (!event->joined_session) { + s_apply_session_lost(manager); + } + + aws_rr_subscription_manager_purge_unused(manager); + s_activate_idle_subscriptions(manager); + } else { + AWS_LOGF_DEBUG( + AWS_LS_MQTT_REQUEST_RESPONSE, + "request-response subscription manager - received a protocol adapter disconnection event"); + + manager->is_protocol_client_connected = false; + } +} + +bool aws_rr_subscription_manager_are_options_valid(const struct aws_rr_subscription_manager_options *options) { + if (options == NULL || options->max_request_response_subscriptions < 2 || options->operation_timeout_seconds == 0) { + return false; + } + + return true; +} + +void aws_rr_subscription_manager_init( + struct aws_rr_subscription_manager *manager, + struct aws_allocator *allocator, + struct aws_mqtt_protocol_adapter *protocol_adapter, + const struct aws_rr_subscription_manager_options *options) { + AWS_ZERO_STRUCT(*manager); + + AWS_FATAL_ASSERT(aws_rr_subscription_manager_are_options_valid(options)); + + manager->allocator = allocator; + manager->config = *options; + manager->protocol_adapter = protocol_adapter; + + aws_hash_table_init( + &manager->subscriptions, + allocator, + options->max_request_response_subscriptions + options->max_streaming_subscriptions, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_aws_rr_subscription_record_destroy); + + manager->is_protocol_client_connected = aws_mqtt_protocol_adapter_is_connected(protocol_adapter); +} + +void aws_rr_subscription_manager_clean_up(struct aws_rr_subscription_manager *manager) { + aws_hash_table_foreach(&manager->subscriptions, s_rr_subscription_clean_up_foreach_wrap, manager); + aws_hash_table_clean_up(&manager->subscriptions); +} diff --git a/source/shared_constants.c b/source/shared.c similarity index 95% rename from source/shared_constants.c rename to source/shared.c index 8d260799..e84c4afe 100644 --- a/source/shared_constants.c +++ b/source/shared.c @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0. */ -#include +#include #include diff --git a/source/v5/mqtt5_client.c b/source/v5/mqtt5_client.c index e6da6d3d..33bad2e6 100644 --- a/source/v5/mqtt5_client.c +++ b/source/v5/mqtt5_client.c @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/source/v5/mqtt5_listener.c b/source/v5/mqtt5_listener.c index 04170394..ebb45b81 100644 --- a/source/v5/mqtt5_listener.c +++ b/source/v5/mqtt5_listener.c @@ -84,6 +84,7 @@ struct aws_mqtt5_listener *aws_mqtt5_listener_new( struct aws_allocator *allocator, struct aws_mqtt5_listener_config *config) { if (config->client == NULL) { + aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); return NULL; } diff --git a/source/v5/mqtt5_to_mqtt3_adapter.c b/source/v5/mqtt5_to_mqtt3_adapter.c index bd90a658..691d2c9a 100644 --- a/source/v5/mqtt5_to_mqtt3_adapter.c +++ b/source/v5/mqtt5_to_mqtt3_adapter.c @@ -2850,6 +2850,18 @@ static uint16_t s_aws_mqtt_5_resubscribe_existing_topics( return 0; } +static enum aws_mqtt311_impl_type s_aws_mqtt_client_connection_5_get_impl(const void *impl) { + (void)impl; + + return AWS_MQTT311_IT_5_ADAPTER; +} + +static struct aws_event_loop *s_aws_mqtt_client_connection_5_get_event_loop(const void *impl) { + const struct aws_mqtt_client_connection_5_impl *adapter = impl; + + return adapter->client->loop; +} + static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_vtable = { .acquire_fn = s_aws_mqtt_client_connection_5_acquire, .release_fn = s_aws_mqtt_client_connection_5_release, @@ -2873,6 +2885,8 @@ static struct aws_mqtt_client_connection_vtable s_aws_mqtt_client_connection_5_v .unsubscribe_fn = s_aws_mqtt_client_connection_5_unsubscribe, .publish_fn = s_aws_mqtt_client_connection_5_publish, .get_stats_fn = s_aws_mqtt_client_connection_5_get_stats, + .get_impl_type = s_aws_mqtt_client_connection_5_get_impl, + .get_event_loop = s_aws_mqtt_client_connection_5_get_event_loop, }; static struct aws_mqtt_client_connection_vtable *s_aws_mqtt_client_connection_5_vtable_ptr = diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 46bbc2b3..67aeb1e7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,8 +3,8 @@ include(AwsTestHarness) include(AwsLibFuzzer) enable_testing() -file(GLOB TEST_HDRS "v3/*.h v5/*.h") -set(TEST_SRC v3/*.c v5/*.c *.c) +file(GLOB TEST_HDRS "v3/*.h" "v5/*.h" "request-response/*.h") +set(TEST_SRC "v3/*.c" "v5/*.c" "request-response/*.c" "*.c") file(GLOB TESTS ${TEST_HDRS} ${TEST_SRC}) add_test_case(mqtt_packet_puback) @@ -78,8 +78,14 @@ add_test_case(mqtt_clean_session_not_retry) add_test_case(mqtt_clean_session_discard_previous) add_test_case(mqtt_clean_session_keep_next_session) add_test_case(mqtt_connection_publish_QoS1_timeout) +add_test_case(mqtt_connection_publish_QoS1_timeout_override) +add_test_case(mqtt_connection_unsubscribe_timeout) +add_test_case(mqtt_connection_unsubscribe_timeout_override) +add_test_case(mqtt_connection_subscribe_single_timeout) +add_test_case(mqtt_connection_subscribe_single_timeout_override) +add_test_case(mqtt_connection_subscribe_multi_timeout) +add_test_case(mqtt_connection_resubscribe_timeout) add_test_case(mqtt_connection_publish_QoS1_timeout_with_ping) -add_test_case(mqtt_connection_unsub_timeout) add_test_case(mqtt_connection_publish_QoS1_timeout_connection_lost_reset_time) add_test_case(mqtt_connection_ping_norm) add_test_case(mqtt_connection_ping_no) @@ -445,6 +451,132 @@ add_test_case(mqtt_subscription_set_publish_single_level_wildcards) add_test_case(mqtt_subscription_set_publish_multi_level_wildcards) add_test_case(mqtt_subscription_set_get_subscriptions) +add_test_case(request_response_mqtt5_protocol_adapter_subscribe_success) +add_test_case(request_response_mqtt5_protocol_adapter_subscribe_failure_error_code) +add_test_case(request_response_mqtt5_protocol_adapter_subscribe_failure_reason_code) +add_test_case(request_response_mqtt5_protocol_adapter_subscribe_failure_timeout) +add_test_case(request_response_mqtt5_protocol_adapter_unsubscribe_success) +add_test_case(request_response_mqtt5_protocol_adapter_unsubscribe_failure_error_code) +add_test_case(request_response_mqtt5_protocol_adapter_unsubscribe_failure_reason_code_retryable) +add_test_case(request_response_mqtt5_protocol_adapter_unsubscribe_failure_reason_code_not_retryable) +add_test_case(request_response_mqtt5_protocol_adapter_unsubscribe_failure_timeout) +add_test_case(request_response_mqtt5_protocol_adapter_publish_success) +add_test_case(request_response_mqtt5_protocol_adapter_publish_failure_error_code) +add_test_case(request_response_mqtt5_protocol_adapter_publish_failure_reason_code) +add_test_case(request_response_mqtt5_protocol_adapter_publish_failure_timeout) +add_test_case(request_response_mqtt5_protocol_adapter_connection_event_connect_no_session) +add_test_case(request_response_mqtt5_protocol_adapter_connection_event_connect_session) +add_test_case(request_response_mqtt5_protocol_adapter_incoming_publish) +add_test_case(request_response_mqtt5_protocol_adapter_shutdown_while_pending) + +add_test_case(request_response_mqtt311_protocol_adapter_subscribe_success) +add_test_case(request_response_mqtt311_protocol_adapter_subscribe_failure_timeout) +add_test_case(request_response_mqtt311_protocol_adapter_subscribe_failure_reason_code) +add_test_case(request_response_mqtt311_protocol_adapter_unsubscribe_success) +add_test_case(request_response_mqtt311_protocol_adapter_unsubscribe_failure_timeout) +add_test_case(request_response_mqtt311_protocol_adapter_publish_success) +add_test_case(request_response_mqtt311_protocol_adapter_publish_failure_timeout) +add_test_case(request_response_mqtt311_protocol_adapter_connection_events) +add_test_case(request_response_mqtt311_protocol_adapter_incoming_publish) +add_test_case(request_response_mqtt311_protocol_adapter_shutdown_while_pending) + +add_test_case(mqtt311_listener_connection_events_no_session) +add_test_case(mqtt311_listener_connection_events_with_session) +add_test_case(mqtt311_listener_publish_event) + +# "rrsm" = "request_response_subscription_manager" +# these tests tend to have longer operation sequences and checks in their names +# more details about each test can be found at the top of each test function +add_test_case(rrsm_acquire_subscribing) +add_test_case(rrsm_acquire_multi_subscribing) +add_test_case(rrsm_acquire_existing_subscribing) +add_test_case(rrsm_acquire_multi_existing_subscribing) +add_test_case(rrsm_acquire_multi_partially_subscribed) +add_test_case(rrsm_acquire_existing_subscribed) +add_test_case(rrsm_acquire_multi_existing_subscribed) +add_test_case(rrsm_acquire_blocked_rr) +add_test_case(rrsm_acquire_multi_blocked_rr) +add_test_case(rrsm_acquire_blocked_eventstream) +add_test_case(rrsm_acquire_multi_blocked_eventstream) +add_test_case(rrsm_acquire_no_capacity_max1) +add_test_case(rrsm_acquire_no_capacity_too_many_event_stream) +add_test_case(rrsm_acquire_multi_no_capacity_event_stream) +add_test_case(rrsm_acquire_failure_mixed_subscription_types) +add_test_case(rrsm_acquire_multi_failure_mixed_subscription_types) +add_test_case(rrsm_acquire_failure_poisoned) +add_test_case(rrsm_release_unsubscribes_request) +add_test_case(rrsm_release_unsubscribes_streaming) +add_test_case(rrsm_release_multi_unsubscribes_request) +add_test_case(rrsm_release_unsubscribe_success_clears_space) +add_test_case(rrsm_release_unsubscribe_failure_blocked) +add_test_case(rrsm_acquire_failure_subscribe_sync_failure_request) +add_test_case(rrsm_acquire_failure_subscribe_sync_failure_streaming) +add_test_case(rrsm_acquire_request_subscribe_failure_event) +add_test_case(rrsm_acquire_streaming_subscribe_failure_retryable_resubscribe) +add_test_case(rrsm_offline_acquire_request_online_success) +add_test_case(rrsm_offline_acquire_request_online_failure) +add_test_case(rrsm_offline_acquire_streaming_online_success) +add_test_case(rrsm_offline_acquire_streaming_online_failure) +add_test_case(rrsm_offline_acquire_release_request_online) +add_test_case(rrsm_offline_acquire_release_streaming_online) +add_test_case(rrsm_acquire_request_success_offline_release_acquire2_no_unsubscribe) +add_test_case(rrsm_acquire_streaming_success_offline_release_acquire2_no_unsubscribe) +add_test_case(rrsm_acquire_request_success_clean_up_unsubscribe_override) +add_test_case(rrsm_acquire_streaming_success_clean_up_unsubscribe_override) +add_test_case(rrsm_acquire_request_pending_clean_up_unsubscribe_override) +add_test_case(rrsm_acquire_streaming_pending_clean_up_unsubscribe_override) +add_test_case(rrsm_offline_acquire_request_pending_clean_up_unsubscribe_override) +add_test_case(rrsm_offline_acquire_streaming_pending_clean_up_unsubscribe_override) +add_test_case(rrsm_acquire_request_success_offline_online_no_session_subscription_ended_can_reacquire) +add_test_case(rrsm_request_subscription_ended_while_unsubscribing) +add_test_case(rrsm_streaming_subscription_lost_resubscribe_on_no_session) +add_test_case(rrsm_request_subscription_purge_events) +add_test_case(rrsm_streaming_subscription_purge_events) + +# "rrc" = request response client +add_test_case(rrc_mqtt5_create_destroy) +add_test_case(rrc_mqtt311_create_destroy) + +add_test_case(rrc_submit_request_operation_failure_no_response_paths) +add_test_case(rrc_submit_request_operation_failure_invalid_response_topic) +add_test_case(rrc_submit_request_operation_failure_invalid_publish_topic) +add_test_case(rrc_submit_request_operation_failure_invalid_subscription_topic_filter) +add_test_case(rrc_submit_request_operation_failure_no_subscription_topic_filters) +add_test_case(rrc_submit_request_operation_failure_empty_request) +add_test_case(rrc_submit_streaming_operation_failure_invalid_subscription_topic_filter) + +add_test_case(rrc_submit_request_operation_failure_by_shutdown) +add_test_case(rrc_create_streaming_operation_and_shutdown) +add_test_case(rrc_activate_streaming_operation_and_shutdown) +add_test_case(rrc_submit_request_operation_failure_by_timeout) + +add_test_case(rrc_streaming_operation_success_single) +add_test_case(rrc_streaming_operation_success_overlapping) +add_test_case(rrc_streaming_operation_success_starting_offline) +add_test_case(rrc_streaming_operation_clean_session_reestablish_subscription) +add_test_case(rrc_streaming_operation_resume_session) +add_test_case(rrc_streaming_operation_first_subscribe_times_out_resub_succeeds) +add_test_case(rrc_streaming_operation_first_subscribe_retryable_failure_resub_succeeds) +add_test_case(rrc_streaming_operation_subscribe_unretryable_failure) +add_test_case(rrc_streaming_operation_failure_exceeds_subscription_budget) +add_test_case(rrc_streaming_operation_success_delayed_by_request_operations) +add_test_case(rrc_streaming_operation_success_sandwiched_by_request_operations) + +add_test_case(rrc_request_response_success_response_path_accepted) +add_test_case(rrc_request_response_multi_sub_success_response_path_accepted) +add_test_case(rrc_request_response_success_response_path_rejected) +add_test_case(rrc_request_response_multi_sub_success_response_path_rejected) +add_test_case(rrc_request_response_success_empty_correlation_token) +add_test_case(rrc_request_response_success_empty_correlation_token_sequence) +add_test_case(rrc_request_response_subscribe_failure) +add_test_case(rrc_request_response_multi_subscribe_failure) +add_test_case(rrc_request_response_failure_puback_reason_code) +add_test_case(rrc_request_response_failure_invalid_payload) +add_test_case(rrc_request_response_failure_missing_correlation_token) +add_test_case(rrc_request_response_failure_invalid_correlation_token_type) +add_test_case(rrc_request_response_failure_non_matching_correlation_token) +add_test_case(rrc_request_response_multi_operation_sequence) + generate_test_driver(${PROJECT_NAME}-tests) set(TEST_PAHO_CLIENT_BINARY_NAME ${PROJECT_NAME}-paho-client) diff --git a/tests/request-response/protocol_adapter_tests.c b/tests/request-response/protocol_adapter_tests.c new file mode 100644 index 00000000..5b5bce58 --- /dev/null +++ b/tests/request-response/protocol_adapter_tests.c @@ -0,0 +1,1784 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "../v3/mqtt311_testing_utils.h" +#include "../v3/mqtt_mock_server_handler.h" +#include "../v5/mqtt5_testing_utils.h" +#include + +#include "aws/mqtt/private/request-response/protocol_adapter.h" + +#include + +struct request_response_protocol_adapter_incoming_publish_event_record { + struct aws_byte_buf topic; + struct aws_byte_buf payload; +}; + +static void s_request_response_protocol_adapter_incoming_publish_event_record_init( + struct request_response_protocol_adapter_incoming_publish_event_record *record, + struct aws_allocator *allocator, + struct aws_byte_cursor topic, + struct aws_byte_cursor payload) { + + aws_byte_buf_init_copy_from_cursor(&record->topic, allocator, topic); + aws_byte_buf_init_copy_from_cursor(&record->payload, allocator, payload); +} + +static void s_request_response_protocol_adapter_incoming_publish_event_record_clean_up( + struct request_response_protocol_adapter_incoming_publish_event_record *record) { + aws_byte_buf_clean_up(&record->topic); + aws_byte_buf_clean_up(&record->payload); +} + +struct request_response_protocol_adapter_subscription_event_record { + enum aws_protocol_adapter_subscription_event_type event_type; + struct aws_byte_buf topic_filter; + int error_code; + bool retryable; +}; + +static void s_request_response_protocol_adapter_subscription_event_record_init( + struct request_response_protocol_adapter_subscription_event_record *record, + struct aws_allocator *allocator, + enum aws_protocol_adapter_subscription_event_type event_type, + struct aws_byte_cursor topic_filter, + int error_code, + bool retryable) { + + AWS_ZERO_STRUCT(*record); + + record->event_type = event_type; + record->error_code = error_code; + record->retryable = retryable; + aws_byte_buf_init_copy_from_cursor(&record->topic_filter, allocator, topic_filter); +} + +static void s_request_response_protocol_adapter_subscription_event_record_cleanup( + struct request_response_protocol_adapter_subscription_event_record *record) { + aws_byte_buf_clean_up(&record->topic_filter); +} + +enum aws_protocol_adapter_protocol_type { + AWS_PAPT_MQTT5, + AWS_PAPT_MQTT311, +}; + +struct mqtt311_protocol_testing_context { + struct mqtt_connection_state_test mqtt311_test_context; + int mqtt311_test_context_setup_result; +}; + +struct aws_request_response_protocol_adapter_test_fixture { + struct aws_allocator *allocator; + + enum aws_protocol_adapter_protocol_type protocol_type; + + union { + struct aws_mqtt5_client_mock_test_fixture mqtt5_fixture; + struct mqtt311_protocol_testing_context mqtt311_fixture; + } protocol_context; + + struct aws_mqtt_protocol_adapter *protocol_adapter; + + struct aws_array_list incoming_publish_events; + struct aws_array_list connection_events; + struct aws_array_list subscription_events; + struct aws_array_list publish_results; + + bool adapter_terminated; + + struct aws_mutex lock; + struct aws_condition_variable signal; +}; + +static void s_rr_mqtt_protocol_adapter_test_on_subscription_event( + const struct aws_protocol_adapter_subscription_event *event, + void *user_data) { + struct aws_request_response_protocol_adapter_test_fixture *fixture = user_data; + + struct request_response_protocol_adapter_subscription_event_record record; + s_request_response_protocol_adapter_subscription_event_record_init( + &record, fixture->allocator, event->event_type, event->topic_filter, event->error_code, event->retryable); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->subscription_events, &record); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_rr_mqtt_protocol_adapter_test_on_incoming_publish( + const struct aws_protocol_adapter_incoming_publish_event *publish, + void *user_data) { + struct aws_request_response_protocol_adapter_test_fixture *fixture = user_data; + + struct request_response_protocol_adapter_incoming_publish_event_record record; + AWS_ZERO_STRUCT(record); + s_request_response_protocol_adapter_incoming_publish_event_record_init( + &record, fixture->allocator, publish->topic, publish->payload); + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->incoming_publish_events, &record); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_rr_mqtt_protocol_adapter_test_on_terminate_callback(void *user_data) { + struct aws_request_response_protocol_adapter_test_fixture *fixture = user_data; + + aws_mutex_lock(&fixture->lock); + fixture->adapter_terminated = true; + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_rr_mqtt_protocol_adapter_test_on_connection_event( + const struct aws_protocol_adapter_connection_event *event, + void *user_data) { + struct aws_request_response_protocol_adapter_test_fixture *fixture = user_data; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->connection_events, event); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_rr_mqtt_protocol_adapter_test_on_publish_result(int error_code, void *user_data) { + struct aws_request_response_protocol_adapter_test_fixture *fixture = user_data; + + aws_mutex_lock(&fixture->lock); + aws_array_list_push_back(&fixture->publish_results, &error_code); + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static int s_aws_request_response_protocol_adapter_test_fixture_init_shared( + struct aws_request_response_protocol_adapter_test_fixture *fixture, + enum aws_protocol_adapter_protocol_type protocol_type) { + + struct aws_allocator *allocator = fixture->allocator; + fixture->protocol_type = protocol_type; + + struct aws_mqtt_protocol_adapter_options protocol_adapter_options = { + .subscription_event_callback = s_rr_mqtt_protocol_adapter_test_on_subscription_event, + .incoming_publish_callback = s_rr_mqtt_protocol_adapter_test_on_incoming_publish, + .terminate_callback = s_rr_mqtt_protocol_adapter_test_on_terminate_callback, + .connection_event_callback = s_rr_mqtt_protocol_adapter_test_on_connection_event, + .user_data = fixture}; + + if (protocol_type == AWS_PAPT_MQTT5) { + fixture->protocol_adapter = aws_mqtt_protocol_adapter_new_from_5( + allocator, &protocol_adapter_options, fixture->protocol_context.mqtt5_fixture.client); + } else { + fixture->protocol_adapter = aws_mqtt_protocol_adapter_new_from_311( + allocator, + &protocol_adapter_options, + fixture->protocol_context.mqtt311_fixture.mqtt311_test_context.mqtt_connection); + } + AWS_FATAL_ASSERT(fixture->protocol_adapter != NULL); + + aws_array_list_init_dynamic( + &fixture->incoming_publish_events, + allocator, + 10, + sizeof(struct request_response_protocol_adapter_incoming_publish_event_record)); + aws_array_list_init_dynamic( + &fixture->connection_events, allocator, 10, sizeof(struct aws_protocol_adapter_connection_event)); + aws_array_list_init_dynamic( + &fixture->subscription_events, + allocator, + 10, + sizeof(struct request_response_protocol_adapter_subscription_event_record)); + aws_array_list_init_dynamic(&fixture->publish_results, allocator, 10, sizeof(int)); + + aws_mutex_init(&fixture->lock); + aws_condition_variable_init(&fixture->signal); + + return AWS_OP_SUCCESS; +} + +static int s_aws_request_response_protocol_adapter_test_fixture_init_mqtt5( + struct aws_request_response_protocol_adapter_test_fixture *fixture, + struct aws_allocator *allocator, + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *mqtt5_fixture_config) { + + AWS_ZERO_STRUCT(*fixture); + + fixture->allocator = allocator; + + if (aws_mqtt5_client_mock_test_fixture_init( + &fixture->protocol_context.mqtt5_fixture, allocator, mqtt5_fixture_config)) { + return AWS_OP_ERR; + } + + return s_aws_request_response_protocol_adapter_test_fixture_init_shared(fixture, AWS_PAPT_MQTT5); +} + +static int s_aws_request_response_protocol_adapter_test_fixture_init_mqtt311( + struct aws_request_response_protocol_adapter_test_fixture *fixture, + struct aws_allocator *allocator) { + + AWS_ZERO_STRUCT(*fixture); + + fixture->allocator = allocator; + + int result = + aws_test311_setup_mqtt_server_fn(allocator, &fixture->protocol_context.mqtt311_fixture.mqtt311_test_context); + ASSERT_SUCCESS(result); + fixture->protocol_context.mqtt311_fixture.mqtt311_test_context_setup_result = result; + + return s_aws_request_response_protocol_adapter_test_fixture_init_shared(fixture, AWS_PAPT_MQTT311); +} + +static bool s_is_adapter_terminated(void *context) { + struct aws_request_response_protocol_adapter_test_fixture *fixture = context; + + return fixture->adapter_terminated; +} + +static void s_aws_request_response_protocol_adapter_test_fixture_destroy_adapters( + struct aws_request_response_protocol_adapter_test_fixture *fixture) { + if (fixture->protocol_adapter != NULL) { + aws_mqtt_protocol_adapter_destroy(fixture->protocol_adapter); + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_is_adapter_terminated, fixture); + aws_mutex_unlock(&fixture->lock); + fixture->protocol_adapter = NULL; + } +} + +static void s_aws_request_response_protocol_adapter_test_fixture_clean_up( + struct aws_request_response_protocol_adapter_test_fixture *fixture) { + + s_aws_request_response_protocol_adapter_test_fixture_destroy_adapters(fixture); + + if (fixture->protocol_type == AWS_PAPT_MQTT5) { + aws_mqtt5_client_mock_test_fixture_clean_up(&fixture->protocol_context.mqtt5_fixture); + } else { + aws_test311_clean_up_mqtt_server_fn( + fixture->allocator, + fixture->protocol_context.mqtt311_fixture.mqtt311_test_context_setup_result, + &fixture->protocol_context.mqtt311_fixture.mqtt311_test_context); + } + + for (size_t i = 0; i < aws_array_list_length(&fixture->subscription_events); ++i) { + struct request_response_protocol_adapter_subscription_event_record record; + aws_array_list_get_at(&fixture->subscription_events, &record, i); + s_request_response_protocol_adapter_subscription_event_record_cleanup(&record); + } + aws_array_list_clean_up(&fixture->subscription_events); + + for (size_t i = 0; i < aws_array_list_length(&fixture->incoming_publish_events); ++i) { + struct request_response_protocol_adapter_incoming_publish_event_record record; + aws_array_list_get_at(&fixture->incoming_publish_events, &record, i); + s_request_response_protocol_adapter_incoming_publish_event_record_clean_up(&record); + } + aws_array_list_clean_up(&fixture->incoming_publish_events); + + aws_array_list_clean_up(&fixture->connection_events); + aws_array_list_clean_up(&fixture->publish_results); + + aws_mutex_clean_up(&fixture->lock); + aws_condition_variable_clean_up(&fixture->signal); +} + +struct test_subscription_event_wait_context { + struct request_response_protocol_adapter_subscription_event_record *expected_event; + size_t expected_count; + struct aws_request_response_protocol_adapter_test_fixture *fixture; +}; + +static bool s_do_subscription_events_contain(void *context) { + struct test_subscription_event_wait_context *wait_context = context; + + size_t found = 0; + + size_t num_events = aws_array_list_length(&wait_context->fixture->subscription_events); + for (size_t i = 0; i < num_events; ++i) { + struct request_response_protocol_adapter_subscription_event_record record; + aws_array_list_get_at(&wait_context->fixture->subscription_events, &record, i); + + if (record.event_type != wait_context->expected_event->event_type) { + continue; + } + + if (record.error_code != wait_context->expected_event->error_code) { + continue; + } + + struct aws_byte_cursor record_topic_filter = aws_byte_cursor_from_buf(&record.topic_filter); + struct aws_byte_cursor expected_topic_filter = + aws_byte_cursor_from_buf(&wait_context->expected_event->topic_filter); + if (!aws_byte_cursor_eq(&record_topic_filter, &expected_topic_filter)) { + continue; + } + + if (record.retryable != wait_context->expected_event->retryable) { + continue; + } + + ++found; + } + + return found >= wait_context->expected_count; +} + +static void s_wait_for_subscription_events_contains( + struct aws_request_response_protocol_adapter_test_fixture *fixture, + struct request_response_protocol_adapter_subscription_event_record *expected_event, + size_t expected_count) { + + struct test_subscription_event_wait_context context = { + .expected_event = expected_event, + .expected_count = expected_count, + .fixture = fixture, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_do_subscription_events_contain, &context); + aws_mutex_unlock(&fixture->lock); +} + +struct test_connection_event_wait_context { + struct aws_protocol_adapter_connection_event *expected_event; + size_t expected_count; + struct aws_request_response_protocol_adapter_test_fixture *fixture; +}; + +static bool s_do_connection_events_contain(void *context) { + struct test_connection_event_wait_context *wait_context = context; + + size_t found = 0; + + size_t num_events = aws_array_list_length(&wait_context->fixture->connection_events); + for (size_t i = 0; i < num_events; ++i) { + struct aws_protocol_adapter_connection_event record; + aws_array_list_get_at(&wait_context->fixture->connection_events, &record, i); + + if (record.event_type != wait_context->expected_event->event_type) { + continue; + } + + if (record.joined_session != wait_context->expected_event->joined_session) { + continue; + } + + ++found; + } + + return found >= wait_context->expected_count; +} + +static void s_wait_for_connection_events_contains( + struct aws_request_response_protocol_adapter_test_fixture *fixture, + struct aws_protocol_adapter_connection_event *expected_event, + size_t expected_count) { + + struct test_connection_event_wait_context context = { + .expected_event = expected_event, + .expected_count = expected_count, + .fixture = fixture, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_do_connection_events_contain, &context); + aws_mutex_unlock(&fixture->lock); +} + +struct test_incoming_publish_event_wait_context { + struct request_response_protocol_adapter_incoming_publish_event_record *expected_event; + size_t expected_count; + struct aws_request_response_protocol_adapter_test_fixture *fixture; +}; + +static bool s_do_incoming_publish_events_contain(void *context) { + struct test_incoming_publish_event_wait_context *wait_context = context; + + size_t found = 0; + + size_t num_events = aws_array_list_length(&wait_context->fixture->incoming_publish_events); + for (size_t i = 0; i < num_events; ++i) { + struct request_response_protocol_adapter_incoming_publish_event_record record; + aws_array_list_get_at(&wait_context->fixture->incoming_publish_events, &record, i); + + struct aws_byte_cursor record_topic = aws_byte_cursor_from_buf(&record.topic); + struct aws_byte_cursor expected_topic = aws_byte_cursor_from_buf(&wait_context->expected_event->topic); + if (!aws_byte_cursor_eq(&record_topic, &expected_topic)) { + continue; + } + + struct aws_byte_cursor record_payload = aws_byte_cursor_from_buf(&record.payload); + struct aws_byte_cursor expected_payload = aws_byte_cursor_from_buf(&wait_context->expected_event->payload); + if (!aws_byte_cursor_eq(&record_payload, &expected_payload)) { + continue; + } + + ++found; + } + + return found >= wait_context->expected_count; +} + +static void s_wait_for_incoming_publish_events_contains( + struct aws_request_response_protocol_adapter_test_fixture *fixture, + struct request_response_protocol_adapter_incoming_publish_event_record *expected_event, + size_t expected_count) { + + struct test_incoming_publish_event_wait_context context = { + .expected_event = expected_event, + .expected_count = expected_count, + .fixture = fixture, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_do_incoming_publish_events_contain, &context); + aws_mutex_unlock(&fixture->lock); +} + +struct test_publish_result_wait_context { + int expected_error_code; + size_t expected_count; + struct aws_request_response_protocol_adapter_test_fixture *fixture; +}; + +static bool s_do_publish_results_contain(void *context) { + struct test_publish_result_wait_context *wait_context = context; + + size_t found = 0; + + size_t num_events = aws_array_list_length(&wait_context->fixture->publish_results); + for (size_t i = 0; i < num_events; ++i) { + int error_code = AWS_ERROR_SUCCESS; + aws_array_list_get_at(&wait_context->fixture->publish_results, &error_code, i); + + if (error_code == wait_context->expected_error_code) { + ++found; + } + } + + return found >= wait_context->expected_count; +} + +static void s_wait_for_publish_results_contains( + struct aws_request_response_protocol_adapter_test_fixture *fixture, + int expected_error_code, + size_t expected_count) { + + struct test_publish_result_wait_context context = { + .expected_error_code = expected_error_code, + .expected_count = expected_count, + .fixture = fixture, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_do_publish_results_contain, &context); + aws_mutex_unlock(&fixture->lock); +} + +enum protocol_adapter_operation_test_type { + PAOTT_SUCCESS, + PAOTT_FAILURE_TIMEOUT, + PAOTT_FAILURE_REASON_CODE_RETRYABLE, + PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE, + PAOTT_FAILURE_ERROR_CODE, +}; + +static enum aws_mqtt5_suback_reason_code s_failed_suback_reason_codes[] = { + AWS_MQTT5_SARC_NOT_AUTHORIZED, +}; + +static int s_aws_mqtt5_server_send_failed_suback_on_subscribe( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_view->packet_id, + .reason_code_count = AWS_ARRAY_SIZE(s_failed_suback_reason_codes), + .reason_codes = s_failed_suback_reason_codes, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); +} + +static int s_test_type_to_expected_error_code( + enum protocol_adapter_operation_test_type test_type, + enum aws_protocol_adapter_protocol_type protocol_type) { + if (protocol_type == AWS_PAPT_MQTT5) { + switch (test_type) { + case PAOTT_FAILURE_TIMEOUT: + return AWS_ERROR_MQTT_TIMEOUT; + case PAOTT_FAILURE_REASON_CODE_RETRYABLE: + case PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE: + return AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE; + case PAOTT_FAILURE_ERROR_CODE: + return AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY; + default: + return AWS_ERROR_SUCCESS; + } + } else { + switch (test_type) { + case PAOTT_FAILURE_TIMEOUT: + return AWS_ERROR_MQTT_TIMEOUT; + case PAOTT_FAILURE_REASON_CODE_RETRYABLE: + case PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE: + return AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE; + case PAOTT_FAILURE_ERROR_CODE: + return AWS_ERROR_MQTT5_OPERATION_FAILED_DUE_TO_OFFLINE_QUEUE_POLICY; + default: + return AWS_ERROR_SUCCESS; + } + } +} + +static int s_do_request_response_mqtt5_protocol_adapter_subscribe_test( + struct aws_allocator *allocator, + enum protocol_adapter_operation_test_type test_type) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + if (test_type == PAOTT_SUCCESS) { + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + aws_mqtt5_server_send_suback_on_subscribe; + } else if ( + test_type == PAOTT_FAILURE_REASON_CODE_RETRYABLE || test_type == PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE) { + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_aws_mqtt5_server_send_failed_suback_on_subscribe; + } + + if (test_type == PAOTT_FAILURE_ERROR_CODE) { + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT; + } + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options mqtt5_test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt5( + &fixture, allocator, &mqtt5_test_fixture_options)); + + struct aws_mqtt5_client *client = fixture.protocol_context.mqtt5_fixture.client; + + if (test_type != PAOTT_FAILURE_ERROR_CODE) { + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&fixture.protocol_context.mqtt5_fixture); + } + + int expected_error_code = s_test_type_to_expected_error_code(test_type, AWS_PAPT_MQTT5); + + struct request_response_protocol_adapter_subscription_event_record expected_outcome; + s_request_response_protocol_adapter_subscription_event_record_init( + &expected_outcome, + allocator, + AWS_PASET_SUBSCRIBE, + aws_byte_cursor_from_c_str("hello/world"), + expected_error_code, + test_type != PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE); + + struct aws_protocol_adapter_subscribe_options subscribe_options = { + .topic_filter = aws_byte_cursor_from_buf(&expected_outcome.topic_filter), + .ack_timeout_seconds = 2, + }; + + aws_mqtt_protocol_adapter_subscribe(fixture.protocol_adapter, &subscribe_options); + + s_wait_for_subscription_events_contains(&fixture, &expected_outcome, 1); + + s_request_response_protocol_adapter_subscription_event_record_cleanup(&expected_outcome); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_request_response_mqtt5_protocol_adapter_subscribe_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_subscribe_test(allocator, PAOTT_SUCCESS)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_subscribe_success, + s_request_response_mqtt5_protocol_adapter_subscribe_success_fn) + +static int s_request_response_mqtt5_protocol_adapter_subscribe_failure_timeout_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_subscribe_test(allocator, PAOTT_FAILURE_TIMEOUT)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_subscribe_failure_timeout, + s_request_response_mqtt5_protocol_adapter_subscribe_failure_timeout_fn) + +static int s_request_response_mqtt5_protocol_adapter_subscribe_failure_reason_code_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_subscribe_test( + allocator, PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_subscribe_failure_reason_code, + s_request_response_mqtt5_protocol_adapter_subscribe_failure_reason_code_fn) + +static int s_request_response_mqtt5_protocol_adapter_subscribe_failure_error_code_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_subscribe_test(allocator, PAOTT_FAILURE_ERROR_CODE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_subscribe_failure_error_code, + s_request_response_mqtt5_protocol_adapter_subscribe_failure_error_code_fn) + +static enum aws_mqtt5_unsuback_reason_code s_retryable_failed_unsuback_reason_codes[] = { + AWS_MQTT5_UARC_IMPLEMENTATION_SPECIFIC_ERROR, +}; + +static int s_aws_mqtt5_server_send_retryable_failed_unsuback_on_unsubscribe( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_subscribe_view *unsubscribe_view = packet; + + struct aws_mqtt5_packet_unsuback_view unsuback_view = { + .packet_id = unsubscribe_view->packet_id, + .reason_code_count = AWS_ARRAY_SIZE(s_retryable_failed_unsuback_reason_codes), + .reason_codes = s_retryable_failed_unsuback_reason_codes, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); +} + +static enum aws_mqtt5_unsuback_reason_code s_not_retryable_failed_unsuback_reason_codes[] = { + AWS_MQTT5_UARC_NOT_AUTHORIZED, +}; + +static int s_aws_mqtt5_server_send_not_retryable_failed_unsuback_on_unsubscribe( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_subscribe_view *unsubscribe_view = packet; + + struct aws_mqtt5_packet_unsuback_view unsuback_view = { + .packet_id = unsubscribe_view->packet_id, + .reason_code_count = AWS_ARRAY_SIZE(s_not_retryable_failed_unsuback_reason_codes), + .reason_codes = s_not_retryable_failed_unsuback_reason_codes, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_UNSUBACK, &unsuback_view); +} + +static int s_do_request_response_mqtt5_protocol_adapter_unsubscribe_test( + struct aws_allocator *allocator, + enum protocol_adapter_operation_test_type test_type) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + if (test_type == PAOTT_SUCCESS) { + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success; + } else if (test_type == PAOTT_FAILURE_REASON_CODE_RETRYABLE) { + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + s_aws_mqtt5_server_send_retryable_failed_unsuback_on_unsubscribe; + } else if (test_type == PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE) { + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + s_aws_mqtt5_server_send_not_retryable_failed_unsuback_on_unsubscribe; + } + + if (test_type == PAOTT_FAILURE_ERROR_CODE) { + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT; + } + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options mqtt5_test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt5( + &fixture, allocator, &mqtt5_test_fixture_options)); + + struct aws_mqtt5_client *client = fixture.protocol_context.mqtt5_fixture.client; + + if (test_type != PAOTT_FAILURE_ERROR_CODE) { + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&fixture.protocol_context.mqtt5_fixture); + } + + int expected_error_code = s_test_type_to_expected_error_code(test_type, AWS_PAPT_MQTT5); + + struct request_response_protocol_adapter_subscription_event_record expected_outcome; + s_request_response_protocol_adapter_subscription_event_record_init( + &expected_outcome, + allocator, + AWS_PASET_UNSUBSCRIBE, + aws_byte_cursor_from_c_str("hello/world"), + expected_error_code, + test_type == PAOTT_FAILURE_REASON_CODE_RETRYABLE || test_type == PAOTT_FAILURE_TIMEOUT); + + struct aws_protocol_adapter_unsubscribe_options unsubscribe_options = { + .topic_filter = aws_byte_cursor_from_buf(&expected_outcome.topic_filter), + .ack_timeout_seconds = 2, + }; + + aws_mqtt_protocol_adapter_unsubscribe(fixture.protocol_adapter, &unsubscribe_options); + + s_wait_for_subscription_events_contains(&fixture, &expected_outcome, 1); + + s_request_response_protocol_adapter_subscription_event_record_cleanup(&expected_outcome); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_request_response_mqtt5_protocol_adapter_unsubscribe_success_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_unsubscribe_test(allocator, PAOTT_SUCCESS)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_unsubscribe_success, + s_request_response_mqtt5_protocol_adapter_unsubscribe_success_fn) + +static int s_request_response_mqtt5_protocol_adapter_unsubscribe_failure_timeout_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_unsubscribe_test(allocator, PAOTT_FAILURE_TIMEOUT)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_unsubscribe_failure_timeout, + s_request_response_mqtt5_protocol_adapter_unsubscribe_failure_timeout_fn) + +static int s_request_response_mqtt5_protocol_adapter_unsubscribe_failure_reason_code_retryable_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS( + s_do_request_response_mqtt5_protocol_adapter_unsubscribe_test(allocator, PAOTT_FAILURE_REASON_CODE_RETRYABLE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_unsubscribe_failure_reason_code_retryable, + s_request_response_mqtt5_protocol_adapter_unsubscribe_failure_reason_code_retryable_fn) + +static int s_request_response_mqtt5_protocol_adapter_unsubscribe_failure_reason_code_not_retryable_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_unsubscribe_test( + allocator, PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_unsubscribe_failure_reason_code_not_retryable, + s_request_response_mqtt5_protocol_adapter_unsubscribe_failure_reason_code_not_retryable_fn) + +static int s_request_response_mqtt5_protocol_adapter_unsubscribe_failure_error_code_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_unsubscribe_test(allocator, PAOTT_FAILURE_ERROR_CODE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_unsubscribe_failure_error_code, + s_request_response_mqtt5_protocol_adapter_unsubscribe_failure_error_code_fn) + +static int s_aws_mqtt5_server_send_failed_puback_on_publish( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + (void)user_data; + + struct aws_mqtt5_packet_publish_view *publish_view = packet; + + if (publish_view->qos == AWS_MQTT5_QOS_AT_LEAST_ONCE) { + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = publish_view->packet_id, + .reason_code = AWS_MQTT5_PARC_NOT_AUTHORIZED, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view); + } + + return AWS_OP_SUCCESS; +} + +static int s_do_request_response_mqtt5_protocol_adapter_publish_test( + struct aws_allocator *allocator, + enum protocol_adapter_operation_test_type test_type) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + if (test_type == PAOTT_SUCCESS) { + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_puback; + } else if ( + test_type == PAOTT_FAILURE_REASON_CODE_RETRYABLE || test_type == PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE) { + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + s_aws_mqtt5_server_send_failed_puback_on_publish; + } + + if (test_type == PAOTT_FAILURE_ERROR_CODE) { + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT; + } + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options mqtt5_test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt5( + &fixture, allocator, &mqtt5_test_fixture_options)); + + struct aws_mqtt5_client *client = fixture.protocol_context.mqtt5_fixture.client; + + if (test_type != PAOTT_FAILURE_ERROR_CODE) { + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&fixture.protocol_context.mqtt5_fixture); + } + + struct aws_protocol_adapter_publish_options publish_options = { + .topic = aws_byte_cursor_from_c_str("hello/world"), + .payload = aws_byte_cursor_from_c_str("SomePayload"), + .ack_timeout_seconds = 2, + .completion_callback_fn = s_rr_mqtt_protocol_adapter_test_on_publish_result, + .user_data = &fixture, + }; + + aws_mqtt_protocol_adapter_publish(fixture.protocol_adapter, &publish_options); + + int expected_error_code = s_test_type_to_expected_error_code(test_type, AWS_PAPT_MQTT5); + s_wait_for_publish_results_contains(&fixture, expected_error_code, 1); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_request_response_mqtt5_protocol_adapter_publish_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_publish_test(allocator, PAOTT_SUCCESS)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_publish_success, + s_request_response_mqtt5_protocol_adapter_publish_success_fn) + +static int s_request_response_mqtt5_protocol_adapter_publish_failure_timeout_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_publish_test(allocator, PAOTT_FAILURE_TIMEOUT)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_publish_failure_timeout, + s_request_response_mqtt5_protocol_adapter_publish_failure_timeout_fn) + +static int s_request_response_mqtt5_protocol_adapter_publish_failure_reason_code_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS( + s_do_request_response_mqtt5_protocol_adapter_publish_test(allocator, PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_publish_failure_reason_code, + s_request_response_mqtt5_protocol_adapter_publish_failure_reason_code_fn) + +static int s_request_response_mqtt5_protocol_adapter_publish_failure_error_code_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt5_protocol_adapter_publish_test(allocator, PAOTT_FAILURE_ERROR_CODE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_publish_failure_error_code, + s_request_response_mqtt5_protocol_adapter_publish_failure_error_code_fn) + +static int s_do_request_response_mqtt5_protocol_adapter_connection_event_connect_test( + struct aws_allocator *allocator, + bool rejoin_session) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + aws_mqtt5_mock_server_handle_connect_honor_session_unconditional; + test_options.client_options.session_behavior = rejoin_session ? AWS_MQTT5_CSBT_REJOIN_ALWAYS : AWS_MQTT5_CSBT_CLEAN; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options mqtt5_test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt5( + &fixture, allocator, &mqtt5_test_fixture_options)); + + struct aws_mqtt5_client *client = fixture.protocol_context.mqtt5_fixture.client; + + struct aws_protocol_adapter_connection_event expected_connect_record = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = rejoin_session, + }; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + s_wait_for_connection_events_contains(&fixture, &expected_connect_record, 1); + + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + aws_wait_for_stopped_lifecycle_event(&fixture.protocol_context.mqtt5_fixture); + + struct aws_protocol_adapter_connection_event expected_disconnect_record = { + .event_type = AWS_PACET_DISCONNECTED, + }; + + s_wait_for_connection_events_contains(&fixture, &expected_disconnect_record, 1); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_request_response_mqtt5_protocol_adapter_connection_event_connect_no_session_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_do_request_response_mqtt5_protocol_adapter_connection_event_connect_test(allocator, false); +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_connection_event_connect_no_session, + s_request_response_mqtt5_protocol_adapter_connection_event_connect_no_session_fn) + +static int s_request_response_mqtt5_protocol_adapter_connection_event_connect_session_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_do_request_response_mqtt5_protocol_adapter_connection_event_connect_test(allocator, true); +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_connection_event_connect_session, + s_request_response_mqtt5_protocol_adapter_connection_event_connect_session_fn) + +static int s_request_response_mqtt5_protocol_adapter_incoming_publish_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_puback_and_forward; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options mqtt5_test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt5( + &fixture, allocator, &mqtt5_test_fixture_options)); + + struct aws_mqtt5_client *client = fixture.protocol_context.mqtt5_fixture.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&fixture.protocol_context.mqtt5_fixture); + + struct request_response_protocol_adapter_incoming_publish_event_record expected_publish; + AWS_ZERO_STRUCT(expected_publish); + + s_request_response_protocol_adapter_incoming_publish_event_record_init( + &expected_publish, + allocator, + aws_byte_cursor_from_c_str("hello/world"), + aws_byte_cursor_from_c_str("SomePayload")); + + struct aws_protocol_adapter_publish_options publish_options = { + .topic = aws_byte_cursor_from_buf(&expected_publish.topic), + .payload = aws_byte_cursor_from_buf(&expected_publish.payload), + .ack_timeout_seconds = 2, + .completion_callback_fn = s_rr_mqtt_protocol_adapter_test_on_publish_result, + .user_data = &fixture, + }; + + aws_mqtt_protocol_adapter_publish(fixture.protocol_adapter, &publish_options); + + s_wait_for_incoming_publish_events_contains(&fixture, &expected_publish, 1); + + s_request_response_protocol_adapter_incoming_publish_event_record_clean_up(&expected_publish); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_incoming_publish, + s_request_response_mqtt5_protocol_adapter_incoming_publish_fn) + +static int s_request_response_mqtt5_protocol_adapter_shutdown_while_pending_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options test_options; + aws_mqtt5_client_test_init_default_options(&test_options); + + // don't respond to anything + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = NULL; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = NULL; + test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = NULL; + + test_options.client_options.offline_queue_behavior = AWS_MQTT5_COQBT_FAIL_ALL_ON_DISCONNECT; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options mqtt5_test_fixture_options = { + .client_options = &test_options.client_options, + .server_function_table = &test_options.server_function_table, + }; + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt5( + &fixture, allocator, &mqtt5_test_fixture_options)); + + struct aws_mqtt5_client *client = fixture.protocol_context.mqtt5_fixture.client; + + ASSERT_SUCCESS(aws_mqtt5_client_start(client)); + + aws_wait_for_connected_lifecycle_event(&fixture.protocol_context.mqtt5_fixture); + + // publish + struct aws_protocol_adapter_publish_options publish_options = { + .topic = aws_byte_cursor_from_c_str("hello/world"), + .payload = aws_byte_cursor_from_c_str("SomePayload"), + .ack_timeout_seconds = 5, + .completion_callback_fn = s_rr_mqtt_protocol_adapter_test_on_publish_result, + .user_data = &fixture, + }; + + aws_mqtt_protocol_adapter_publish(fixture.protocol_adapter, &publish_options); + + // subscribe + struct aws_protocol_adapter_subscribe_options subscribe_options = { + .topic_filter = aws_byte_cursor_from_c_str("hello/world"), + .ack_timeout_seconds = 5, + }; + + aws_mqtt_protocol_adapter_subscribe(fixture.protocol_adapter, &subscribe_options); + + // unsubscribe + struct aws_protocol_adapter_unsubscribe_options unsubscribe_options = { + .topic_filter = aws_byte_cursor_from_c_str("hello/world"), + .ack_timeout_seconds = 5, + }; + + aws_mqtt_protocol_adapter_unsubscribe(fixture.protocol_adapter, &unsubscribe_options); + + // tear down the adapter, leaving the in-progress operations with nothing to call back into + s_aws_request_response_protocol_adapter_test_fixture_destroy_adapters(&fixture); + + // stop the mqtt client, which fails the pending MQTT operations + ASSERT_SUCCESS(aws_mqtt5_client_stop(client, NULL, NULL)); + + // wait for the stop to complete, which implies all the operations have been completed without calling back + // into a deleted adapter + aws_wait_for_n_lifecycle_events(&fixture.protocol_context.mqtt5_fixture, AWS_MQTT5_CLET_STOPPED, 1); + + // nothing to verify, we just don't want to crash + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt5_protocol_adapter_shutdown_while_pending, + s_request_response_mqtt5_protocol_adapter_shutdown_while_pending_fn) + +static int s_do_request_response_mqtt311_protocol_adapter_subscribe_test( + struct aws_allocator *allocator, + enum protocol_adapter_operation_test_type test_type) { + aws_mqtt_library_init(allocator); + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt311(&fixture, allocator)); + + struct aws_mqtt_client_connection *connection = + fixture.protocol_context.mqtt311_fixture.mqtt311_test_context.mqtt_connection; + + struct mqtt_connection_state_test *test_context_311 = + &fixture.protocol_context.mqtt311_fixture.mqtt311_test_context; + + switch (test_type) { + case PAOTT_SUCCESS: + mqtt_mock_server_enable_auto_ack(test_context_311->mock_server); + break; + case PAOTT_FAILURE_REASON_CODE_RETRYABLE: + case PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE: + mqtt_mock_server_enable_auto_ack(test_context_311->mock_server); + mqtt_mock_server_suback_reason_code(test_context_311->mock_server, 128); + break; + case PAOTT_FAILURE_ERROR_CODE: + // there is no reasonable way to generate an async error-code-based failure; so skip this case + AWS_FATAL_ASSERT(false); + break; + case PAOTT_FAILURE_TIMEOUT: + mqtt_mock_server_disable_auto_ack(test_context_311->mock_server); + default: + break; + } + + struct aws_mqtt_connection_options connection_options = { + .user_data = test_context_311, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(test_context_311->endpoint.address), + .socket_options = &test_context_311->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(connection, &connection_options)); + + struct aws_protocol_adapter_connection_event connection_success_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = false, + }; + + s_wait_for_connection_events_contains(&fixture, &connection_success_event, 1); + + int expected_error_code = s_test_type_to_expected_error_code(test_type, AWS_PAPT_MQTT311); + + struct request_response_protocol_adapter_subscription_event_record expected_outcome; + s_request_response_protocol_adapter_subscription_event_record_init( + &expected_outcome, + allocator, + AWS_PASET_SUBSCRIBE, + aws_byte_cursor_from_c_str("hello/world"), + expected_error_code, + true); + + struct aws_protocol_adapter_subscribe_options subscribe_options = { + .topic_filter = aws_byte_cursor_from_buf(&expected_outcome.topic_filter), + .ack_timeout_seconds = 2, + }; + + aws_mqtt_protocol_adapter_subscribe(fixture.protocol_adapter, &subscribe_options); + + s_wait_for_subscription_events_contains(&fixture, &expected_outcome, 1); + + s_request_response_protocol_adapter_subscription_event_record_cleanup(&expected_outcome); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect(connection, aws_test311_on_disconnect_fn, test_context_311)); + + struct aws_protocol_adapter_connection_event interruption_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + + s_wait_for_connection_events_contains(&fixture, &interruption_event, 1); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_request_response_mqtt311_protocol_adapter_subscribe_success_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt311_protocol_adapter_subscribe_test(allocator, PAOTT_SUCCESS)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_subscribe_success, + s_request_response_mqtt311_protocol_adapter_subscribe_success_fn) + +static int s_request_response_mqtt311_protocol_adapter_subscribe_failure_timeout_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt311_protocol_adapter_subscribe_test(allocator, PAOTT_FAILURE_TIMEOUT)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_subscribe_failure_timeout, + s_request_response_mqtt311_protocol_adapter_subscribe_failure_timeout_fn) + +static int s_request_response_mqtt311_protocol_adapter_subscribe_failure_reason_code_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt311_protocol_adapter_subscribe_test( + allocator, PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_subscribe_failure_reason_code, + s_request_response_mqtt311_protocol_adapter_subscribe_failure_reason_code_fn) + +static int s_do_request_response_mqtt311_protocol_adapter_unsubscribe_test( + struct aws_allocator *allocator, + enum protocol_adapter_operation_test_type test_type) { + aws_mqtt_library_init(allocator); + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt311(&fixture, allocator)); + + struct aws_mqtt_client_connection *connection = + fixture.protocol_context.mqtt311_fixture.mqtt311_test_context.mqtt_connection; + + struct mqtt_connection_state_test *test_context_311 = + &fixture.protocol_context.mqtt311_fixture.mqtt311_test_context; + + switch (test_type) { + case PAOTT_SUCCESS: + mqtt_mock_server_enable_auto_ack(test_context_311->mock_server); + break; + case PAOTT_FAILURE_REASON_CODE_RETRYABLE: + case PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE: + // 311 does not have unsuback reason codes or a way to fail + AWS_FATAL_ASSERT(false); + break; + case PAOTT_FAILURE_ERROR_CODE: + // there is no reasonable way to generate an async error-code-based failure; so skip this case + AWS_FATAL_ASSERT(false); + break; + case PAOTT_FAILURE_TIMEOUT: + mqtt_mock_server_disable_auto_ack(test_context_311->mock_server); + default: + break; + } + + struct aws_mqtt_connection_options connection_options = { + .user_data = test_context_311, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(test_context_311->endpoint.address), + .socket_options = &test_context_311->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(connection, &connection_options)); + + struct aws_protocol_adapter_connection_event connection_success_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = false, + }; + + s_wait_for_connection_events_contains(&fixture, &connection_success_event, 1); + + int expected_error_code = s_test_type_to_expected_error_code(test_type, AWS_PAPT_MQTT311); + + struct request_response_protocol_adapter_subscription_event_record expected_outcome; + s_request_response_protocol_adapter_subscription_event_record_init( + &expected_outcome, + allocator, + AWS_PASET_UNSUBSCRIBE, + aws_byte_cursor_from_c_str("hello/world"), + expected_error_code, + expected_error_code == AWS_ERROR_MQTT_TIMEOUT); + + struct aws_protocol_adapter_unsubscribe_options unsubscribe_options = { + .topic_filter = aws_byte_cursor_from_buf(&expected_outcome.topic_filter), + .ack_timeout_seconds = 2, + }; + + aws_mqtt_protocol_adapter_unsubscribe(fixture.protocol_adapter, &unsubscribe_options); + + s_wait_for_subscription_events_contains(&fixture, &expected_outcome, 1); + + s_request_response_protocol_adapter_subscription_event_record_cleanup(&expected_outcome); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect(connection, aws_test311_on_disconnect_fn, test_context_311)); + + struct aws_protocol_adapter_connection_event interruption_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + + s_wait_for_connection_events_contains(&fixture, &interruption_event, 1); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_request_response_mqtt311_protocol_adapter_unsubscribe_success_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt311_protocol_adapter_unsubscribe_test(allocator, PAOTT_SUCCESS)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_unsubscribe_success, + s_request_response_mqtt311_protocol_adapter_unsubscribe_success_fn) + +static int s_request_response_mqtt311_protocol_adapter_unsubscribe_failure_timeout_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt311_protocol_adapter_unsubscribe_test(allocator, PAOTT_FAILURE_TIMEOUT)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_unsubscribe_failure_timeout, + s_request_response_mqtt311_protocol_adapter_unsubscribe_failure_timeout_fn) + +static int s_do_request_response_mqtt311_protocol_adapter_publish_test( + struct aws_allocator *allocator, + enum protocol_adapter_operation_test_type test_type) { + aws_mqtt_library_init(allocator); + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt311(&fixture, allocator)); + + struct aws_mqtt_client_connection *connection = + fixture.protocol_context.mqtt311_fixture.mqtt311_test_context.mqtt_connection; + + struct mqtt_connection_state_test *test_context_311 = + &fixture.protocol_context.mqtt311_fixture.mqtt311_test_context; + + switch (test_type) { + case PAOTT_SUCCESS: + mqtt_mock_server_enable_auto_ack(test_context_311->mock_server); + break; + case PAOTT_FAILURE_REASON_CODE_RETRYABLE: + case PAOTT_FAILURE_REASON_CODE_NOT_RETRYABLE: + // 311 does not have puback reason codes or a way to fail + AWS_FATAL_ASSERT(false); + break; + case PAOTT_FAILURE_ERROR_CODE: + // there is no reasonable way to generate an async error-code-based failure; so skip this case + AWS_FATAL_ASSERT(false); + break; + case PAOTT_FAILURE_TIMEOUT: + mqtt_mock_server_disable_auto_ack(test_context_311->mock_server); + default: + break; + } + + struct aws_mqtt_connection_options connection_options = { + .user_data = test_context_311, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(test_context_311->endpoint.address), + .socket_options = &test_context_311->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(connection, &connection_options)); + + struct aws_protocol_adapter_connection_event connection_success_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = false, + }; + + s_wait_for_connection_events_contains(&fixture, &connection_success_event, 1); + + struct aws_protocol_adapter_publish_options publish_options = { + .topic = aws_byte_cursor_from_c_str("hello/world"), + .payload = aws_byte_cursor_from_c_str("SomePayload"), + .ack_timeout_seconds = 2, + .completion_callback_fn = s_rr_mqtt_protocol_adapter_test_on_publish_result, + .user_data = &fixture, + }; + + aws_mqtt_protocol_adapter_publish(fixture.protocol_adapter, &publish_options); + + int expected_error_code = s_test_type_to_expected_error_code(test_type, AWS_PAPT_MQTT311); + s_wait_for_publish_results_contains(&fixture, expected_error_code, 1); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect(connection, aws_test311_on_disconnect_fn, test_context_311)); + + struct aws_protocol_adapter_connection_event interruption_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + + s_wait_for_connection_events_contains(&fixture, &interruption_event, 1); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_request_response_mqtt311_protocol_adapter_publish_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt311_protocol_adapter_publish_test(allocator, PAOTT_SUCCESS)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_publish_success, + s_request_response_mqtt311_protocol_adapter_publish_success_fn) + +static int s_request_response_mqtt311_protocol_adapter_publish_failure_timeout_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + ASSERT_SUCCESS(s_do_request_response_mqtt311_protocol_adapter_publish_test(allocator, PAOTT_FAILURE_TIMEOUT)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_publish_failure_timeout, + s_request_response_mqtt311_protocol_adapter_publish_failure_timeout_fn) + +static int s_request_response_mqtt311_protocol_adapter_connection_events_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt311(&fixture, allocator)); + + struct aws_mqtt_client_connection *connection = + fixture.protocol_context.mqtt311_fixture.mqtt311_test_context.mqtt_connection; + + struct mqtt_connection_state_test *test_context_311 = + &fixture.protocol_context.mqtt311_fixture.mqtt311_test_context; + + // always rejoin session and cause continual ping timeouts to generate interrupted events + mqtt_mock_server_set_session_present(test_context_311->mock_server, true); + mqtt_mock_server_set_max_ping_resp(test_context_311->mock_server, 0); + + struct aws_mqtt_connection_options connection_options = { + .user_data = test_context_311, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(test_context_311->endpoint.address), + .socket_options = &test_context_311->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 2, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(connection, &connection_options)); + + struct aws_protocol_adapter_connection_event connection_success_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = true, + }; + + s_wait_for_connection_events_contains(&fixture, &connection_success_event, 1); + + struct aws_protocol_adapter_connection_event interruption_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + + /* wait for ping timeout disconnect */ + s_wait_for_connection_events_contains(&fixture, &interruption_event, 1); + + /* wait for automatic reconnect */ + s_wait_for_connection_events_contains(&fixture, &connection_success_event, 2); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect(connection, aws_test311_on_disconnect_fn, test_context_311)); + + s_wait_for_connection_events_contains(&fixture, &interruption_event, 2); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_connection_events, + s_request_response_mqtt311_protocol_adapter_connection_events_fn) + +static int s_request_response_mqtt311_protocol_adapter_incoming_publish_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt311(&fixture, allocator)); + + struct aws_mqtt_client_connection *connection = + fixture.protocol_context.mqtt311_fixture.mqtt311_test_context.mqtt_connection; + + struct mqtt_connection_state_test *test_context_311 = + &fixture.protocol_context.mqtt311_fixture.mqtt311_test_context; + + /* reflect publishes */ + mqtt_mock_server_set_publish_reflection(test_context_311->mock_server, true); + + struct aws_mqtt_connection_options connection_options = { + .user_data = test_context_311, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(test_context_311->endpoint.address), + .socket_options = &test_context_311->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = 30 * 1000, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 20000, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(connection, &connection_options)); + + struct aws_protocol_adapter_connection_event connection_success_event = { + .event_type = AWS_PACET_CONNECTED, + }; + + s_wait_for_connection_events_contains(&fixture, &connection_success_event, 1); + + struct request_response_protocol_adapter_incoming_publish_event_record expected_publish; + AWS_ZERO_STRUCT(expected_publish); + + s_request_response_protocol_adapter_incoming_publish_event_record_init( + &expected_publish, + allocator, + aws_byte_cursor_from_c_str("hello/world"), + aws_byte_cursor_from_c_str("SomePayload")); + + struct aws_protocol_adapter_publish_options publish_options = { + .topic = aws_byte_cursor_from_buf(&expected_publish.topic), + .payload = aws_byte_cursor_from_buf(&expected_publish.payload), + .ack_timeout_seconds = 2, + .completion_callback_fn = s_rr_mqtt_protocol_adapter_test_on_publish_result, + .user_data = &fixture, + }; + + aws_mqtt_protocol_adapter_publish(fixture.protocol_adapter, &publish_options); + + s_wait_for_incoming_publish_events_contains(&fixture, &expected_publish, 1); + + s_request_response_protocol_adapter_incoming_publish_event_record_clean_up(&expected_publish); + + struct aws_protocol_adapter_connection_event interruption_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect(connection, aws_test311_on_disconnect_fn, test_context_311)); + + s_wait_for_connection_events_contains(&fixture, &interruption_event, 1); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_incoming_publish, + s_request_response_mqtt311_protocol_adapter_incoming_publish_fn) + +static int s_request_response_mqtt311_protocol_adapter_shutdown_while_pending_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_request_response_protocol_adapter_test_fixture fixture; + ASSERT_SUCCESS(s_aws_request_response_protocol_adapter_test_fixture_init_mqtt311(&fixture, allocator)); + + struct aws_mqtt_client_connection *connection = + fixture.protocol_context.mqtt311_fixture.mqtt311_test_context.mqtt_connection; + + struct mqtt_connection_state_test *test_context_311 = + &fixture.protocol_context.mqtt311_fixture.mqtt311_test_context; + + /* reflect publishes */ + mqtt_mock_server_set_publish_reflection(test_context_311->mock_server, true); + + struct aws_mqtt_connection_options connection_options = { + .user_data = test_context_311, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(test_context_311->endpoint.address), + .socket_options = &test_context_311->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = 30 * 1000, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 20000, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(connection, &connection_options)); + + struct aws_protocol_adapter_connection_event connection_success_event = { + .event_type = AWS_PACET_CONNECTED, + }; + + s_wait_for_connection_events_contains(&fixture, &connection_success_event, 1); + + // publish + struct aws_protocol_adapter_publish_options publish_options = { + .topic = aws_byte_cursor_from_c_str("hello/world"), + .payload = aws_byte_cursor_from_c_str("SomePayload"), + .ack_timeout_seconds = 5, + .completion_callback_fn = s_rr_mqtt_protocol_adapter_test_on_publish_result, + .user_data = &fixture, + }; + + aws_mqtt_protocol_adapter_publish(fixture.protocol_adapter, &publish_options); + + // subscribe + struct aws_protocol_adapter_subscribe_options subscribe_options = { + .topic_filter = aws_byte_cursor_from_c_str("hello/world"), + .ack_timeout_seconds = 5, + }; + + aws_mqtt_protocol_adapter_subscribe(fixture.protocol_adapter, &subscribe_options); + + // unsubscribe + struct aws_protocol_adapter_unsubscribe_options unsubscribe_options = { + .topic_filter = aws_byte_cursor_from_c_str("hello/world"), + .ack_timeout_seconds = 5, + }; + + aws_mqtt_protocol_adapter_unsubscribe(fixture.protocol_adapter, &unsubscribe_options); + + // tear down the adapter, leaving the in-progress operations with nothing to call back into + s_aws_request_response_protocol_adapter_test_fixture_destroy_adapters(&fixture); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect(connection, aws_test311_on_disconnect_fn, test_context_311)); + + /* have to dig into the 311 test fixture because we won't be getting an interrupted event */ + aws_test311_wait_for_disconnect_to_complete(test_context_311); + + s_aws_request_response_protocol_adapter_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + request_response_mqtt311_protocol_adapter_shutdown_while_pending, + s_request_response_mqtt311_protocol_adapter_shutdown_while_pending_fn) \ No newline at end of file diff --git a/tests/request-response/request_response_client_tests.c b/tests/request-response/request_response_client_tests.c new file mode 100644 index 00000000..a1b55f1d --- /dev/null +++ b/tests/request-response/request_response_client_tests.c @@ -0,0 +1,3151 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "../v3/mqtt311_testing_utils.h" +#include "../v5/mqtt5_testing_utils.h" + +enum rr_test_client_protocol { + RRCP_MQTT311, + RRCP_MQTT5, +}; + +struct aws_rr_client_test_fixture { + struct aws_allocator *allocator; + + struct aws_mqtt_request_response_client *rr_client; + + enum rr_test_client_protocol test_protocol; + union { + struct aws_mqtt5_client_mock_test_fixture mqtt5_test_fixture; + struct mqtt_connection_state_test mqtt311_test_fixture; + } client_test_fixture; + + void *test_context; + + struct aws_mutex lock; + struct aws_condition_variable signal; + + bool client_initialized; + bool client_destroyed; + + struct aws_hash_table request_response_records; + struct aws_hash_table streaming_records; +}; + +struct aws_rr_client_fixture_request_response_record { + struct aws_allocator *allocator; + + struct aws_rr_client_test_fixture *fixture; + + struct aws_byte_cursor record_key_cursor; + struct aws_byte_buf record_key; + + bool completed; + int error_code; + struct aws_byte_buf response; + struct aws_byte_buf response_topic; +}; + +struct aws_rr_client_fixture_request_response_record *s_aws_rr_client_fixture_request_response_record_new( + struct aws_allocator *allocator, + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor request_payload) { + struct aws_rr_client_fixture_request_response_record *record = + aws_mem_calloc(allocator, 1, sizeof(struct aws_rr_client_fixture_request_response_record)); + + record->allocator = allocator; + record->fixture = fixture; + + aws_byte_buf_init_copy_from_cursor(&record->record_key, allocator, request_payload); + record->record_key_cursor = aws_byte_cursor_from_buf(&record->record_key); + + return record; +} + +void s_aws_rr_client_fixture_request_response_record_delete( + struct aws_rr_client_fixture_request_response_record *record) { + aws_byte_buf_clean_up(&record->record_key); + aws_byte_buf_clean_up(&record->response); + aws_byte_buf_clean_up(&record->response_topic); + + aws_mem_release(record->allocator, record); +} + +static void s_aws_rr_client_fixture_request_response_record_hash_destroy(void *element) { + struct aws_rr_client_fixture_request_response_record *record = element; + + s_aws_rr_client_fixture_request_response_record_delete(record); +} + +static void s_rrc_fixture_request_completion_callback( + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + int error_code, + void *user_data) { + struct aws_rr_client_fixture_request_response_record *record = user_data; + struct aws_rr_client_test_fixture *fixture = record->fixture; + + aws_mutex_lock(&fixture->lock); + + if (error_code == AWS_ERROR_SUCCESS) { + AWS_FATAL_ASSERT(topic != NULL && payload != NULL); + + aws_byte_buf_init_copy_from_cursor(&record->response, fixture->allocator, *payload); + aws_byte_buf_init_copy_from_cursor(&record->response_topic, fixture->allocator, *topic); + } else { + AWS_FATAL_ASSERT(topic == NULL && payload == NULL); + record->error_code = error_code; + } + + record->completed = true; + + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static struct aws_rr_client_fixture_request_response_record *s_rrc_fixture_add_request_record( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor record_key) { + struct aws_rr_client_fixture_request_response_record *record = + s_aws_rr_client_fixture_request_response_record_new(fixture->allocator, fixture, record_key); + + aws_hash_table_put(&fixture->request_response_records, &record->record_key_cursor, record, NULL); + + return record; +} + +struct rrc_operation_completion_context { + struct aws_byte_cursor key; + struct aws_rr_client_test_fixture *fixture; +}; + +static bool s_is_request_complete(void *context) { + struct rrc_operation_completion_context *completion_context = context; + + struct aws_hash_element *element = NULL; + aws_hash_table_find(&completion_context->fixture->request_response_records, &completion_context->key, &element); + + AWS_FATAL_ASSERT(element != NULL && element->value != NULL); + + struct aws_rr_client_fixture_request_response_record *record = element->value; + + return record->completed; +} + +static void s_rrc_wait_on_request_completion( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor record_key) { + struct rrc_operation_completion_context context = { + .key = record_key, + .fixture = fixture, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_is_request_complete, &context); + aws_mutex_unlock(&fixture->lock); +} + +static int s_rrc_verify_request_completion( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor record_key, + int expected_error_code, + struct aws_byte_cursor *expected_response_topic, + struct aws_byte_cursor *expected_response) { + aws_mutex_lock(&fixture->lock); + + struct aws_hash_element *element = NULL; + aws_hash_table_find(&fixture->request_response_records, &record_key, &element); + + AWS_FATAL_ASSERT(element != NULL && element->value != NULL); + + struct aws_rr_client_fixture_request_response_record *record = element->value; + + ASSERT_INT_EQUALS(expected_error_code, record->error_code); + + if (expected_response != NULL) { + struct aws_byte_cursor actual_payload = aws_byte_cursor_from_buf(&record->response); + ASSERT_TRUE(aws_byte_cursor_eq(expected_response, &actual_payload)); + + struct aws_byte_cursor actual_response_topic = aws_byte_cursor_from_buf(&record->response_topic); + ASSERT_TRUE(aws_byte_cursor_eq(expected_response_topic, &actual_response_topic)); + } else { + ASSERT_INT_EQUALS(0, record->response.len); + ASSERT_INT_EQUALS(0, record->response_topic.len); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +struct aws_rr_client_fixture_streaming_record { + struct aws_allocator *allocator; + + struct aws_rr_client_test_fixture *fixture; + + struct aws_byte_cursor record_key_cursor; + struct aws_byte_buf record_key; + + struct aws_array_list publishes; + struct aws_array_list subscription_events; + + bool terminated; +}; + +struct aws_rr_client_fixture_streaming_record_subscription_event { + enum aws_rr_streaming_subscription_event_type status; + int error_code; +}; + +struct aws_rr_client_fixture_streaming_record *s_aws_rr_client_fixture_streaming_record_new( + struct aws_allocator *allocator, + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor record_key) { + struct aws_rr_client_fixture_streaming_record *record = + aws_mem_calloc(allocator, 1, sizeof(struct aws_rr_client_fixture_streaming_record)); + + record->allocator = allocator; + record->fixture = fixture; + + aws_byte_buf_init_copy_from_cursor(&record->record_key, allocator, record_key); + record->record_key_cursor = aws_byte_cursor_from_buf(&record->record_key); + + aws_array_list_init_dynamic(&record->publishes, allocator, 10, sizeof(struct aws_byte_buf)); + aws_array_list_init_dynamic( + &record->subscription_events, + allocator, + 10, + sizeof(struct aws_rr_client_fixture_streaming_record_subscription_event)); + + return record; +} + +void s_aws_rr_client_fixture_streaming_record_delete(struct aws_rr_client_fixture_streaming_record *record) { + aws_byte_buf_clean_up(&record->record_key); + + size_t publish_count = aws_array_list_length(&record->publishes); + for (size_t i = 0; i < publish_count; ++i) { + struct aws_byte_buf publish_payload; + aws_array_list_get_at(&record->publishes, &publish_payload, i); + + aws_byte_buf_clean_up(&publish_payload); + } + + aws_array_list_clean_up(&record->publishes); + aws_array_list_clean_up(&record->subscription_events); + + aws_mem_release(record->allocator, record); +} + +static void s_aws_rr_client_fixture_streaming_record_hash_destroy(void *element) { + struct aws_rr_client_fixture_streaming_record *record = element; + + s_aws_rr_client_fixture_streaming_record_delete(record); +} + +static void s_rrc_fixture_streaming_operation_subscription_status_callback( + enum aws_rr_streaming_subscription_event_type status, + int error_code, + void *user_data) { + + struct aws_rr_client_fixture_streaming_record *record = user_data; + struct aws_rr_client_test_fixture *fixture = record->fixture; + + aws_mutex_lock(&fixture->lock); + + struct aws_rr_client_fixture_streaming_record_subscription_event event = { + .status = status, + .error_code = error_code, + }; + aws_array_list_push_back(&record->subscription_events, &event); + + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_rrc_fixture_streaming_operation_incoming_publish_callback( + struct aws_byte_cursor payload, + void *user_data) { + struct aws_rr_client_fixture_streaming_record *record = user_data; + struct aws_rr_client_test_fixture *fixture = record->fixture; + + aws_mutex_lock(&fixture->lock); + + struct aws_byte_buf payload_buffer; + aws_byte_buf_init_copy_from_cursor(&payload_buffer, fixture->allocator, payload); + + aws_array_list_push_back(&record->publishes, &payload_buffer); + + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static void s_rrc_fixture_streaming_operation_terminated_callback(void *user_data) { + struct aws_rr_client_fixture_streaming_record *record = user_data; + struct aws_rr_client_test_fixture *fixture = record->fixture; + + aws_mutex_lock(&fixture->lock); + + record->terminated = true; + + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static struct aws_rr_client_fixture_streaming_record *s_rrc_fixture_add_streaming_record( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor key) { + struct aws_rr_client_fixture_streaming_record *record = + s_aws_rr_client_fixture_streaming_record_new(fixture->allocator, fixture, key); + + aws_hash_table_put(&fixture->streaming_records, &record->record_key_cursor, record, NULL); + + return record; +} + +static bool s_is_stream_terminated(void *context) { + struct rrc_operation_completion_context *completion_context = context; + + struct aws_hash_element *element = NULL; + aws_hash_table_find(&completion_context->fixture->streaming_records, &completion_context->key, &element); + + AWS_FATAL_ASSERT(element != NULL && element->value != NULL); + + struct aws_rr_client_fixture_streaming_record *record = element->value; + + return record->terminated; +} + +static void s_rrc_wait_on_streaming_termination( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor key) { + struct rrc_operation_completion_context context = { + .key = key, + .fixture = fixture, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_is_stream_terminated, &context); + aws_mutex_unlock(&fixture->lock); +} + +struct rrc_streaming_event_wait_context { + struct aws_byte_cursor operation_key; + struct aws_rr_client_test_fixture *fixture; + size_t event_count; +}; + +static bool s_streaming_operation_has_n_publishes(void *context) { + struct rrc_streaming_event_wait_context *streaming_publish_context = context; + + struct aws_hash_element *element = NULL; + aws_hash_table_find( + &streaming_publish_context->fixture->streaming_records, &streaming_publish_context->operation_key, &element); + + AWS_FATAL_ASSERT(element != NULL && element->value != NULL); + + struct aws_rr_client_fixture_streaming_record *record = element->value; + + return aws_array_list_length(&record->publishes) >= streaming_publish_context->event_count; +} + +static void s_rrc_wait_for_n_streaming_publishes( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor key, + size_t count) { + struct rrc_streaming_event_wait_context context = { + .operation_key = key, + .fixture = fixture, + .event_count = count, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_streaming_operation_has_n_publishes, &context); + aws_mutex_unlock(&fixture->lock); +} + +static int s_rrc_verify_streaming_publishes( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor key, + size_t expected_publish_count, + struct aws_byte_cursor *expected_publishes) { + + aws_mutex_lock(&fixture->lock); + + struct aws_hash_element *element = NULL; + aws_hash_table_find(&fixture->streaming_records, &key, &element); + + AWS_FATAL_ASSERT(element != NULL && element->value != NULL); + + struct aws_rr_client_fixture_streaming_record *record = element->value; + + size_t actual_publish_count = aws_array_list_length(&record->publishes); + ASSERT_INT_EQUALS(expected_publish_count, actual_publish_count); + + for (size_t i = 0; i < actual_publish_count; ++i) { + struct aws_byte_buf actual_payload; + aws_array_list_get_at(&record->publishes, &actual_payload, i); + + struct aws_byte_cursor *expected_payload = &expected_publishes[i]; + + ASSERT_BIN_ARRAYS_EQUALS( + expected_payload->ptr, expected_payload->len, actual_payload.buffer, actual_payload.len); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static bool s_streaming_operation_has_n_subscription_events(void *context) { + struct rrc_streaming_event_wait_context *streaming_publish_context = context; + + struct aws_hash_element *element = NULL; + aws_hash_table_find( + &streaming_publish_context->fixture->streaming_records, &streaming_publish_context->operation_key, &element); + + AWS_FATAL_ASSERT(element != NULL && element->value != NULL); + + struct aws_rr_client_fixture_streaming_record *record = element->value; + + return aws_array_list_length(&record->subscription_events) >= streaming_publish_context->event_count; +} + +static void s_rrc_wait_for_n_streaming_subscription_events( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor key, + size_t count) { + struct rrc_streaming_event_wait_context context = { + .operation_key = key, + .fixture = fixture, + .event_count = count, + }; + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred( + &fixture->signal, &fixture->lock, s_streaming_operation_has_n_subscription_events, &context); + aws_mutex_unlock(&fixture->lock); +} + +static int s_rrc_verify_streaming_record_subscription_events( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor key, + size_t expected_subscription_event_count, + struct aws_rr_client_fixture_streaming_record_subscription_event *expected_subscription_events) { + aws_mutex_lock(&fixture->lock); + + struct aws_hash_element *element = NULL; + aws_hash_table_find(&fixture->streaming_records, &key, &element); + + AWS_FATAL_ASSERT(element != NULL && element->value != NULL); + + struct aws_rr_client_fixture_streaming_record *record = element->value; + + size_t actual_subscription_event_count = aws_array_list_length(&record->subscription_events); + ASSERT_INT_EQUALS(expected_subscription_event_count, actual_subscription_event_count); + + for (size_t i = 0; i < actual_subscription_event_count; ++i) { + struct aws_rr_client_fixture_streaming_record_subscription_event actual_event; + aws_array_list_get_at(&record->subscription_events, &actual_event, i); + + struct aws_rr_client_fixture_streaming_record_subscription_event *expected_event = + &expected_subscription_events[i]; + + ASSERT_INT_EQUALS(expected_event->status, actual_event.status); + ASSERT_INT_EQUALS(expected_event->error_code, actual_event.error_code); + } + + aws_mutex_unlock(&fixture->lock); + + return AWS_OP_SUCCESS; +} + +static void s_aws_rr_client_test_fixture_on_initialized(void *user_data) { + struct aws_rr_client_test_fixture *fixture = user_data; + + aws_mutex_lock(&fixture->lock); + fixture->client_initialized = true; + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static bool s_rr_client_test_fixture_initialized(void *context) { + struct aws_rr_client_test_fixture *fixture = context; + + return fixture->client_initialized; +} + +static void s_aws_rr_client_test_fixture_wait_for_initialized(struct aws_rr_client_test_fixture *fixture) { + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_rr_client_test_fixture_initialized, fixture); + aws_mutex_unlock(&fixture->lock); +} + +static void s_aws_rr_client_test_fixture_on_terminated(void *user_data) { + struct aws_rr_client_test_fixture *fixture = user_data; + + aws_mutex_lock(&fixture->lock); + fixture->client_destroyed = true; + aws_mutex_unlock(&fixture->lock); + aws_condition_variable_notify_all(&fixture->signal); +} + +static int s_aws_rr_client_test_fixture_init_from_mqtt5( + struct aws_rr_client_test_fixture *fixture, + struct aws_allocator *allocator, + struct aws_mqtt_request_response_client_options *rr_client_options, + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options *client_test_fixture_options, + void *test_context) { + AWS_ZERO_STRUCT(*fixture); + fixture->allocator = allocator; + fixture->test_protocol = RRCP_MQTT5; + + aws_mutex_init(&fixture->lock); + aws_condition_variable_init(&fixture->signal); + fixture->test_context = test_context; + + aws_hash_table_init( + &fixture->request_response_records, + allocator, + 10, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_aws_rr_client_fixture_request_response_record_hash_destroy); + + aws_hash_table_init( + &fixture->streaming_records, + allocator, + 10, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_aws_rr_client_fixture_streaming_record_hash_destroy); + + if (aws_mqtt5_client_mock_test_fixture_init( + &fixture->client_test_fixture.mqtt5_test_fixture, allocator, client_test_fixture_options)) { + return AWS_OP_ERR; + } + + struct aws_mqtt_request_response_client_options client_options = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = 5, + }; + + if (rr_client_options != NULL) { + client_options = *rr_client_options; + } + + client_options.initialized_callback = s_aws_rr_client_test_fixture_on_initialized; + client_options.terminated_callback = s_aws_rr_client_test_fixture_on_terminated; + client_options.user_data = fixture; + + fixture->rr_client = aws_mqtt_request_response_client_new_from_mqtt5_client( + allocator, fixture->client_test_fixture.mqtt5_test_fixture.client, &client_options); + AWS_FATAL_ASSERT(fixture->rr_client != NULL); + + aws_mqtt5_client_start(fixture->client_test_fixture.mqtt5_test_fixture.client); + + aws_wait_for_connected_lifecycle_event(&fixture->client_test_fixture.mqtt5_test_fixture); + s_aws_rr_client_test_fixture_wait_for_initialized(fixture); + + return AWS_OP_SUCCESS; +} + +static int s_aws_rr_client_test_fixture_init_from_mqtt311( + struct aws_rr_client_test_fixture *fixture, + struct aws_allocator *allocator, + struct aws_mqtt_request_response_client_options *rr_client_options, + void *test_context) { + AWS_ZERO_STRUCT(*fixture); + fixture->allocator = allocator; + fixture->test_protocol = RRCP_MQTT311; + + aws_mutex_init(&fixture->lock); + aws_condition_variable_init(&fixture->signal); + fixture->test_context = test_context; + + aws_hash_table_init( + &fixture->request_response_records, + allocator, + 10, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_aws_rr_client_fixture_request_response_record_hash_destroy); + + aws_hash_table_init( + &fixture->streaming_records, + allocator, + 10, + aws_hash_byte_cursor_ptr, + aws_mqtt_byte_cursor_hash_equality, + NULL, + s_aws_rr_client_fixture_streaming_record_hash_destroy); + + aws_test311_setup_mqtt_server_fn(allocator, &fixture->client_test_fixture.mqtt311_test_fixture); + + struct aws_mqtt_request_response_client_options client_options = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = 5, + }; + + if (rr_client_options != NULL) { + client_options = *rr_client_options; + } + + client_options.initialized_callback = s_aws_rr_client_test_fixture_on_initialized; + client_options.terminated_callback = s_aws_rr_client_test_fixture_on_terminated; + client_options.user_data = fixture; + + struct aws_mqtt_client_connection *mqtt_client = fixture->client_test_fixture.mqtt311_test_fixture.mqtt_connection; + + fixture->rr_client = + aws_mqtt_request_response_client_new_from_mqtt311_client(allocator, mqtt_client, &client_options); + AWS_FATAL_ASSERT(fixture->rr_client != NULL); + + struct aws_mqtt_connection_options connection_options = { + .user_data = &fixture->client_test_fixture.mqtt311_test_fixture, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(fixture->client_test_fixture.mqtt311_test_fixture.endpoint.address), + .socket_options = &fixture->client_test_fixture.mqtt311_test_fixture.socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = 16960, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(mqtt_client, &connection_options)); + aws_test311_wait_for_connection_to_complete(&fixture->client_test_fixture.mqtt311_test_fixture); + + s_aws_rr_client_test_fixture_wait_for_initialized(fixture); + + return AWS_OP_SUCCESS; +} + +static bool s_rr_client_test_fixture_terminated(void *context) { + struct aws_rr_client_test_fixture *fixture = context; + + return fixture->client_destroyed; +} + +static void s_aws_rr_client_test_fixture_clean_up(struct aws_rr_client_test_fixture *fixture) { + aws_mqtt_request_response_client_release(fixture->rr_client); + + aws_mutex_lock(&fixture->lock); + aws_condition_variable_wait_pred(&fixture->signal, &fixture->lock, s_rr_client_test_fixture_terminated, fixture); + aws_mutex_unlock(&fixture->lock); + + if (fixture->test_protocol == RRCP_MQTT5) { + aws_mqtt5_client_mock_test_fixture_clean_up(&fixture->client_test_fixture.mqtt5_test_fixture); + } else { + struct mqtt_connection_state_test *mqtt311_test_fixture = &fixture->client_test_fixture.mqtt311_test_fixture; + aws_mqtt_client_connection_disconnect( + mqtt311_test_fixture->mqtt_connection, aws_test311_on_disconnect_fn, mqtt311_test_fixture); + aws_test311_clean_up_mqtt_server_fn( + fixture->allocator, AWS_OP_SUCCESS, &fixture->client_test_fixture.mqtt311_test_fixture); + } + + aws_mutex_clean_up(&fixture->lock); + aws_condition_variable_clean_up(&fixture->signal); + + aws_hash_table_clean_up(&fixture->request_response_records); + aws_hash_table_clean_up(&fixture->streaming_records); +} + +static int s_rrc_mqtt5_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + aws_mqtt5_client_test_init_default_options(&client_test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options client_test_fixture_options = { + .client_options = &client_test_options.client_options, + .server_function_table = &client_test_options.server_function_table, + }; + + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS( + s_aws_rr_client_test_fixture_init_from_mqtt5(&fixture, allocator, NULL, &client_test_fixture_options, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_mqtt5_create_destroy, s_rrc_mqtt5_create_destroy_fn) + +static int s_rrc_mqtt311_create_destroy_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_aws_rr_client_test_fixture_init_from_mqtt311(&fixture, allocator, NULL, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_mqtt311_create_destroy, s_rrc_mqtt311_create_destroy_fn) + +static char s_response_filter_wildcard[] = "response/filter/+"; +static struct aws_byte_cursor s_response_filter_wildcard_cursor = { + .ptr = (uint8_t *)s_response_filter_wildcard, + .len = AWS_ARRAY_SIZE(s_response_filter_wildcard) - 1, +}; + +static int s_rrc_do_submit_request_operation_failure_test( + struct aws_allocator *allocator, + void (*request_mutator_fn)(struct aws_mqtt_request_operation_options *)) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + aws_mqtt5_client_test_init_default_options(&client_test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options client_test_fixture_options = { + .client_options = &client_test_options.client_options, + .server_function_table = &client_test_options.server_function_table, + }; + + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS( + s_aws_rr_client_test_fixture_init_from_mqtt5(&fixture, allocator, NULL, &client_test_fixture_options, NULL)); + + struct aws_mqtt_request_operation_response_path response_paths[] = { + { + .topic = aws_byte_cursor_from_c_str("response/filter/accepted"), + .correlation_token_json_path = aws_byte_cursor_from_c_str("client_token"), + }, + { + .topic = aws_byte_cursor_from_c_str("response/filter/rejected"), + .correlation_token_json_path = aws_byte_cursor_from_c_str("client_token"), + }, + }; + struct aws_mqtt_request_operation_options good_request = { + .subscription_topic_filters = &s_response_filter_wildcard_cursor, + .subscription_topic_filter_count = 1, + .response_paths = response_paths, + .response_path_count = AWS_ARRAY_SIZE(response_paths), + .publish_topic = aws_byte_cursor_from_c_str("get/shadow"), + .serialized_request = aws_byte_cursor_from_c_str("{}"), + .correlation_token = aws_byte_cursor_from_c_str("MyRequest#1"), + }; + ASSERT_SUCCESS(aws_mqtt_request_response_client_submit_request(fixture.rr_client, &good_request)); + + struct aws_mqtt_request_operation_options bad_request = good_request; + (*request_mutator_fn)(&bad_request); + + ASSERT_FAILS(aws_mqtt_request_response_client_submit_request(fixture.rr_client, &bad_request)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static void s_no_response_paths_mutator(struct aws_mqtt_request_operation_options *request_options) { + request_options->response_path_count = 0; + request_options->response_paths = NULL; +} + +static int s_rrc_submit_request_operation_failure_no_response_paths_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_rrc_do_submit_request_operation_failure_test(allocator, s_no_response_paths_mutator); +} + +AWS_TEST_CASE( + rrc_submit_request_operation_failure_no_response_paths, + s_rrc_submit_request_operation_failure_no_response_paths_fn) + +static void s_invalid_response_topic_mutator(struct aws_mqtt_request_operation_options *request_options) { + request_options->response_paths[0].topic = aws_byte_cursor_from_c_str("a/b/#"); +} + +static int s_rrc_submit_request_operation_failure_invalid_response_topic_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_rrc_do_submit_request_operation_failure_test(allocator, s_invalid_response_topic_mutator); +} + +AWS_TEST_CASE( + rrc_submit_request_operation_failure_invalid_response_topic, + s_rrc_submit_request_operation_failure_invalid_response_topic_fn) + +static void s_invalid_publish_topic_mutator(struct aws_mqtt_request_operation_options *request_options) { + request_options->publish_topic = aws_byte_cursor_from_c_str("a/b/#"); +} + +static int s_rrc_submit_request_operation_failure_invalid_publish_topic_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_rrc_do_submit_request_operation_failure_test(allocator, s_invalid_publish_topic_mutator); +} + +AWS_TEST_CASE( + rrc_submit_request_operation_failure_invalid_publish_topic, + s_rrc_submit_request_operation_failure_invalid_publish_topic_fn) + +static char s_bad_filter[] = "a/#/c"; +static struct aws_byte_cursor s_bad_filter_cursor = { + .ptr = (uint8_t *)s_bad_filter, + .len = AWS_ARRAY_SIZE(s_bad_filter) - 1, +}; + +static void s_invalid_subscription_topic_filter_mutator(struct aws_mqtt_request_operation_options *request_options) { + request_options->subscription_topic_filters = &s_bad_filter_cursor; +} + +static int s_rrc_submit_request_operation_failure_invalid_subscription_topic_filter_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_rrc_do_submit_request_operation_failure_test(allocator, s_invalid_subscription_topic_filter_mutator); +} + +AWS_TEST_CASE( + rrc_submit_request_operation_failure_invalid_subscription_topic_filter, + s_rrc_submit_request_operation_failure_invalid_subscription_topic_filter_fn) + +static void s_no_subscription_topic_filter_mutator(struct aws_mqtt_request_operation_options *request_options) { + request_options->subscription_topic_filter_count = 0; +} + +static int s_rrc_submit_request_operation_failure_no_subscription_topic_filters_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_rrc_do_submit_request_operation_failure_test(allocator, s_no_subscription_topic_filter_mutator); +} + +AWS_TEST_CASE( + rrc_submit_request_operation_failure_no_subscription_topic_filters, + s_rrc_submit_request_operation_failure_no_subscription_topic_filters_fn) + +static void s_empty_request_mutator(struct aws_mqtt_request_operation_options *request_options) { + request_options->serialized_request = aws_byte_cursor_from_c_str(""); +} + +static int s_rrc_submit_request_operation_failure_empty_request_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_rrc_do_submit_request_operation_failure_test(allocator, s_empty_request_mutator); +} + +AWS_TEST_CASE( + rrc_submit_request_operation_failure_empty_request, + s_rrc_submit_request_operation_failure_empty_request_fn) + +static int s_rrc_submit_streaming_operation_failure_invalid_subscription_topic_filter_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + aws_mqtt5_client_test_init_default_options(&client_test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options client_test_fixture_options = { + .client_options = &client_test_options.client_options, + .server_function_table = &client_test_options.server_function_table, + }; + + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS( + s_aws_rr_client_test_fixture_init_from_mqtt5(&fixture, allocator, NULL, &client_test_fixture_options, NULL)); + + struct aws_mqtt_streaming_operation_options good_options = { + .topic_filter = aws_byte_cursor_from_c_str("a/b"), + }; + + struct aws_mqtt_rr_client_operation *good_operation = + aws_mqtt_request_response_client_create_streaming_operation(fixture.rr_client, &good_options); + ASSERT_NOT_NULL(good_operation); + ASSERT_SUCCESS(aws_mqtt_rr_client_operation_activate(good_operation)); + + aws_mqtt_rr_client_operation_release(good_operation); + + struct aws_mqtt_streaming_operation_options bad_options = good_options; + bad_options.topic_filter = aws_byte_cursor_from_c_str(""); + + struct aws_mqtt_rr_client_operation *bad_operation = + aws_mqtt_request_response_client_create_streaming_operation(fixture.rr_client, &bad_options); + ASSERT_NULL(bad_operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_submit_streaming_operation_failure_invalid_subscription_topic_filter, + s_rrc_submit_streaming_operation_failure_invalid_subscription_topic_filter_fn) + +static int s_do_rrc_single_request_operation_test_fn( + struct aws_allocator *allocator, + struct aws_mqtt_request_response_client_options *rr_client_options, + struct aws_mqtt_request_operation_options *request_options, + int expected_error_code, + struct aws_byte_cursor *expected_response_topic, + struct aws_byte_cursor *expected_payload, + bool shutdown_after_submit) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + aws_mqtt5_client_test_init_default_options(&client_test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options client_test_fixture_options = { + .client_options = &client_test_options.client_options, + .server_function_table = &client_test_options.server_function_table, + }; + + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_aws_rr_client_test_fixture_init_from_mqtt5( + &fixture, allocator, rr_client_options, &client_test_fixture_options, NULL)); + + struct aws_rr_client_fixture_request_response_record *record = + s_rrc_fixture_add_request_record(&fixture, request_options->serialized_request); + + request_options->completion_callback = s_rrc_fixture_request_completion_callback; + request_options->user_data = record; + + ASSERT_SUCCESS(aws_mqtt_request_response_client_submit_request(fixture.rr_client, request_options)); + + if (shutdown_after_submit) { + aws_mqtt_request_response_client_release(fixture.rr_client); + fixture.rr_client = NULL; + } + + s_rrc_wait_on_request_completion(&fixture, request_options->serialized_request); + + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, request_options->serialized_request, expected_error_code, expected_response_topic, expected_payload)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_rrc_submit_request_operation_failure_by_shutdown_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt_request_operation_response_path response_paths[] = { + { + .topic = aws_byte_cursor_from_c_str("response/filter/accepted"), + .correlation_token_json_path = aws_byte_cursor_from_c_str("client_token"), + }, + }; + + struct aws_mqtt_request_operation_options request = { + .subscription_topic_filters = &s_response_filter_wildcard_cursor, + .subscription_topic_filter_count = 1, + .response_paths = response_paths, + .response_path_count = AWS_ARRAY_SIZE(response_paths), + .publish_topic = aws_byte_cursor_from_c_str("get/shadow"), + .serialized_request = aws_byte_cursor_from_c_str("request1"), + .correlation_token = aws_byte_cursor_from_c_str("MyRequest#1"), + }; + + return s_do_rrc_single_request_operation_test_fn( + allocator, NULL, &request, AWS_ERROR_MQTT_REQUEST_RESPONSE_CLIENT_SHUT_DOWN, NULL, NULL, true); +} + +AWS_TEST_CASE(rrc_submit_request_operation_failure_by_shutdown, s_rrc_submit_request_operation_failure_by_shutdown_fn) + +static int s_do_rrc_single_streaming_operation_test_fn( + struct aws_allocator *allocator, + struct aws_mqtt_request_response_client_options *rr_client_options, + struct aws_mqtt_streaming_operation_options *streaming_options, + size_t expected_subscription_event_count, + struct aws_rr_client_fixture_streaming_record_subscription_event *expected_subscription_events, + bool should_activate) { + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + aws_mqtt5_client_test_init_default_options(&client_test_options); + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options client_test_fixture_options = { + .client_options = &client_test_options.client_options, + .server_function_table = &client_test_options.server_function_table, + }; + + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_aws_rr_client_test_fixture_init_from_mqtt5( + &fixture, allocator, rr_client_options, &client_test_fixture_options, NULL)); + + struct aws_byte_cursor streaming_id = aws_byte_cursor_from_c_str("streaming1"); + struct aws_rr_client_fixture_streaming_record *record = s_rrc_fixture_add_streaming_record(&fixture, streaming_id); + + streaming_options->incoming_publish_callback = s_rrc_fixture_streaming_operation_incoming_publish_callback; + streaming_options->subscription_status_callback = s_rrc_fixture_streaming_operation_subscription_status_callback; + streaming_options->terminated_callback = s_rrc_fixture_streaming_operation_terminated_callback; + streaming_options->user_data = record; + + struct aws_mqtt_rr_client_operation *streaming_operation = + aws_mqtt_request_response_client_create_streaming_operation(fixture.rr_client, streaming_options); + ASSERT_NOT_NULL(streaming_operation); + + if (should_activate) { + ASSERT_SUCCESS(aws_mqtt_rr_client_operation_activate(streaming_operation)); + } + + aws_mqtt_request_response_client_release(fixture.rr_client); + fixture.rr_client = NULL; + + /* + * Extremely awkward sleep: + * + * We've submitted the operation and we've decref'd the client to zero. When the operation submit task + * is processed, if the release in the succeeding line has happened-before the client external destroy task + * has run, then the operation's destroy will be scheduled in-thread and run ahead of the client external + * destroy. This doesn't break correctness, but it does prevent the client from emitting a HALTED event + * on the subscription because the subscription/operation will be gone before the client external destroy + * task runs. + * + * So we add a nice, fat sleep to guarantee that the client external destroy task runs before the operation + * destroy task. + */ + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + aws_mqtt_rr_client_operation_release(streaming_operation); + + s_rrc_wait_on_streaming_termination(&fixture, streaming_id); + + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, streaming_id, expected_subscription_event_count, expected_subscription_events)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_rrc_activate_streaming_operation_and_shutdown_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_HALTED, + .error_code = AWS_ERROR_MQTT_REQUEST_RESPONSE_CLIENT_SHUT_DOWN, + }, + }; + + struct aws_mqtt_streaming_operation_options streaming_options = { + .topic_filter = aws_byte_cursor_from_c_str("derp/filter"), + }; + + return s_do_rrc_single_streaming_operation_test_fn( + allocator, NULL, &streaming_options, AWS_ARRAY_SIZE(expected_events), expected_events, true); +} + +AWS_TEST_CASE(rrc_activate_streaming_operation_and_shutdown, s_rrc_activate_streaming_operation_and_shutdown_fn) + +static int s_rrc_create_streaming_operation_and_shutdown_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt_streaming_operation_options streaming_options = { + .topic_filter = aws_byte_cursor_from_c_str("derp/filter"), + }; + + return s_do_rrc_single_streaming_operation_test_fn(allocator, NULL, &streaming_options, 0, NULL, false); +} + +AWS_TEST_CASE(rrc_create_streaming_operation_and_shutdown, s_rrc_create_streaming_operation_and_shutdown_fn) + +static int s_rrc_submit_request_operation_failure_by_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt_request_operation_response_path response_paths[] = { + { + .topic = aws_byte_cursor_from_c_str("response/filter/accepted"), + .correlation_token_json_path = aws_byte_cursor_from_c_str("client_token"), + }, + }; + + struct aws_mqtt_request_operation_options request = { + .subscription_topic_filters = &s_response_filter_wildcard_cursor, + .subscription_topic_filter_count = 1, + .response_paths = response_paths, + .response_path_count = AWS_ARRAY_SIZE(response_paths), + .publish_topic = aws_byte_cursor_from_c_str("get/shadow"), + .serialized_request = aws_byte_cursor_from_c_str("request1"), + .correlation_token = aws_byte_cursor_from_c_str("MyRequest#1"), + }; + + struct aws_mqtt_request_response_client_options rr_client_options = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 1, + .operation_timeout_seconds = 2, + }; + + return s_do_rrc_single_request_operation_test_fn( + allocator, &rr_client_options, &request, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL, false); +} + +AWS_TEST_CASE(rrc_submit_request_operation_failure_by_timeout, s_rrc_submit_request_operation_failure_by_timeout_fn) + +static struct aws_mqtt_rr_client_operation *s_create_streaming_operation( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor record_key, + struct aws_byte_cursor topic_filter) { + struct aws_rr_client_fixture_streaming_record *record = s_rrc_fixture_add_streaming_record(fixture, record_key); + + struct aws_mqtt_streaming_operation_options streaming_options = { + .topic_filter = topic_filter, + }; + streaming_options.incoming_publish_callback = s_rrc_fixture_streaming_operation_incoming_publish_callback; + streaming_options.subscription_status_callback = s_rrc_fixture_streaming_operation_subscription_status_callback; + streaming_options.terminated_callback = s_rrc_fixture_streaming_operation_terminated_callback; + streaming_options.user_data = record; + + struct aws_mqtt_rr_client_operation *streaming_operation = + aws_mqtt_request_response_client_create_streaming_operation(fixture->rr_client, &streaming_options); + aws_mqtt_rr_client_operation_activate(streaming_operation); + + return streaming_operation; +} + +static int s_rrc_publish_5( + struct aws_mqtt5_client *client, + struct aws_byte_cursor topic, + struct aws_byte_cursor payload) { + struct aws_mqtt5_packet_publish_view publish_options = { + .topic = topic, + .qos = AWS_MQTT5_QOS_AT_LEAST_ONCE, + .payload = payload, + }; + + struct aws_mqtt5_publish_completion_options completion_options; + AWS_ZERO_STRUCT(completion_options); + + return aws_mqtt5_client_publish(client, &publish_options, &completion_options); +} + +static int s_rrc_publish_311( + struct aws_mqtt_client_connection *connection, + struct aws_byte_cursor topic, + struct aws_byte_cursor payload) { + return aws_mqtt_client_connection_publish( + connection, &topic, AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload, NULL, NULL); +} + +static int s_rrc_protocol_client_publish( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor topic, + struct aws_byte_cursor payload) { + + if (fixture->test_protocol == RRCP_MQTT311) { + return s_rrc_publish_311(fixture->client_test_fixture.mqtt311_test_fixture.mqtt_connection, topic, payload); + } else { + return s_rrc_publish_5(fixture->client_test_fixture.mqtt5_test_fixture.client, topic, payload); + } +} + +typedef void(modify_fixture_options_fn)( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options); + +static int s_init_fixture_streaming_operation_success( + struct aws_rr_client_test_fixture *fixture, + struct mqtt5_client_test_options *client_test_options, + struct aws_allocator *allocator, + modify_fixture_options_fn *config_modifier, + void *user_data) { + aws_mqtt5_client_test_init_default_options(client_test_options); + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + aws_mqtt5_server_send_suback_on_subscribe; + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_puback_and_forward; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options client_test_fixture_options = { + .client_options = &client_test_options->client_options, + .server_function_table = &client_test_options->server_function_table, + .mock_server_user_data = user_data, + }; + + struct aws_mqtt_request_response_client_options rr_client_options = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 1, + .operation_timeout_seconds = 2, + }; + + if (config_modifier != NULL) { + (*config_modifier)(&rr_client_options, client_test_options); + } + + ASSERT_SUCCESS(s_aws_rr_client_test_fixture_init_from_mqtt5( + fixture, allocator, &rr_client_options, &client_test_fixture_options, NULL)); + + return AWS_OP_SUCCESS; +} + +/* + * Minimal success test: + * + * Create a streaming operation, verify subscription established, inject several publishes to the operation's topic, + * verify publishes received and routed through callbacks + */ +static int s_rrc_streaming_operation_success_single_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_events), expected_events)); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_publishes), expected_publishes)); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_streaming_operation_success_single, s_rrc_streaming_operation_success_single_fn) + +/* + * Variant of the minimal success test where we create two operations on the same topic filter, verify they both + * get subscriptions established and publishes, then close one, send another publish and verify only the still-open + * operation received it. + */ +static int s_rrc_streaming_operation_success_overlapping_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation1 = + s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + struct aws_byte_cursor record_key2 = aws_byte_cursor_from_c_str("key2"); + struct aws_mqtt_rr_client_operation *operation2 = + s_create_streaming_operation(&fixture, record_key2, topic_filter1); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key2, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_events), expected_events)); + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key2, AWS_ARRAY_SIZE(expected_events), expected_events)); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + struct aws_byte_cursor payload3 = aws_byte_cursor_from_c_str("Payload3"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key2, 2); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + payload3, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes(&fixture, record_key1, 2, expected_publishes)); + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes(&fixture, record_key2, 2, expected_publishes)); + + // close the first, wait for terminate + aws_mqtt_rr_client_operation_release(operation1); + s_rrc_wait_on_streaming_termination(&fixture, record_key1); + + // publish again + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload3)); + + // verify second operation got the new publish + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key2, 3); + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes( + &fixture, record_key2, AWS_ARRAY_SIZE(expected_publishes), expected_publishes)); + + // verify first operation did not + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes(&fixture, record_key1, 2, expected_publishes)); + + aws_mqtt_rr_client_operation_release(operation2); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_streaming_operation_success_overlapping, s_rrc_streaming_operation_success_overlapping_fn) + +/* + * Variant of the simple test where we start the protocol client offline. In addition to the normal verifies, we also + * verify nothing happens event wise until we start the client. + */ +static int s_rrc_streaming_operation_success_starting_offline_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + /* stop and start the underlying client */ + aws_mqtt5_client_stop(fixture.client_test_fixture.mqtt5_test_fixture.client, NULL, NULL); + aws_wait_for_stopped_lifecycle_event(&fixture.client_test_fixture.mqtt5_test_fixture); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + /* wait a while (longer than request timeout) to see if anything happens */ + aws_thread_current_sleep(aws_timestamp_convert(3, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + + /* fails the test if any subscription events have been emitted */ + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events(&fixture, record_key1, 0, NULL)); + + /* start the protocol client */ + aws_mqtt5_client_start(fixture.client_test_fixture.mqtt5_test_fixture.client); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_events), expected_events)); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_publishes), expected_publishes)); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_streaming_operation_success_starting_offline, s_rrc_streaming_operation_success_starting_offline_fn) + +static void s_rrc_force_clean_session_config( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options) { + (void)fixture_options; + + client_test_options->client_options.session_behavior = AWS_MQTT5_CSBT_CLEAN; +} + +/* + * Verifies that a streaming operation recovers properly from a clean session resumption (by resubscribing), emitting + * the proper subscription events, and receiving expected publishes once the subscription is re-established. + */ +static int s_rrc_streaming_operation_clean_session_reestablish_subscription_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success( + &fixture, &client_test_options, allocator, s_rrc_force_clean_session_config, NULL)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + { + .status = ARRSSET_SUBSCRIPTION_LOST, + .error_code = AWS_ERROR_SUCCESS, + }, + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events(&fixture, record_key1, 1, expected_events)); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 1); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes(&fixture, record_key1, 1, expected_publishes)); + + /* stop and start the underlying client, this will force a resubscribe since it's a clean session */ + aws_mqtt5_client_stop(fixture.client_test_fixture.mqtt5_test_fixture.client, NULL, NULL); + aws_wait_for_stopped_lifecycle_event(&fixture.client_test_fixture.mqtt5_test_fixture); + + aws_mqtt5_client_start(fixture.client_test_fixture.mqtt5_test_fixture.client); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 3); + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events(&fixture, record_key1, 3, expected_events)); + + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes(&fixture, record_key1, 2, expected_publishes)); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_streaming_operation_clean_session_reestablish_subscription, + s_rrc_streaming_operation_clean_session_reestablish_subscription_fn) + +static void s_rrc_force_resume_session_config( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options) { + (void)fixture_options; + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = + aws_mqtt5_mock_server_handle_connect_honor_session_unconditional; + client_test_options->client_options.session_behavior = AWS_MQTT5_CSBT_REJOIN_ALWAYS; +} + +/* + * Variant of the clean session test where instead we always resume a session. Verify we don't get subscription + * lost/established events afterwards and can still receive messages. + */ +static int s_rrc_streaming_operation_resume_session_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success( + &fixture, &client_test_options, allocator, s_rrc_force_resume_session_config, NULL)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events(&fixture, record_key1, 1, expected_events)); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 1); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes(&fixture, record_key1, 1, expected_publishes)); + + /* stop and start the underlying client */ + aws_mqtt5_client_stop(fixture.client_test_fixture.mqtt5_test_fixture.client, NULL, NULL); + aws_wait_for_stopped_lifecycle_event(&fixture.client_test_fixture.mqtt5_test_fixture); + + aws_mqtt5_client_start(fixture.client_test_fixture.mqtt5_test_fixture.client); + + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes(&fixture, record_key1, 2, expected_publishes)); + + /* check events after the publish has completed, that shows nothing happened subscription-wise */ + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events(&fixture, record_key1, 1, expected_events)); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_streaming_operation_resume_session, s_rrc_streaming_operation_resume_session_fn) + +struct rrc_subscribe_handler_context { + struct aws_rr_client_test_fixture *fixture; + + size_t subscribes_received; +}; + +static enum aws_mqtt5_suback_reason_code s_rrc_success_suback_rcs[] = { + AWS_MQTT5_SARC_GRANTED_QOS_1, +}; + +int s_handle_subscribe_with_initial_timeout( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct rrc_subscribe_handler_context *context = user_data; + + bool should_respond = false; + aws_mutex_lock(&context->fixture->lock); + should_respond = context->subscribes_received > 0; + ++context->subscribes_received; + aws_mutex_unlock(&context->fixture->lock); + + if (!should_respond) { + return AWS_OP_SUCCESS; + } + + struct aws_mqtt5_packet_subscribe_view *subscribe_packet = packet; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_packet->packet_id, + .reason_code_count = 1, + .reason_codes = s_rrc_success_suback_rcs, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); +} + +static void s_rrc_initial_subscribe_timeout_config( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options) { + (void)fixture_options; + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_handle_subscribe_with_initial_timeout; +} + +/* + * Variant of the basic success test where the first subscribe is ignored, causing it to timeout. Verify the + * client sends a second subscribe (which succeeds) after which everything is fine. + */ +static int s_rrc_streaming_operation_first_subscribe_times_out_resub_succeeds_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + + struct rrc_subscribe_handler_context subscribe_context = { + .fixture = &fixture, + .subscribes_received = 0, + }; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success( + &fixture, &client_test_options, allocator, s_rrc_initial_subscribe_timeout_config, &subscribe_context)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_events), expected_events)); + + // verify we ignored the first subscribe, triggering a second + aws_mutex_lock(&fixture.lock); + ASSERT_INT_EQUALS(2, subscribe_context.subscribes_received); + aws_mutex_unlock(&fixture.lock); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_publishes), expected_publishes)); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_streaming_operation_first_subscribe_times_out_resub_succeeds, + s_rrc_streaming_operation_first_subscribe_times_out_resub_succeeds_fn) + +static enum aws_mqtt5_suback_reason_code s_rrc_retryable_suback_rcs[] = { + AWS_MQTT5_SARC_UNSPECIFIED_ERROR, +}; + +int s_handle_subscribe_with_initial_retryable_failure( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct rrc_subscribe_handler_context *context = user_data; + + bool should_succeed = false; + aws_mutex_lock(&context->fixture->lock); + should_succeed = context->subscribes_received > 0; + ++context->subscribes_received; + aws_mutex_unlock(&context->fixture->lock); + + struct aws_mqtt5_packet_subscribe_view *subscribe_packet = packet; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_packet->packet_id, + .reason_code_count = 1, + }; + + if (should_succeed) { + suback_view.reason_codes = s_rrc_success_suback_rcs; + } else { + suback_view.reason_codes = s_rrc_retryable_suback_rcs; + } + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); +} + +static void s_rrc_initial_subscribe_retryable_failure_config( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options) { + (void)fixture_options; + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_handle_subscribe_with_initial_retryable_failure; +} + +/* + * Variant of the basic success test where the first subscribe triggers a retryable suback failure. Verify the + * client sends a second subscribe (which succeeds) after which everything is fine. + */ +static int s_rrc_streaming_operation_first_subscribe_retryable_failure_resub_succeeds_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + + struct rrc_subscribe_handler_context subscribe_context = { + .fixture = &fixture, + .subscribes_received = 0, + }; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success( + &fixture, + &client_test_options, + allocator, + s_rrc_initial_subscribe_retryable_failure_config, + &subscribe_context)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_events), expected_events)); + + // verify we ignored the first subscribe, triggering a second + aws_mutex_lock(&fixture.lock); + ASSERT_INT_EQUALS(2, subscribe_context.subscribes_received); + aws_mutex_unlock(&fixture.lock); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_publishes), expected_publishes)); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_streaming_operation_first_subscribe_retryable_failure_resub_succeeds, + s_rrc_streaming_operation_first_subscribe_retryable_failure_resub_succeeds_fn) + +static enum aws_mqtt5_suback_reason_code s_rrc_unretryable_suback_rcs[] = { + AWS_MQTT5_SARC_NOT_AUTHORIZED, +}; + +int s_handle_subscribe_with_terminal_failure( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct rrc_subscribe_handler_context *context = user_data; + + aws_mutex_lock(&context->fixture->lock); + ++context->subscribes_received; + aws_mutex_unlock(&context->fixture->lock); + + struct aws_mqtt5_packet_subscribe_view *subscribe_packet = packet; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_packet->packet_id, + .reason_code_count = 1, + .reason_codes = s_rrc_unretryable_suback_rcs, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); +} + +static void s_rrc_subscribe_terminal_failure_config( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options) { + (void)fixture_options; + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_handle_subscribe_with_terminal_failure; +} + +/* + * Failure variant where the subscribe triggers a non-retryable suback failure. Verify the + * operation gets halted. + */ +static int s_rrc_streaming_operation_subscribe_unretryable_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + + struct rrc_subscribe_handler_context subscribe_context = { + .fixture = &fixture, + .subscribes_received = 0, + }; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success( + &fixture, &client_test_options, allocator, s_rrc_subscribe_terminal_failure_config, &subscribe_context)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + // wait an extra amount just for fun + aws_thread_current_sleep(aws_timestamp_convert(2, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_HALTED, + .error_code = AWS_ERROR_MQTT_REQUEST_RESPONSE_SUBSCRIBE_FAILURE, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_events), expected_events)); + + aws_mutex_lock(&fixture.lock); + ASSERT_INT_EQUALS(1, subscribe_context.subscribes_received); + aws_mutex_unlock(&fixture.lock); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_streaming_operation_subscribe_unretryable_failure, + s_rrc_streaming_operation_subscribe_unretryable_failure_fn) + +static void s_rrc_unsubscribe_success_config( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options) { + (void)fixture_options; + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success; +} + +/* + * Multi-operation variant where we exceed the streaming subscription budget, release everything and then verify + * we can successfully establish a new streaming operation after everything cleans up. + */ +static int s_rrc_streaming_operation_failure_exceeds_subscription_budget_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success( + &fixture, &client_test_options, allocator, s_rrc_unsubscribe_success_config, NULL)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation1 = + s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + struct aws_byte_cursor record_key2 = aws_byte_cursor_from_c_str("key2"); + struct aws_byte_cursor topic_filter2 = aws_byte_cursor_from_c_str("topic/2"); + struct aws_mqtt_rr_client_operation *operation2 = + s_create_streaming_operation(&fixture, record_key2, topic_filter2); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key2, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_success_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_success_events), expected_success_events)); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_failure_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_HALTED, + .error_code = AWS_ERROR_MQTT_REQUEST_RESPONSE_NO_SUBSCRIPTION_CAPACITY, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key2, AWS_ARRAY_SIZE(expected_failure_events), expected_failure_events)); + + // two publishes on the mqtt client that get reflected into our subscription topic1 + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes(&fixture, record_key1, 2, expected_publishes)); + + // close the first, wait for terminate + aws_mqtt_rr_client_operation_release(operation1); + s_rrc_wait_on_streaming_termination(&fixture, record_key1); + + // close the second, wait for terminate + aws_mqtt_rr_client_operation_release(operation2); + s_rrc_wait_on_streaming_termination(&fixture, record_key2); + + // let the unsubscribe resolve + aws_thread_current_sleep(aws_timestamp_convert(1, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + + // make a third using topic filter 2 + struct aws_byte_cursor record_key3 = aws_byte_cursor_from_c_str("key3"); + struct aws_mqtt_rr_client_operation *operation3 = + s_create_streaming_operation(&fixture, record_key3, topic_filter2); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key3, 1); + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key3, AWS_ARRAY_SIZE(expected_success_events), expected_success_events)); + + // publish again + struct aws_byte_cursor payload3 = aws_byte_cursor_from_c_str("payload3"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter2, payload3)); + + // verify third operation got the new publish + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key3, 1); + + struct aws_byte_cursor third_expected_publishes[] = { + payload3, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes( + &fixture, record_key3, AWS_ARRAY_SIZE(third_expected_publishes), third_expected_publishes)); + + aws_mqtt_rr_client_operation_release(operation3); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_streaming_operation_failure_exceeds_subscription_budget, + s_rrc_streaming_operation_failure_exceeds_subscription_budget_fn) + +static int s_submit_request_operation_from_prefix( + struct aws_rr_client_test_fixture *fixture, + struct aws_byte_cursor record_key, + struct aws_byte_cursor prefix) { + char accepted_path[128]; + char rejected_path[128]; + char subscription_topic_filter[128]; + char publish_topic[128]; + + snprintf(accepted_path, AWS_ARRAY_SIZE(accepted_path), PRInSTR "/accepted", AWS_BYTE_CURSOR_PRI(prefix)); + snprintf(rejected_path, AWS_ARRAY_SIZE(rejected_path), PRInSTR "/rejected", AWS_BYTE_CURSOR_PRI(prefix)); + snprintf( + subscription_topic_filter, + AWS_ARRAY_SIZE(subscription_topic_filter), + PRInSTR "/+", + AWS_BYTE_CURSOR_PRI(prefix)); + snprintf(publish_topic, AWS_ARRAY_SIZE(publish_topic), PRInSTR "/get", AWS_BYTE_CURSOR_PRI(prefix)); + + struct aws_byte_cursor subscription_topic_filter_cursor = aws_byte_cursor_from_c_str(subscription_topic_filter); + + char correlation_token[128]; + struct aws_byte_buf correlation_token_buf = + aws_byte_buf_from_empty_array(correlation_token, AWS_ARRAY_SIZE(correlation_token)); + + struct aws_uuid uuid; + aws_uuid_init(&uuid); + aws_uuid_to_str(&uuid, &correlation_token_buf); + + struct aws_mqtt_request_operation_response_path response_paths[] = { + { + .topic = aws_byte_cursor_from_c_str(accepted_path), + .correlation_token_json_path = aws_byte_cursor_from_c_str("client_token"), + }, + { + .topic = aws_byte_cursor_from_c_str(rejected_path), + .correlation_token_json_path = aws_byte_cursor_from_c_str("client_token"), + }, + }; + + struct aws_rr_client_fixture_request_response_record *record = + s_rrc_fixture_add_request_record(fixture, record_key); + + struct aws_mqtt_request_operation_options request = { + .subscription_topic_filters = &subscription_topic_filter_cursor, + .subscription_topic_filter_count = 1, + .response_paths = response_paths, + .response_path_count = AWS_ARRAY_SIZE(response_paths), + .publish_topic = aws_byte_cursor_from_c_str(publish_topic), + .serialized_request = aws_byte_cursor_from_c_str("{}"), + .correlation_token = aws_byte_cursor_from_buf(&correlation_token_buf), + .completion_callback = s_rrc_fixture_request_completion_callback, + .user_data = record, + }; + + return aws_mqtt_request_response_client_submit_request(fixture->rr_client, &request); +} + +/* + * Configure server to only respond to subscribes that match a streaming filter. Submit a couple of + * request-response operations ahead of a streaming operation. Verify they both time out and that the streaming + * operation successfully subscribes and receives publishes. + */ +static int s_rrc_streaming_operation_success_delayed_by_request_operations_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success( + &fixture, &client_test_options, allocator, s_rrc_unsubscribe_success_config, NULL)); + + struct aws_byte_cursor request_key1 = aws_byte_cursor_from_c_str("requestkey1"); + struct aws_byte_cursor request_key2 = aws_byte_cursor_from_c_str("requestkey2"); + + ASSERT_SUCCESS(s_submit_request_operation_from_prefix(&fixture, request_key1, request_key1)); + ASSERT_SUCCESS(s_submit_request_operation_from_prefix(&fixture, request_key2, request_key2)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + s_rrc_wait_on_request_completion(&fixture, request_key1); + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, request_key1, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + s_rrc_wait_on_request_completion(&fixture, request_key2); + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, request_key2, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_events), expected_events)); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_publishes), expected_publishes)); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_streaming_operation_success_delayed_by_request_operations, + s_rrc_streaming_operation_success_delayed_by_request_operations_fn) + +/* + * Variant of previous test where we sandwich the streaming operation by multiple request response operations and + * verify all request-response operations fail with a timeout. + */ +static int s_rrc_streaming_operation_success_sandwiched_by_request_operations_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_streaming_operation_success( + &fixture, &client_test_options, allocator, s_rrc_unsubscribe_success_config, NULL)); + + struct aws_byte_cursor request_key1 = aws_byte_cursor_from_c_str("requestkey1"); + struct aws_byte_cursor request_key2 = aws_byte_cursor_from_c_str("requestkey2"); + struct aws_byte_cursor request_key3 = aws_byte_cursor_from_c_str("requestkey3"); + struct aws_byte_cursor request_key4 = aws_byte_cursor_from_c_str("requestkey4"); + + ASSERT_SUCCESS(s_submit_request_operation_from_prefix(&fixture, request_key1, request_key1)); + ASSERT_SUCCESS(s_submit_request_operation_from_prefix(&fixture, request_key2, request_key2)); + + struct aws_byte_cursor record_key1 = aws_byte_cursor_from_c_str("key1"); + struct aws_byte_cursor topic_filter1 = aws_byte_cursor_from_c_str("topic/1"); + struct aws_mqtt_rr_client_operation *operation = s_create_streaming_operation(&fixture, record_key1, topic_filter1); + + ASSERT_SUCCESS(s_submit_request_operation_from_prefix(&fixture, request_key3, request_key3)); + ASSERT_SUCCESS(s_submit_request_operation_from_prefix(&fixture, request_key4, request_key4)); + + s_rrc_wait_on_request_completion(&fixture, request_key1); + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, request_key1, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + s_rrc_wait_on_request_completion(&fixture, request_key2); + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, request_key2, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + + s_rrc_wait_for_n_streaming_subscription_events(&fixture, record_key1, 1); + + struct aws_rr_client_fixture_streaming_record_subscription_event expected_events[] = { + { + .status = ARRSSET_SUBSCRIPTION_ESTABLISHED, + .error_code = AWS_ERROR_SUCCESS, + }, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_record_subscription_events( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_events), expected_events)); + + // two publishes on the mqtt client that get reflected into our subscription topic + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("Payload1"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("Payload2"); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload1)); + ASSERT_SUCCESS(s_rrc_protocol_client_publish(&fixture, topic_filter1, payload2)); + + s_rrc_wait_for_n_streaming_publishes(&fixture, record_key1, 2); + + struct aws_byte_cursor expected_publishes[] = { + payload1, + payload2, + }; + ASSERT_SUCCESS(s_rrc_verify_streaming_publishes( + &fixture, record_key1, AWS_ARRAY_SIZE(expected_publishes), expected_publishes)); + + s_rrc_wait_on_request_completion(&fixture, request_key3); + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, request_key3, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + s_rrc_wait_on_request_completion(&fixture, request_key4); + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, request_key4, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + + aws_mqtt_rr_client_operation_release(operation); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_streaming_operation_success_sandwiched_by_request_operations, + s_rrc_streaming_operation_success_sandwiched_by_request_operations_fn) + +enum rrc_publish_handler_directive_type { + RRC_PHDT_SUCCESS, + RRC_PHDT_FAILURE_PUBACK_REASON_CODE, + RRC_PHDT_FAILURE_BAD_PAYLOAD_FORMAT, + RRC_PHDT_FAILURE_MISSING_CORRELATION_TOKEN, + RRC_PHDT_FAILURE_BAD_CORRELATION_TOKEN_TYPE, + RRC_PHDT_FAILURE_MISMATCHED_CORRELATION_TOKEN, +}; + +int aws_mqtt5_mock_server_handle_publish_json_request( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)user_data; + + struct aws_json_value *payload_json = NULL; + struct aws_json_value *response_json = NULL; + struct aws_allocator *allocator = connection->allocator; + + struct aws_mqtt5_packet_publish_view *publish_view = packet; + + /* unmarshal the payload as json */ + payload_json = aws_json_value_new_from_string(allocator, publish_view->payload); + AWS_FATAL_ASSERT(payload_json != NULL); + + /* 'topic' field is where we should publish to */ + struct aws_json_value *topic_value = + aws_json_value_get_from_object(payload_json, aws_byte_cursor_from_c_str("topic")); + AWS_FATAL_ASSERT(topic_value != NULL && aws_json_value_is_string(topic_value)); + + struct aws_byte_cursor topic; + AWS_ZERO_STRUCT(topic); + aws_json_value_get_string(topic_value, &topic); + + /* 'token' field is the correlation token we should reflect */ + struct aws_byte_cursor token; + AWS_ZERO_STRUCT(token); + + struct aws_json_value *token_value = + aws_json_value_get_from_object(payload_json, aws_byte_cursor_from_c_str("token")); + if (token_value != NULL) { + AWS_FATAL_ASSERT(aws_json_value_is_string(token_value)); + aws_json_value_get_string(token_value, &token); + } + + /* 'reflection' field is an optional field we should reflect. Used to ensure proper correlation on requests that + * don't use correlation tokens */ + struct aws_byte_cursor reflection; + AWS_ZERO_STRUCT(reflection); + + struct aws_json_value *reflection_value = + aws_json_value_get_from_object(payload_json, aws_byte_cursor_from_c_str("reflection")); + if (reflection_value != NULL) { + AWS_FATAL_ASSERT(aws_json_value_is_string(reflection_value)); + aws_json_value_get_string(reflection_value, &reflection); + } + + /* 'directive' field indicates how the response handler should behave */ + struct aws_json_value *directive_value = + aws_json_value_get_from_object(payload_json, aws_byte_cursor_from_c_str("directive")); + AWS_FATAL_ASSERT(directive_value != NULL && aws_json_value_is_number(directive_value)); + + double raw_directive_value = 0; + aws_json_value_get_number(directive_value, &raw_directive_value); + enum rrc_publish_handler_directive_type directive = (int)raw_directive_value; + + /* send a PUBACK? */ + if (publish_view->qos == AWS_MQTT5_QOS_AT_LEAST_ONCE) { + struct aws_mqtt5_packet_puback_view puback_view = { + .packet_id = publish_view->packet_id, + .reason_code = (directive == RRC_PHDT_FAILURE_PUBACK_REASON_CODE) ? AWS_MQTT5_PARC_NOT_AUTHORIZED + : AWS_MQTT5_PARC_SUCCESS, + }; + + if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBACK, &puback_view)) { + return AWS_OP_ERR; + } + } + + if (directive == RRC_PHDT_FAILURE_PUBACK_REASON_CODE) { + goto done; + } + + /* build the json response blob */ + char response_buffer[512]; + switch (directive) { + case RRC_PHDT_FAILURE_BAD_PAYLOAD_FORMAT: + snprintf( + response_buffer, + AWS_ARRAY_SIZE(response_buffer), + "" PRInSTR "", + AWS_BYTE_CURSOR_PRI(token)); + break; + case RRC_PHDT_FAILURE_MISSING_CORRELATION_TOKEN: + snprintf( + response_buffer, + AWS_ARRAY_SIZE(response_buffer), + "{\"wrongfield\":\"" PRInSTR "\"}", + AWS_BYTE_CURSOR_PRI(token)); + break; + case RRC_PHDT_FAILURE_BAD_CORRELATION_TOKEN_TYPE: + snprintf(response_buffer, AWS_ARRAY_SIZE(response_buffer), "{\"token\":5}"); + break; + case RRC_PHDT_FAILURE_MISMATCHED_CORRELATION_TOKEN: + snprintf(response_buffer, AWS_ARRAY_SIZE(response_buffer), "{\"token\":\"NotTheToken\"}"); + break; + default: { + int bytes_used = snprintf(response_buffer, AWS_ARRAY_SIZE(response_buffer), "{"); + if (token.len > 0) { + bytes_used += snprintf( + response_buffer + bytes_used, + AWS_ARRAY_SIZE(response_buffer) - bytes_used, + "\"token\":\"" PRInSTR "\"", + AWS_BYTE_CURSOR_PRI(token)); + } + if (reflection.len > 0) { + if (token.len > 0) { + bytes_used += + snprintf(response_buffer + bytes_used, AWS_ARRAY_SIZE(response_buffer) - bytes_used, ","); + } + bytes_used += snprintf( + response_buffer + bytes_used, + AWS_ARRAY_SIZE(response_buffer) - bytes_used, + "\"reflection\":\"" PRInSTR "\"", + AWS_BYTE_CURSOR_PRI(reflection)); + } + snprintf(response_buffer + bytes_used, AWS_ARRAY_SIZE(response_buffer) - bytes_used, "}"); + break; + } + } + + /* build the response publish packet */ + struct aws_mqtt5_packet_publish_view response_publish_view = { + .qos = AWS_MQTT5_QOS_AT_MOST_ONCE, + .topic = topic, + .payload = aws_byte_cursor_from_c_str(response_buffer), + }; + + aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_PUBLISH, &response_publish_view); + +done: + + aws_json_value_destroy(payload_json); + aws_json_value_destroy(response_json); + + return AWS_OP_SUCCESS; +} + +static int s_init_fixture_request_operation_success( + struct aws_rr_client_test_fixture *fixture, + struct mqtt5_client_test_options *client_test_options, + struct aws_allocator *allocator, + modify_fixture_options_fn *config_modifier, + void *user_data) { + + aws_mqtt5_client_test_init_default_options(client_test_options); + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + aws_mqtt5_server_send_suback_on_subscribe; + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = + aws_mqtt5_mock_server_handle_publish_json_request; + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = + aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success; + + struct aws_mqtt5_client_mqtt5_mock_test_fixture_options client_test_fixture_options = { + .client_options = &client_test_options->client_options, + .server_function_table = &client_test_options->server_function_table, + .mock_server_user_data = user_data, + }; + + struct aws_mqtt_request_response_client_options rr_client_options = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = 2, + }; + + if (config_modifier != NULL) { + (*config_modifier)(&rr_client_options, client_test_options); + } + + ASSERT_SUCCESS(s_aws_rr_client_test_fixture_init_from_mqtt5( + fixture, allocator, &rr_client_options, &client_test_fixture_options, NULL)); + + return AWS_OP_SUCCESS; +} + +static int s_rrc_test_submit_test_request( + struct aws_rr_client_test_fixture *fixture, + enum rrc_publish_handler_directive_type test_directive, + const char *topic_prefix, + struct aws_byte_cursor record_key, + const char *response_topic, + const char *token, + const char *reflection, + bool is_multi_subscribe) { + + char path1_buffer[128]; + snprintf(path1_buffer, AWS_ARRAY_SIZE(path1_buffer), "%s/accepted", topic_prefix); + char path2_buffer[128]; + snprintf(path2_buffer, AWS_ARRAY_SIZE(path2_buffer), "%s/rejected", topic_prefix); + + struct aws_byte_cursor token_path = aws_byte_cursor_from_c_str("token"); + if (token == NULL) { + AWS_ZERO_STRUCT(token_path); + } + + struct aws_mqtt_request_operation_response_path response_paths[] = { + { + .topic = aws_byte_cursor_from_c_str(path1_buffer), + .correlation_token_json_path = token_path, + }, + { + .topic = aws_byte_cursor_from_c_str(path2_buffer), + .correlation_token_json_path = token_path, + }, + }; + + char subscription_buffer[128]; + snprintf(subscription_buffer, AWS_ARRAY_SIZE(subscription_buffer), "%s/+", topic_prefix); + struct aws_byte_cursor subscription_buffer_cursor = aws_byte_cursor_from_c_str(subscription_buffer); + + struct aws_byte_cursor *subscriptions = &subscription_buffer_cursor; + size_t subscription_count = 1; + + struct aws_byte_cursor multi_subs[] = { + aws_byte_cursor_from_c_str(path1_buffer), + aws_byte_cursor_from_c_str(path2_buffer), + }; + if (is_multi_subscribe) { + subscriptions = multi_subs; + subscription_count = 2; + } + + char publish_topic_buffer[128]; + snprintf(publish_topic_buffer, AWS_ARRAY_SIZE(publish_topic_buffer), "%s/publish", topic_prefix); + + char request_buffer[512]; + int used_bytes = snprintf( + request_buffer, + AWS_ARRAY_SIZE(request_buffer), + "{\"topic\":\"%s\",\"directive\":%d", + response_topic, + (int)test_directive); + + if (token != NULL) { + used_bytes += snprintf( + request_buffer + used_bytes, AWS_ARRAY_SIZE(request_buffer) - used_bytes, ",\"token\":\"%s\"", token); + } + + if (reflection != NULL) { + used_bytes += snprintf( + request_buffer + used_bytes, + AWS_ARRAY_SIZE(request_buffer) - used_bytes, + ",\"reflection\":\"%s\"", + reflection); + } + + snprintf(request_buffer + used_bytes, AWS_ARRAY_SIZE(request_buffer) - used_bytes, "}"); + + struct aws_mqtt_request_operation_options request = { + .subscription_topic_filters = subscriptions, + .subscription_topic_filter_count = subscription_count, + .response_paths = response_paths, + .response_path_count = AWS_ARRAY_SIZE(response_paths), + .publish_topic = aws_byte_cursor_from_c_str(publish_topic_buffer), + .serialized_request = aws_byte_cursor_from_c_str(request_buffer), + .correlation_token = aws_byte_cursor_from_c_str(token), + }; + + struct aws_rr_client_fixture_request_response_record *record = + s_rrc_fixture_add_request_record(fixture, record_key); + + request.completion_callback = s_rrc_fixture_request_completion_callback; + request.user_data = record; + + return aws_mqtt_request_response_client_submit_request(fixture->rr_client, &request); +} + +static int s_rrc_request_response_success_response_path_accepted_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_SUCCESS, "test", record_key, "test/accepted", "token1", NULL, false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + struct aws_byte_cursor expected_response_topic = aws_byte_cursor_from_c_str("test/accepted"); + struct aws_byte_cursor expected_payload = aws_byte_cursor_from_c_str("{\"token\":\"token1\"}"); + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_SUCCESS, &expected_response_topic, &expected_payload)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_success_response_path_accepted, + s_rrc_request_response_success_response_path_accepted_fn) + +static int s_rrc_request_response_multi_sub_success_response_path_accepted_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_SUCCESS, "test", record_key, "test/accepted", "token1", NULL, true)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + struct aws_byte_cursor expected_response_topic = aws_byte_cursor_from_c_str("test/accepted"); + struct aws_byte_cursor expected_payload = aws_byte_cursor_from_c_str("{\"token\":\"token1\"}"); + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_SUCCESS, &expected_response_topic, &expected_payload)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_multi_sub_success_response_path_accepted, + s_rrc_request_response_multi_sub_success_response_path_accepted_fn) + +static int s_rrc_request_response_success_response_path_rejected_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_SUCCESS, "test", record_key, "test/rejected", "token5", NULL, false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + struct aws_byte_cursor expected_response_topic = aws_byte_cursor_from_c_str("test/rejected"); + struct aws_byte_cursor expected_payload = aws_byte_cursor_from_c_str("{\"token\":\"token5\"}"); + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_SUCCESS, &expected_response_topic, &expected_payload)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_success_response_path_rejected, + s_rrc_request_response_success_response_path_rejected_fn) + +static int s_rrc_request_response_multi_sub_success_response_path_rejected_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_SUCCESS, "test", record_key, "test/rejected", "token5", NULL, true)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + struct aws_byte_cursor expected_response_topic = aws_byte_cursor_from_c_str("test/rejected"); + struct aws_byte_cursor expected_payload = aws_byte_cursor_from_c_str("{\"token\":\"token5\"}"); + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_SUCCESS, &expected_response_topic, &expected_payload)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_multi_sub_success_response_path_rejected, + s_rrc_request_response_multi_sub_success_response_path_rejected_fn) + +static void s_fail_all_subscribes_config_modifier_fn( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options) { + (void)fixture_options; + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_handle_subscribe_with_terminal_failure; +} + +static int s_rrc_request_response_subscribe_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + + struct rrc_subscribe_handler_context subscribe_context = { + .fixture = &fixture, + .subscribes_received = 0, + }; + ASSERT_SUCCESS(s_init_fixture_request_operation_success( + &fixture, &client_test_options, allocator, s_fail_all_subscribes_config_modifier_fn, &subscribe_context)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_SUCCESS, "test", record_key, "test/accepted", "token1", NULL, false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_MQTT_REQUEST_RESPONSE_SUBSCRIBE_FAILURE, NULL, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_request_response_subscribe_failure, s_rrc_request_response_subscribe_failure_fn) + +static enum aws_mqtt5_suback_reason_code s_rrc_usuback_success_rcs[] = { + AWS_MQTT5_SARC_GRANTED_QOS_1, +}; + +int s_handle_second_subscribe_with_failure( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data) { + (void)packet; + + struct rrc_subscribe_handler_context *context = user_data; + + size_t subscribes_received = 0; + + aws_mutex_lock(&context->fixture->lock); + ++context->subscribes_received; + subscribes_received = context->subscribes_received; + aws_mutex_unlock(&context->fixture->lock); + + struct aws_mqtt5_packet_subscribe_view *subscribe_packet = packet; + + struct aws_mqtt5_packet_suback_view suback_view = { + .packet_id = subscribe_packet->packet_id, + .reason_code_count = 1, + .reason_codes = (subscribes_received == 1) ? s_rrc_usuback_success_rcs : s_rrc_unretryable_suback_rcs, + }; + + return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view); +} + +static void s_fail_second_subscribe_config_modifier_fn( + struct aws_mqtt_request_response_client_options *fixture_options, + struct mqtt5_client_test_options *client_test_options) { + (void)fixture_options; + + client_test_options->server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = + s_handle_second_subscribe_with_failure; +} + +static int s_rrc_request_response_multi_subscribe_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + + struct rrc_subscribe_handler_context subscribe_context = { + .fixture = &fixture, + .subscribes_received = 0, + }; + ASSERT_SUCCESS(s_init_fixture_request_operation_success( + &fixture, &client_test_options, allocator, s_fail_second_subscribe_config_modifier_fn, &subscribe_context)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_SUCCESS, "test", record_key, "test/accepted", "token1", NULL, true)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_MQTT_REQUEST_RESPONSE_SUBSCRIBE_FAILURE, NULL, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_request_response_multi_subscribe_failure, s_rrc_request_response_multi_subscribe_failure_fn) + +static int s_rrc_request_response_failure_puback_reason_code_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_FAILURE_PUBACK_REASON_CODE, "test", record_key, "test/accepted", "token1", NULL, false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_MQTT_REQUEST_RESPONSE_PUBLISH_FAILURE, NULL, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_request_response_failure_puback_reason_code, s_rrc_request_response_failure_puback_reason_code_fn) + +static int s_rrc_request_response_failure_invalid_payload_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_FAILURE_BAD_PAYLOAD_FORMAT, "test", record_key, "test/accepted", "token1", NULL, false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, record_key, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_request_response_failure_invalid_payload, s_rrc_request_response_failure_invalid_payload_fn) + +static int s_rrc_request_response_failure_missing_correlation_token_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, + RRC_PHDT_FAILURE_MISSING_CORRELATION_TOKEN, + "test", + record_key, + "test/accepted", + "token1", + NULL, + false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, record_key, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_failure_missing_correlation_token, + s_rrc_request_response_failure_missing_correlation_token_fn) + +static int s_rrc_request_response_failure_invalid_correlation_token_type_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, + RRC_PHDT_FAILURE_BAD_CORRELATION_TOKEN_TYPE, + "test", + record_key, + "test/accepted", + "token1", + NULL, + false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, record_key, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_failure_invalid_correlation_token_type, + s_rrc_request_response_failure_invalid_correlation_token_type_fn) + +static int s_rrc_request_response_failure_non_matching_correlation_token_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, + RRC_PHDT_FAILURE_MISMATCHED_CORRELATION_TOKEN, + "test", + record_key, + "test/accepted", + "token1", + NULL, + false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + ASSERT_SUCCESS( + s_rrc_verify_request_completion(&fixture, record_key, AWS_ERROR_MQTT_REQUEST_RESPONSE_TIMEOUT, NULL, NULL)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_failure_non_matching_correlation_token, + s_rrc_request_response_failure_non_matching_correlation_token_fn) + +static int s_rrc_request_response_success_empty_correlation_token_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str("testkey"); + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_SUCCESS, "test", record_key, "test/accepted", NULL, NULL, false)); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + struct aws_byte_cursor expected_response_topic = aws_byte_cursor_from_c_str("test/accepted"); + struct aws_byte_cursor expected_payload = aws_byte_cursor_from_c_str("{}"); + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_SUCCESS, &expected_response_topic, &expected_payload)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_success_empty_correlation_token, + s_rrc_request_response_success_empty_correlation_token_fn) + +static int s_rrc_request_response_success_empty_correlation_token_sequence_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + for (size_t i = 0; i < 20; ++i) { + char key_buffer[128]; + snprintf(key_buffer, AWS_ARRAY_SIZE(key_buffer), "testkey%zu", i); + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str(key_buffer); + + char prefix_buffer[128]; + snprintf(prefix_buffer, AWS_ARRAY_SIZE(prefix_buffer), "test%zu", i); + + char response_topic_buffer[128]; + snprintf(response_topic_buffer, AWS_ARRAY_SIZE(response_topic_buffer), "test%zu/accepted", i); + + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + &fixture, RRC_PHDT_SUCCESS, prefix_buffer, record_key, response_topic_buffer, NULL, NULL, false)); + } + + for (size_t i = 0; i < 20; ++i) { + char key_buffer[128]; + snprintf(key_buffer, AWS_ARRAY_SIZE(key_buffer), "testkey%zu", i); + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str(key_buffer); + + char response_topic_buffer[128]; + snprintf(response_topic_buffer, AWS_ARRAY_SIZE(response_topic_buffer), "test%zu/accepted", i); + + s_rrc_wait_on_request_completion(&fixture, record_key); + + struct aws_byte_cursor expected_response_topic = aws_byte_cursor_from_c_str(response_topic_buffer); + struct aws_byte_cursor expected_payload = aws_byte_cursor_from_c_str("{}"); + ASSERT_SUCCESS(s_rrc_verify_request_completion( + &fixture, record_key, AWS_ERROR_SUCCESS, &expected_response_topic, &expected_payload)); + } + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrc_request_response_success_empty_correlation_token_sequence, + s_rrc_request_response_success_empty_correlation_token_sequence_fn) + +struct rrc_multi_test_operation { + const char *prefix; + const char *token; + const char *reflection; +}; + +static int s_do_rrc_operation_sequence_test( + struct aws_rr_client_test_fixture *fixture, + size_t operation_count, + struct rrc_multi_test_operation *operations) { + for (size_t i = 0; i < operation_count; ++i) { + + struct rrc_multi_test_operation *operation = &operations[i]; + + char key_buffer[128]; + snprintf(key_buffer, AWS_ARRAY_SIZE(key_buffer), "testkey%zu", i); + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str(key_buffer); + + char response_topic_buffer[128]; + snprintf(response_topic_buffer, AWS_ARRAY_SIZE(response_topic_buffer), "%s/accepted", operation->prefix); + + ASSERT_SUCCESS(s_rrc_test_submit_test_request( + fixture, + RRC_PHDT_SUCCESS, + operation->prefix, + record_key, + response_topic_buffer, + operation->token, + operation->reflection, + false)); + } + + for (size_t i = 0; i < operation_count; ++i) { + + struct rrc_multi_test_operation *operation = &operations[i]; + + char key_buffer[128]; + snprintf(key_buffer, AWS_ARRAY_SIZE(key_buffer), "testkey%zu", i); + struct aws_byte_cursor record_key = aws_byte_cursor_from_c_str(key_buffer); + + char response_topic_buffer[128]; + snprintf(response_topic_buffer, AWS_ARRAY_SIZE(response_topic_buffer), "%s/accepted", operation->prefix); + + s_rrc_wait_on_request_completion(fixture, record_key); + + struct aws_byte_cursor expected_response_topic = aws_byte_cursor_from_c_str(response_topic_buffer); + struct aws_byte_cursor expected_payload; + AWS_ZERO_STRUCT(expected_payload); + char payload_buffer[256]; + int bytes_used = snprintf(payload_buffer, AWS_ARRAY_SIZE(payload_buffer), "{"); + if (operation->token != NULL) { + bytes_used += snprintf( + payload_buffer + bytes_used, + AWS_ARRAY_SIZE(payload_buffer) - bytes_used, + "\"token\":\"%s\"", + operation->token); + if (operation->reflection != NULL) { + bytes_used += snprintf(payload_buffer + bytes_used, AWS_ARRAY_SIZE(payload_buffer) - bytes_used, ","); + } + } + if (operation->reflection != NULL) { + bytes_used += snprintf( + payload_buffer + bytes_used, + AWS_ARRAY_SIZE(payload_buffer) - bytes_used, + "\"reflection\":\"%s\"", + operation->reflection); + } + snprintf(payload_buffer + bytes_used, AWS_ARRAY_SIZE(payload_buffer) - bytes_used, "}"); + + expected_payload = aws_byte_cursor_from_c_str(payload_buffer); + ASSERT_SUCCESS(s_rrc_verify_request_completion( + fixture, record_key, AWS_ERROR_SUCCESS, &expected_response_topic, &expected_payload)); + } + + return AWS_OP_SUCCESS; +} + +static int s_rrc_request_response_multi_operation_sequence_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt5_client_test_options client_test_options; + struct aws_rr_client_test_fixture fixture; + ASSERT_SUCCESS(s_init_fixture_request_operation_success(&fixture, &client_test_options, allocator, NULL, NULL)); + + struct rrc_multi_test_operation request_sequence[] = { + { + .prefix = "test", + .token = "token1", + }, + { + .prefix = "test", + .token = "token2", + }, + { + .prefix = "test2", + .token = "token3", + }, + { + .prefix = "whatthe", + .token = "hey", + .reflection = "something", + }, + { + .prefix = "test", + .token = "token4", + }, + { + .prefix = "test2", + .token = "token5", + }, + { + .prefix = "provision", + .reflection = "provision1", + }, + { + .prefix = "provision", + .reflection = "provision2", + }, + { + .prefix = "create-keys-and-cert", + .reflection = "create-keys1", + }, + { + .prefix = "test", + .token = "token6", + }, + { + .prefix = "test2", + .token = "token7", + }, + { + .prefix = "provision", + .reflection = "provision3", + }, + }; + + ASSERT_SUCCESS(s_do_rrc_operation_sequence_test(&fixture, AWS_ARRAY_SIZE(request_sequence), request_sequence)); + + s_aws_rr_client_test_fixture_clean_up(&fixture); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrc_request_response_multi_operation_sequence, s_rrc_request_response_multi_operation_sequence_fn) diff --git a/tests/request-response/subscription_manager_tests.c b/tests/request-response/subscription_manager_tests.c new file mode 100644 index 00000000..cf55fe2f --- /dev/null +++ b/tests/request-response/subscription_manager_tests.c @@ -0,0 +1,2877 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include + +#include + +enum aws_protocol_adapter_api_type { + PAAT_SUBSCRIBE, + PAAT_UNSUBSCRIBE, +}; + +struct aws_protocol_adapter_api_record { + enum aws_protocol_adapter_api_type type; + + struct aws_byte_buf topic_filter; + struct aws_byte_cursor topic_filter_cursor; + + uint32_t timeout; +}; + +static void s_aws_protocol_adapter_api_record_init_from_subscribe( + struct aws_protocol_adapter_api_record *record, + struct aws_allocator *allocator, + struct aws_byte_cursor topic_filter, + uint32_t timeout) { + AWS_ZERO_STRUCT(*record); + record->type = PAAT_SUBSCRIBE; + record->timeout = timeout; + + aws_byte_buf_init_copy_from_cursor(&record->topic_filter, allocator, topic_filter); + record->topic_filter_cursor = aws_byte_cursor_from_buf(&record->topic_filter); +} + +static void s_aws_protocol_adapter_api_record_init_from_unsubscribe( + struct aws_protocol_adapter_api_record *record, + struct aws_allocator *allocator, + struct aws_byte_cursor topic_filter, + uint32_t timeout) { + AWS_ZERO_STRUCT(*record); + record->type = PAAT_UNSUBSCRIBE; + record->timeout = timeout; + + aws_byte_buf_init_copy_from_cursor(&record->topic_filter, allocator, topic_filter); + record->topic_filter_cursor = aws_byte_cursor_from_buf(&record->topic_filter); +} + +static void s_aws_protocol_adapter_api_record_clean_up(struct aws_protocol_adapter_api_record *record) { + aws_byte_buf_clean_up(&record->topic_filter); +} + +struct aws_mqtt_protocol_adapter_mock_impl { + struct aws_allocator *allocator; + struct aws_mqtt_protocol_adapter base; + + struct aws_array_list api_records; + bool is_connected; + size_t subscribe_count; +}; + +static void s_aws_mqtt_protocol_adapter_mock_destroy(void *impl) { + struct aws_mqtt_protocol_adapter_mock_impl *adapter = impl; + + size_t record_count = aws_array_list_length(&adapter->api_records); + for (size_t i = 0; i < record_count; ++i) { + struct aws_protocol_adapter_api_record *record = NULL; + aws_array_list_get_at_ptr(&adapter->api_records, (void **)&record, i); + + s_aws_protocol_adapter_api_record_clean_up(record); + } + + aws_array_list_clean_up(&adapter->api_records); + + aws_mem_release(adapter->allocator, adapter); +} + +static int s_aws_mqtt_protocol_adapter_mock_subscribe( + void *impl, + struct aws_protocol_adapter_subscribe_options *options) { + struct aws_mqtt_protocol_adapter_mock_impl *adapter = impl; + + struct aws_protocol_adapter_api_record record; + s_aws_protocol_adapter_api_record_init_from_subscribe( + &record, adapter->allocator, options->topic_filter, options->ack_timeout_seconds); + + aws_array_list_push_back(&adapter->api_records, &record); + + return AWS_OP_SUCCESS; +} + +static int s_aws_mqtt_protocol_adapter_mock_unsubscribe( + void *impl, + struct aws_protocol_adapter_unsubscribe_options *options) { + struct aws_mqtt_protocol_adapter_mock_impl *adapter = impl; + + struct aws_protocol_adapter_api_record record; + s_aws_protocol_adapter_api_record_init_from_unsubscribe( + &record, adapter->allocator, options->topic_filter, options->ack_timeout_seconds); + + aws_array_list_push_back(&adapter->api_records, &record); + + return AWS_OP_SUCCESS; +} + +static bool s_aws_mqtt_protocol_adapter_mock_is_connected(void *impl) { + struct aws_mqtt_protocol_adapter_mock_impl *adapter = impl; + + return adapter->is_connected; +} + +static struct aws_mqtt_protocol_adapter_vtable s_protocol_adapter_mock_vtable = { + .aws_mqtt_protocol_adapter_destroy_fn = s_aws_mqtt_protocol_adapter_mock_destroy, + .aws_mqtt_protocol_adapter_subscribe_fn = s_aws_mqtt_protocol_adapter_mock_subscribe, + .aws_mqtt_protocol_adapter_unsubscribe_fn = s_aws_mqtt_protocol_adapter_mock_unsubscribe, + .aws_mqtt_protocol_adapter_publish_fn = NULL, + .aws_mqtt_protocol_adapter_is_connected_fn = s_aws_mqtt_protocol_adapter_mock_is_connected, +}; + +static struct aws_mqtt_protocol_adapter *s_aws_mqtt_mock_protocol_adapter_new( + struct aws_allocator *allocator, + const struct aws_mqtt_protocol_adapter_vtable *vtable, + bool is_connected) { + struct aws_mqtt_protocol_adapter_mock_impl *adapter = + aws_mem_calloc(allocator, 1, sizeof(struct aws_mqtt_protocol_adapter_mock_impl)); + + adapter->allocator = allocator; + adapter->base.impl = adapter; + if (vtable != NULL) { + adapter->base.vtable = vtable; + } else { + adapter->base.vtable = &s_protocol_adapter_mock_vtable; + } + adapter->is_connected = is_connected; + aws_array_list_init_dynamic(&adapter->api_records, allocator, 10, sizeof(struct aws_protocol_adapter_api_record)); + + return &adapter->base; +} + +static bool s_protocol_adapter_api_records_equal( + struct aws_protocol_adapter_api_record *record1, + struct aws_protocol_adapter_api_record *record2) { + if (record1->type != record2->type) { + return false; + } + + if (record1->timeout != record2->timeout) { + return false; + } + + if (!aws_byte_cursor_eq(&record1->topic_filter_cursor, &record2->topic_filter_cursor)) { + return false; + } + + return true; +} + +static bool s_api_records_contains_record( + struct aws_mqtt_protocol_adapter *protocol_adapter, + struct aws_protocol_adapter_api_record *expected_record) { + + struct aws_mqtt_protocol_adapter_mock_impl *adapter = protocol_adapter->impl; + + size_t record_count = aws_array_list_length(&adapter->api_records); + for (size_t i = 0; i < record_count; ++i) { + struct aws_protocol_adapter_api_record *actual_record = NULL; + aws_array_list_get_at_ptr(&adapter->api_records, (void **)&actual_record, i); + + if (s_protocol_adapter_api_records_equal(expected_record, actual_record)) { + return true; + } + } + + return false; +} + +static bool s_api_records_contains_range( + struct aws_mqtt_protocol_adapter *protocol_adapter, + size_t range_start, + size_t range_end, + struct aws_protocol_adapter_api_record *expected_records) { + struct aws_mqtt_protocol_adapter_mock_impl *adapter = protocol_adapter->impl; + + size_t record_count = aws_array_list_length(&adapter->api_records); + if (record_count < range_end) { + return false; + } + + for (size_t i = range_start; i < range_end; ++i) { + struct aws_protocol_adapter_api_record *expected_record = &expected_records[i - range_start]; + + struct aws_protocol_adapter_api_record *actual_record = NULL; + aws_array_list_get_at_ptr(&adapter->api_records, (void **)&actual_record, i); + + if (!s_protocol_adapter_api_records_equal(expected_record, actual_record)) { + return false; + } + } + + return true; +} + +static bool s_api_records_equals( + struct aws_mqtt_protocol_adapter *protocol_adapter, + size_t record_count, + struct aws_protocol_adapter_api_record *expected_records) { + struct aws_mqtt_protocol_adapter_mock_impl *adapter = protocol_adapter->impl; + + return aws_array_list_length(&adapter->api_records) == record_count && + s_api_records_contains_range(protocol_adapter, 0, record_count, expected_records); +} + +////////////////////////////////////////////////////////////// + +struct aws_subscription_status_record { + enum aws_rr_subscription_event_type type; + struct aws_byte_buf topic_filter; + struct aws_byte_cursor topic_filter_cursor; + uint64_t operation_id; +}; + +static void s_aws_subscription_status_record_init_from_event( + struct aws_subscription_status_record *record, + struct aws_allocator *allocator, + const struct aws_rr_subscription_status_event *event) { + AWS_ZERO_STRUCT(*record); + record->type = event->type; + record->operation_id = event->operation_id; + + aws_byte_buf_init_copy_from_cursor(&record->topic_filter, allocator, event->topic_filter); + record->topic_filter_cursor = aws_byte_cursor_from_buf(&record->topic_filter); +} + +static void s_aws_subscription_status_record_clean_up(struct aws_subscription_status_record *record) { + aws_byte_buf_clean_up(&record->topic_filter); +} + +////////////////////////////////////////////////////////////// + +struct aws_subscription_manager_test_fixture { + struct aws_allocator *allocator; + + struct aws_mqtt_protocol_adapter *mock_protocol_adapter; + struct aws_rr_subscription_manager subscription_manager; + + struct aws_array_list subscription_status_records; +}; + +static void s_aws_rr_subscription_status_event_test_callback_fn( + const struct aws_rr_subscription_status_event *event, + void *userdata) { + struct aws_subscription_manager_test_fixture *fixture = userdata; + + struct aws_subscription_status_record record; + s_aws_subscription_status_record_init_from_event(&record, fixture->allocator, event); + + aws_array_list_push_back(&fixture->subscription_status_records, &record); +} + +struct aws_subscription_manager_test_fixture_options { + uint32_t operation_timeout_seconds; + size_t max_request_response_subscriptions; + size_t max_streaming_subscriptions; + bool start_connected; + const struct aws_mqtt_protocol_adapter_vtable *adapter_vtable; +}; + +static const uint32_t DEFAULT_SM_TEST_TIMEOUT = 5; + +static int s_aws_subscription_manager_test_fixture_init( + struct aws_subscription_manager_test_fixture *fixture, + struct aws_allocator *allocator, + const struct aws_subscription_manager_test_fixture_options *options) { + AWS_ZERO_STRUCT(*fixture); + + struct aws_subscription_manager_test_fixture_options default_options = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = DEFAULT_SM_TEST_TIMEOUT, + .start_connected = true, + }; + + if (options == NULL) { + options = &default_options; + } + + fixture->allocator = allocator; + fixture->mock_protocol_adapter = + s_aws_mqtt_mock_protocol_adapter_new(allocator, options->adapter_vtable, options->start_connected); + + aws_array_list_init_dynamic( + &fixture->subscription_status_records, allocator, 10, sizeof(struct aws_subscription_status_record)); + + struct aws_rr_subscription_manager_options subscription_manager_options = { + .max_request_response_subscriptions = options->max_request_response_subscriptions, + .max_streaming_subscriptions = options->max_streaming_subscriptions, + .operation_timeout_seconds = options->operation_timeout_seconds, + .subscription_status_callback = s_aws_rr_subscription_status_event_test_callback_fn, + .userdata = fixture}; + aws_rr_subscription_manager_init( + &fixture->subscription_manager, allocator, fixture->mock_protocol_adapter, &subscription_manager_options); + + return AWS_OP_SUCCESS; +} + +static void s_aws_subscription_manager_test_fixture_clean_up(struct aws_subscription_manager_test_fixture *fixture) { + + aws_rr_subscription_manager_clean_up(&fixture->subscription_manager); + aws_mqtt_protocol_adapter_destroy(fixture->mock_protocol_adapter); + + size_t record_count = aws_array_list_length(&fixture->subscription_status_records); + for (size_t i = 0; i < record_count; ++i) { + struct aws_subscription_status_record *record = NULL; + aws_array_list_get_at_ptr(&fixture->subscription_status_records, (void **)&record, i); + + s_aws_subscription_status_record_clean_up(record); + } + + aws_array_list_clean_up(&fixture->subscription_status_records); +} + +static bool s_find_subscription_event_record( + struct aws_subscription_manager_test_fixture *fixture, + struct aws_subscription_status_record *expected_record, + size_t *index) { + + size_t record_count = aws_array_list_length(&fixture->subscription_status_records); + for (size_t i = 0; i < record_count; ++i) { + + struct aws_subscription_status_record *actual_record = NULL; + aws_array_list_get_at_ptr(&fixture->subscription_status_records, (void **)&actual_record, i); + + if (expected_record->type != actual_record->type) { + continue; + } + + if (expected_record->operation_id != actual_record->operation_id) { + continue; + } + + if (!aws_byte_cursor_eq(&expected_record->topic_filter_cursor, &actual_record->topic_filter_cursor)) { + continue; + } + + *index = i; + return true; + } + + return false; +} + +static bool s_contains_subscription_event_record( + struct aws_subscription_manager_test_fixture *fixture, + struct aws_subscription_status_record *record) { + size_t index = 0; + return s_find_subscription_event_record(fixture, record, &index); +} + +static bool s_contains_subscription_event_records( + struct aws_subscription_manager_test_fixture *fixture, + size_t record_count, + struct aws_subscription_status_record *records) { + for (size_t i = 0; i < record_count; ++i) { + if (!s_contains_subscription_event_record(fixture, &records[i])) { + return false; + } + } + + return true; +} + +static bool s_contains_subscription_event_sequential_records( + struct aws_subscription_manager_test_fixture *fixture, + size_t record_count, + struct aws_subscription_status_record *records) { + size_t previous_index = 0; + for (size_t i = 0; i < record_count; ++i) { + size_t index = 0; + if (!s_find_subscription_event_record(fixture, &records[i], &index)) { + return false; + } + + if (i > 0) { + if (index <= previous_index) { + return false; + } + } + previous_index = index; + } + + return true; +} + +static char s_hello_world1[] = "hello/world1"; +static struct aws_byte_cursor s_hello_world1_cursor = { + .ptr = (uint8_t *)s_hello_world1, + .len = AWS_ARRAY_SIZE(s_hello_world1) - 1, +}; + +static char s_hello_world2[] = "hello/world2"; +static struct aws_byte_cursor s_hello_world2_cursor = { + .ptr = (uint8_t *)s_hello_world2, + .len = AWS_ARRAY_SIZE(s_hello_world2) - 1, +}; + +static char s_hello_world3[] = "hello/world3"; +static struct aws_byte_cursor s_hello_world3_cursor = { + .ptr = (uint8_t *)s_hello_world3, + .len = AWS_ARRAY_SIZE(s_hello_world3) - 1, +}; + +/* + * Verify: Acquiring a new subscription triggers a protocol client subscribe and returns SUBSCRIBING + */ +static int s_rrsm_acquire_subscribing_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world3_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 1, expected_subscribes)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + struct aws_rr_acquire_subscription_options acquire3_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world3_cursor, + .topic_filter_count = 1, + .operation_id = 3, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire3_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 3, expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_subscribing, s_rrsm_acquire_subscribing_fn) + +/* + * Verify: Acquiring a new multi-topic-filter subscription triggers two protocol client subscribes and returns + * SUBSCRIBING + */ +static int s_rrsm_acquire_multi_subscribing_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_byte_cursor multi_filters[] = { + s_hello_world1_cursor, + s_hello_world2_cursor, + }; + + struct aws_rr_acquire_subscription_options acquire_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_filters, + .topic_filter_count = AWS_ARRAY_SIZE(multi_filters), + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_multi_subscribing, s_rrsm_acquire_multi_subscribing_fn) + +/* + * Verify: Acquiring an existing, incomplete subscription does not trigger a protocol client subscribe and returns + * SUBSCRIBING + */ +static int s_rrsm_acquire_existing_subscribing_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 1, expected_subscribes)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + struct aws_rr_acquire_subscription_options reacquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 3, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &reacquire1_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + struct aws_rr_acquire_subscription_options reacquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 4, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &reacquire2_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_existing_subscribing, s_rrsm_acquire_existing_subscribing_fn) + +/* + * Verify: Acquiring multiple existing, incomplete subscriptions does not trigger a protocol client subscribe and + * returns SUBSCRIBING + */ +static int s_rrsm_acquire_multi_existing_subscribing_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_byte_cursor multi_subs[] = { + s_hello_world1_cursor, + s_hello_world2_cursor, + }; + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + ASSERT_TRUE( + s_api_records_equals(fixture.mock_protocol_adapter, AWS_ARRAY_SIZE(expected_subscribes), expected_subscribes)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_multi_existing_subscribing, s_rrsm_acquire_multi_existing_subscribing_fn) + +/* + * Verify: A two-subscription acquire where one is already subscribing triggers a single subscribe and returns + * SUBSCRIBING + */ +static int s_rrsm_acquire_multi_partially_subscribed_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_byte_cursor multi_subs[] = { + s_hello_world1_cursor, + s_hello_world2_cursor, + }; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 1, expected_subscribes)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_multi_partially_subscribed, s_rrsm_acquire_multi_partially_subscribed_fn) + +/* + * Verify: Acquiring an existing, completed request subscription does not trigger a protocol client subscribe and + * returns SUBSCRIBED. Verify request and streaming subscription events are emitted. + */ +static int s_rrsm_acquire_existing_subscribed_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 1, expected_subscribes)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + struct aws_protocol_adapter_subscription_event successful_subscription1_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription1_event); + + struct aws_subscription_status_record expected_subscription_events[] = { + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }, + { + .type = ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED, + .topic_filter_cursor = s_hello_world2_cursor, + .operation_id = 2, + }}; + ASSERT_TRUE(s_contains_subscription_event_sequential_records(&fixture, 1, expected_subscription_events)); + + struct aws_protocol_adapter_subscription_event successful_subscription2_event = { + .topic_filter = s_hello_world2_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription2_event); + + ASSERT_TRUE(s_contains_subscription_event_sequential_records(&fixture, 2, expected_subscription_events)); + + struct aws_rr_acquire_subscription_options reacquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 3, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBED, aws_rr_subscription_manager_acquire_subscription(manager, &reacquire1_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + struct aws_rr_acquire_subscription_options reacquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 4, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBED, aws_rr_subscription_manager_acquire_subscription(manager, &reacquire2_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_existing_subscribed, s_rrsm_acquire_existing_subscribed_fn) + +/* + * Verify: A multi-sub acquire where all subscriptions are currently subscribed + * returns SUBSCRIBED. Verify subscription events are emitted. + */ +static int s_rrsm_acquire_multi_existing_subscribed_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_byte_cursor multi_subs[] = { + s_hello_world1_cursor, + s_hello_world2_cursor, + }; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + ASSERT_TRUE( + s_api_records_equals(fixture.mock_protocol_adapter, AWS_ARRAY_SIZE(expected_subscribes), expected_subscribes)); + + struct aws_protocol_adapter_subscription_event successful_subscription1_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription1_event); + + struct aws_protocol_adapter_subscription_event successful_subscription2_event = { + .topic_filter = s_hello_world2_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription2_event); + + struct aws_subscription_status_record expected_subscription_events[] = { + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }, + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world2_cursor, + .operation_id = 1, + }}; + ASSERT_TRUE(s_contains_subscription_event_sequential_records( + &fixture, AWS_ARRAY_SIZE(expected_subscription_events), expected_subscription_events)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBED, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 2, expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_multi_existing_subscribed, s_rrsm_acquire_multi_existing_subscribed_fn) + +/* + * Verify: Acquiring a new request-response subscription when there is no room returns BLOCKED + */ +static int s_rrsm_acquire_blocked_rr_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 1, + .operation_timeout_seconds = 30, + }; + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire1_options)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire2_options)); + + // no room, but it could potentially free up in the future + struct aws_rr_acquire_subscription_options acquire3_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world3_cursor, + .topic_filter_count = 1, + .operation_id = 3, + }; + ASSERT_INT_EQUALS( + AASRT_BLOCKED, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire3_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_blocked_rr, s_rrsm_acquire_blocked_rr_fn) + +/* + * Verify: A two-subscription acquire returns BLOCKED when only one space is available + */ +static int s_rrsm_acquire_multi_blocked_rr_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 1, + .operation_timeout_seconds = 30, + }; + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire1_options)); + + struct aws_byte_cursor multi_subs[] = { + s_hello_world2_cursor, + s_hello_world3_cursor, + }; + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + ASSERT_INT_EQUALS( + AASRT_BLOCKED, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire2_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_multi_blocked_rr, s_rrsm_acquire_multi_blocked_rr_fn) + +/* + * Verify: Acquiring a new eventstream subscription when there is no room, but room could free up later, returns BLOCKED + */ +static int s_rrsm_acquire_blocked_eventstream_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 1, + .operation_timeout_seconds = DEFAULT_SM_TEST_TIMEOUT, + }; + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_protocol_adapter_connection_event connected_event = { + .event_type = AWS_PACET_CONNECTED, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(&fixture.subscription_manager, &connected_event); + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire1_options)); + + struct aws_protocol_adapter_subscription_event subscribe_success_event = { + .event_type = AWS_PASET_SUBSCRIBE, + .topic_filter = s_hello_world1_cursor, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event( + &fixture.subscription_manager, &subscribe_success_event); + + // release and trigger an unsubscribe + struct aws_rr_release_subscription_options release1_options = { + .operation_id = 1, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + }; + aws_rr_subscription_manager_release_subscription(&fixture.subscription_manager, &release1_options); + aws_rr_subscription_manager_purge_unused(&fixture.subscription_manager); + + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // acquire while the streaming unsubscribe is in-progress should return blocked + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS( + AASRT_BLOCKED, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire2_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_blocked_eventstream, s_rrsm_acquire_blocked_eventstream_fn) + +/* + * Verify: A two-subscription streaming acquire when there is no room, but room could free up later, returns BLOCKED + */ +static int s_rrsm_acquire_multi_blocked_eventstream_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = DEFAULT_SM_TEST_TIMEOUT, + }; + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_protocol_adapter_connection_event connected_event = { + .event_type = AWS_PACET_CONNECTED, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(&fixture.subscription_manager, &connected_event); + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire1_options)); + + struct aws_protocol_adapter_subscription_event subscribe_success_event = { + .event_type = AWS_PASET_SUBSCRIBE, + .topic_filter = s_hello_world1_cursor, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event( + &fixture.subscription_manager, &subscribe_success_event); + + // release and trigger an unsubscribe + struct aws_rr_release_subscription_options release1_options = { + .operation_id = 1, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + }; + aws_rr_subscription_manager_release_subscription(&fixture.subscription_manager, &release1_options); + aws_rr_subscription_manager_purge_unused(&fixture.subscription_manager); + + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // multi-acquire while the streaming unsubscribe is in-progress should return blocked + struct aws_byte_cursor multi_subs[] = { + s_hello_world2_cursor, + s_hello_world3_cursor, + }; + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + ASSERT_INT_EQUALS( + AASRT_BLOCKED, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire2_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_multi_blocked_eventstream, s_rrsm_acquire_multi_blocked_eventstream_fn) + +static int s_do_acquire_no_capacity_test(struct aws_subscription_manager_test_fixture *fixture) { + struct aws_rr_subscription_manager *manager = &fixture->subscription_manager; + + struct aws_rr_acquire_subscription_options acquire_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_NO_CAPACITY, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + return AWS_OP_SUCCESS; +} + +/* + * Verify: Acquiring a new eventstream subscription when the budget is 0 returns NO_CAPACITY + */ +static int s_rrsm_acquire_no_capacity_max1_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 0, + .operation_timeout_seconds = 30, + }; + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + ASSERT_SUCCESS(s_do_acquire_no_capacity_test(&fixture)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_no_capacity_max1, s_rrsm_acquire_no_capacity_max1_fn) + +/* + * Verify: Acquiring a new eventstream subscription when there is no room, and no room could free up later, returns + * NO_CAPACITY + */ +static int s_rrsm_acquire_no_capacity_too_many_event_stream_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 4, + .operation_timeout_seconds = 30, + }; + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + for (size_t i = 0; i < 4; ++i) { + char topic_filter_buffer[256]; + snprintf(topic_filter_buffer, AWS_ARRAY_SIZE(topic_filter_buffer), "hello/world/%d", (int)(i + 1)); + struct aws_byte_cursor topic_filter_cursor = aws_byte_cursor_from_c_str(topic_filter_buffer); + + struct aws_rr_acquire_subscription_options acquire_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &topic_filter_cursor, + .topic_filter_count = 1, + .operation_id = i + 2, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire_options)); + } + + // 1 more, should fail + ASSERT_SUCCESS(s_do_acquire_no_capacity_test(&fixture)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_no_capacity_too_many_event_stream, s_rrsm_acquire_no_capacity_too_many_event_stream_fn) + +/* + * Verify: A two-subscription streaming acquire, where there is only room for one, and no room could free up later, + * returns NO_CAPACITY + */ +static int s_rrsm_acquire_multi_no_capacity_event_stream_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = 30, + }; + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire1_options)); + + struct aws_byte_cursor multi_subs[] = { + s_hello_world2_cursor, + s_hello_world3_cursor, + }; + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + ASSERT_INT_EQUALS( + AASRT_NO_CAPACITY, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire2_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_multi_no_capacity_event_stream, s_rrsm_acquire_multi_no_capacity_event_stream_fn) + +/* + * Verify: Acquiring an existing subscription with an unequal subscription type returns FAILURE + */ +static int s_rrsm_acquire_failure_mixed_subscription_types_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire1_options)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS( + AASRT_FAILURE, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire2_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_failure_mixed_subscription_types, s_rrsm_acquire_failure_mixed_subscription_types_fn) + +/* + * Verify: A two-subscription acquire that partially overlaps a subscription of different type results in FAILURE + */ +static int s_rrsm_acquire_multi_failure_mixed_subscription_types_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS( + AASRT_SUBSCRIBING, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire1_options)); + + struct aws_byte_cursor multi_subs[] = { + s_hello_world2_cursor, + s_hello_world1_cursor, + }; + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + ASSERT_INT_EQUALS( + AASRT_FAILURE, + aws_rr_subscription_manager_acquire_subscription(&fixture.subscription_manager, &acquire2_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrsm_acquire_multi_failure_mixed_subscription_types, + s_rrsm_acquire_multi_failure_mixed_subscription_types_fn) + +/* + * Verify: Acquiring a poisoned streaming subscription results in failure. + */ +static int s_rrsm_acquire_failure_poisoned_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + + struct aws_protocol_adapter_subscription_event unretryable_failure_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE, + .retryable = false, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &unretryable_failure_event); + + struct aws_subscription_status_record expected_subscription_events[] = {{ + .type = ARRSET_STREAMING_SUBSCRIPTION_HALTED, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }}; + ASSERT_TRUE(s_contains_subscription_event_sequential_records(&fixture, 1, expected_subscription_events)); + + struct aws_rr_acquire_subscription_options reacquire1_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 3, + }; + ASSERT_INT_EQUALS(AASRT_FAILURE, aws_rr_subscription_manager_acquire_subscription(manager, &reacquire1_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_failure_poisoned, s_rrsm_acquire_failure_poisoned_fn) + +/* + * Verify: A request subscription that resolves successfully invokes callbacks for every operation listener; releasing + * both references and purging will trigger an unsubscribe of the first subscription + */ +static int s_rrsm_release_unsubscribes_request_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + + struct aws_protocol_adapter_subscription_event successful_subscription_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription_event); + + // verify two success callbacks + struct aws_subscription_status_record expected_subscription_events[] = { + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }, + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 2, + }}; + ASSERT_TRUE(s_contains_subscription_event_records(&fixture, 2, expected_subscription_events)); + + // verify no unsubscribes + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // release once, verify no unsubscribe + struct aws_rr_release_subscription_options release1_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + aws_rr_subscription_manager_release_subscription(manager, &release1_options); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // release second + struct aws_rr_release_subscription_options release2_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + aws_rr_subscription_manager_release_subscription(manager, &release2_options); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + aws_rr_subscription_manager_purge_unused(manager); + + // now the unsubscribe should be present + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_release_unsubscribes_request, s_rrsm_release_unsubscribes_request_fn) + +/* + * Verify: A multi subscription that resolves successfully invokes callbacks for every operation listener; releasing + * all references and purging will trigger an unsubscribe of both subscriptions + */ +static int s_rrsm_release_multi_unsubscribes_request_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_byte_cursor multi_subs[] = { + s_hello_world1_cursor, + s_hello_world2_cursor, + }; + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + + struct aws_protocol_adapter_subscription_event successful_subscription_event1 = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription_event1); + + struct aws_protocol_adapter_subscription_event successful_subscription_event2 = { + .topic_filter = s_hello_world2_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription_event2); + + // verify four success callbacks + struct aws_subscription_status_record expected_subscription_events[] = { + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }, + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 2, + }, + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world2_cursor, + .operation_id = 1, + }, + { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world2_cursor, + .operation_id = 2, + }, + }; + ASSERT_TRUE(s_contains_subscription_event_records( + &fixture, AWS_ARRAY_SIZE(expected_subscription_events), expected_subscription_events)); + + // verify no unsubscribes + struct aws_protocol_adapter_api_record expected_unsubscribe1 = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe1)); + + struct aws_protocol_adapter_api_record expected_unsubscribe2 = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe2)); + + // release once, verify no unsubscribes + struct aws_rr_release_subscription_options release1_options = { + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 1, + }; + aws_rr_subscription_manager_release_subscription(manager, &release1_options); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe1)); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe2)); + + // release second + struct aws_rr_release_subscription_options release2_options = { + .topic_filters = multi_subs, + .topic_filter_count = AWS_ARRAY_SIZE(multi_subs), + .operation_id = 2, + }; + aws_rr_subscription_manager_release_subscription(manager, &release2_options); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe1)); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe2)); + + aws_rr_subscription_manager_purge_unused(manager); + + // now the unsubscribes should be present + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe1)); + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe2)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_release_multi_unsubscribes_request, s_rrsm_release_multi_unsubscribes_request_fn) + +/* + * Verify: A streaming subscription that resolves successfully invokes callbacks for every operation listener; releasing + * both references and calling a new acquire will trigger an unsubscribe of the first subscription + */ +static int s_rrsm_release_unsubscribes_streaming_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + + struct aws_protocol_adapter_subscription_event successful_subscription_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription_event); + + // verify two success callbacks + struct aws_subscription_status_record expected_subscription_events[] = { + { + .type = ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }, + { + .type = ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 2, + }}; + ASSERT_TRUE(s_contains_subscription_event_records(&fixture, 2, expected_subscription_events)); + + // verify no unsubscribes + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // release once, verify no unsubscribe + struct aws_rr_release_subscription_options release1_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + aws_rr_subscription_manager_release_subscription(manager, &release1_options); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // release second + struct aws_rr_release_subscription_options release2_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + aws_rr_subscription_manager_release_subscription(manager, &release2_options); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + aws_rr_subscription_manager_purge_unused(manager); + + // now the unsubscribe should be present + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_release_unsubscribes_streaming, s_rrsm_release_unsubscribes_streaming_fn) + +static int s_rrsm_do_unsubscribe_test(struct aws_allocator *allocator, bool should_succeed) { + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .operation_timeout_seconds = DEFAULT_SM_TEST_TIMEOUT, + .start_connected = true, + }; + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + + // budget of 2, so new acquires should be blocked + struct aws_rr_acquire_subscription_options acquire3_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world3_cursor, + .topic_filter_count = 1, + .operation_id = 3, + }; + ASSERT_INT_EQUALS(AASRT_BLOCKED, aws_rr_subscription_manager_acquire_subscription(manager, &acquire3_options)); + + // complete the subscribe + struct aws_protocol_adapter_subscription_event successful_subscription_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription_event); + + // still blocked + ASSERT_INT_EQUALS(AASRT_BLOCKED, aws_rr_subscription_manager_acquire_subscription(manager, &acquire3_options)); + + // release + struct aws_rr_release_subscription_options release1_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + aws_rr_subscription_manager_release_subscription(manager, &release1_options); + + // unsubscribe should be visible, but we're still blocked because it hasn't completed + aws_rr_subscription_manager_purge_unused(manager); + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // complete the unsubscribe + struct aws_protocol_adapter_subscription_event successful_unsubscribe_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_UNSUBSCRIBE, + .error_code = should_succeed ? AWS_ERROR_SUCCESS : AWS_ERROR_MQTT5_UNSUBSCRIBE_OPTIONS_VALIDATION, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_unsubscribe_event); + + aws_rr_subscription_manager_purge_unused(manager); + + // a successful unsubscribe should clear space, a failed one should not + ASSERT_INT_EQUALS( + (should_succeed ? AASRT_SUBSCRIBING : AASRT_BLOCKED), + aws_rr_subscription_manager_acquire_subscription(manager, &acquire3_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +/* + * Verify: Releasing a subscription and successfully unsubscribing frees up space for new acquires + */ +static int s_rrsm_release_unsubscribe_success_clears_space_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_rrsm_do_unsubscribe_test(allocator, true); +} + +AWS_TEST_CASE(rrsm_release_unsubscribe_success_clears_space, s_rrsm_release_unsubscribe_success_clears_space_fn) + +/* + * Verify: Releasing a subscription but failing to unsubscribe does not free up space for new acquires + */ +static int s_rrsm_release_unsubscribe_failure_blocked_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_rrsm_do_unsubscribe_test(allocator, false); +} + +AWS_TEST_CASE(rrsm_release_unsubscribe_failure_blocked, s_rrsm_release_unsubscribe_failure_blocked_fn) + +static int s_aws_mqtt_protocol_adapter_mock_subscribe_fails_first_time( + void *impl, + struct aws_protocol_adapter_subscribe_options *options) { + (void)impl; + (void)options; + + struct aws_mqtt_protocol_adapter_mock_impl *mock_impl = impl; + + if (mock_impl->subscribe_count++ == 0) { + return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); + } else { + return AWS_OP_SUCCESS; + } +} + +/* + * Verify: Acquiring a new request subscription but synchronously failing the protocol adapter subscribe returns FAILURE + */ +static int s_rrsm_acquire_failure_subscribe_sync_failure_request_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_protocol_adapter_vtable failing_vtable = s_protocol_adapter_mock_vtable; + failing_vtable.aws_mqtt_protocol_adapter_subscribe_fn = s_aws_mqtt_protocol_adapter_mock_subscribe_fails_first_time; + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = DEFAULT_SM_TEST_TIMEOUT, + .start_connected = true, + .adapter_vtable = &failing_vtable, + }; + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_FAILURE, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrsm_acquire_failure_subscribe_sync_failure_request, + s_rrsm_acquire_failure_subscribe_sync_failure_request_fn) + +/* + * Verify: Acquiring a new streaming subscription but synchronously failing the protocol adapter subscribe returns + * FAILURE Trying again also fails even though the mock subscribe succeeds after the first try. + */ +static int s_rrsm_acquire_failure_subscribe_sync_failure_streaming_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_mqtt_protocol_adapter_vtable failing_vtable = s_protocol_adapter_mock_vtable; + failing_vtable.aws_mqtt_protocol_adapter_subscribe_fn = s_aws_mqtt_protocol_adapter_mock_subscribe_fails_first_time; + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = DEFAULT_SM_TEST_TIMEOUT, + .start_connected = true, + .adapter_vtable = &failing_vtable, + }; + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_FAILURE, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + struct aws_rr_acquire_subscription_options acquire_options2 = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_FAILURE, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options2)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrsm_acquire_failure_subscribe_sync_failure_streaming, + s_rrsm_acquire_failure_subscribe_sync_failure_streaming_fn) + +/* + * Verify: Completing a request subscription-acquire with a failing reason code emits a subscription failed event + */ +static int s_rrsm_acquire_request_subscribe_failure_event_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + + // complete the subscribe with a failure + struct aws_protocol_adapter_subscription_event failed_subscription_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &failed_subscription_event); + + // verify subscribe failure event emission + struct aws_subscription_status_record expected_subscription_event = { + .type = ARRSET_REQUEST_SUBSCRIBE_FAILURE, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }; + + ASSERT_TRUE(s_contains_subscription_event_record(&fixture, &expected_subscription_event)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(rrsm_acquire_request_subscribe_failure_event, s_rrsm_acquire_request_subscribe_failure_event_fn) + +/* + * Verify: Completing a streaming subscription-acquire with a retryable failing failure triggers a resubscribe attempt + */ +static int s_rrsm_acquire_streaming_subscribe_failure_retryable_resubscribe_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + + // complete the subscribe with a retryable failure + struct aws_protocol_adapter_subscription_event failed_subscription_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE, + .retryable = true, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &failed_subscription_event); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + ASSERT_TRUE( + s_api_records_equals(fixture.mock_protocol_adapter, AWS_ARRAY_SIZE(expected_subscribes), expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrsm_acquire_streaming_subscribe_failure_retryable_resubscribe, + s_rrsm_acquire_streaming_subscribe_failure_retryable_resubscribe_fn) + +static enum aws_rr_subscription_event_type s_compute_expected_subscription_event_offline_acquire_online( + enum aws_rr_subscription_type subscription_type, + bool success) { + if (subscription_type == ARRST_REQUEST_RESPONSE) { + if (success) { + return ARRSET_REQUEST_SUBSCRIBE_SUCCESS; + } else { + return ARRSET_REQUEST_SUBSCRIBE_FAILURE; + } + } else { + if (success) { + return ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED; + } else { + return ARRSET_STREAMING_SUBSCRIPTION_HALTED; + } + } +} + +static int s_do_offline_acquire_online_test( + struct aws_allocator *allocator, + enum aws_rr_subscription_type subscription_type, + bool success) { + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture_options fixture_config = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = DEFAULT_SM_TEST_TIMEOUT, + .start_connected = false, + }; + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, &fixture_config)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire_options = { + .type = subscription_type, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + // nothing should happen while offline + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 0, NULL)); + + // go online, should trigger a subscribe + struct aws_protocol_adapter_connection_event online_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = false, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(&fixture.subscription_manager, &online_event); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 1, expected_subscribes)); + + // complete the subscribe, verify a subscription success/failure event + struct aws_protocol_adapter_subscription_event subscription_event = { + .event_type = AWS_PASET_SUBSCRIBE, + .topic_filter = s_hello_world1_cursor, + .error_code = success ? AWS_ERROR_SUCCESS : AWS_ERROR_MQTT_PROTOCOL_ADAPTER_FAILING_REASON_CODE, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &subscription_event); + + struct aws_subscription_status_record expected_subscription_event = { + .type = s_compute_expected_subscription_event_offline_acquire_online(subscription_type, success), + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }; + ASSERT_TRUE(s_contains_subscription_event_record(&fixture, &expected_subscription_event)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +/* + * Verify: Acquiring a new request subscription while offline returns SUBSCRIBING. Going online triggers a protocol + * adapter subscribe. Completing the subscription successfully emits a request subscribe success event. + */ +static int s_rrsm_offline_acquire_request_online_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_offline_acquire_online_test(allocator, ARRST_REQUEST_RESPONSE, true); +} + +AWS_TEST_CASE(rrsm_offline_acquire_request_online_success, s_rrsm_offline_acquire_request_online_success_fn) + +/* + * Verify: Acquiring a new request subscription while offline returns SUBSCRIBING. Going online triggers a protocol + * adapter subscribe. Completing the subscription with a failure emits a request subscribe failure event. + */ +static int s_rrsm_offline_acquire_request_online_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_offline_acquire_online_test(allocator, ARRST_REQUEST_RESPONSE, false); +} + +AWS_TEST_CASE(rrsm_offline_acquire_request_online_failure, s_rrsm_offline_acquire_request_online_failure_fn) + +/* + * Verify: Acquiring a new steaming subscription while offline returns SUBSCRIBING. Going online triggers a protocol + * adapter subscribe. Completing the subscription successfully emits a streaming subscription established event. + */ +static int s_rrsm_offline_acquire_streaming_online_success_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_offline_acquire_online_test(allocator, ARRST_EVENT_STREAM, true); +} + +AWS_TEST_CASE(rrsm_offline_acquire_streaming_online_success, s_rrsm_offline_acquire_streaming_online_success_fn) + +/* + * Verify: Acquiring a new request subscription while offline returns SUBSCRIBING. Going online triggers a protocol + * adapter subscribe. Completing the subscription with a failure emits a streaming subscription halted event. + */ +static int s_rrsm_offline_acquire_streaming_online_failure_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_offline_acquire_online_test(allocator, ARRST_EVENT_STREAM, false); +} + +AWS_TEST_CASE(rrsm_offline_acquire_streaming_online_failure, s_rrsm_offline_acquire_streaming_online_failure_fn) + +static int s_do_offline_acquire_release_online_test( + struct aws_allocator *allocator, + enum aws_rr_subscription_type subscription_type) { + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + // trigger online -> offline + struct aws_protocol_adapter_connection_event offline_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &offline_event); + + // acquire + struct aws_rr_acquire_subscription_options acquire_options = { + .type = subscription_type, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + // nothing should happen while offline + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 0, NULL)); + + // release while offline, nothing should happen + struct aws_rr_release_subscription_options release_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + aws_rr_subscription_manager_release_subscription(manager, &release_options); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 0, NULL)); + + // go online, nothing should be sent to the protocol adapter + struct aws_protocol_adapter_connection_event online_event = { + .event_type = AWS_PACET_CONNECTED, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &online_event); + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 0, NULL)); + + // trigger a different subscription, verify it's the only thing that has reached the protocol adapter + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = subscription_type, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world2_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + ASSERT_TRUE(s_api_records_equals(fixture.mock_protocol_adapter, 1, expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +/* + * Verify: Acquiring and releasing a subscription while offline and then going online should remove the + * subscription without invoking any protocol adapter APIs. + */ +static int s_rrsm_offline_acquire_release_request_online_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_offline_acquire_release_online_test(allocator, ARRST_REQUEST_RESPONSE); +} + +AWS_TEST_CASE(rrsm_offline_acquire_release_request_online, s_rrsm_offline_acquire_release_request_online_fn) + +/* + * Verify: Acquiring and releasing a subscription while offline and then going online should remove the + * subscription without invoking any protocol adapter APIs. + */ +static int s_rrsm_offline_acquire_release_streaming_online_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_offline_acquire_release_online_test(allocator, ARRST_EVENT_STREAM); +} + +AWS_TEST_CASE(rrsm_offline_acquire_release_streaming_online, s_rrsm_offline_acquire_release_streaming_online_fn) + +static int s_do_acquire_success_offline_release_acquire2_no_unsubscribe_test( + struct aws_allocator *allocator, + enum aws_rr_subscription_type subscription_type) { + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + // acquire + struct aws_rr_acquire_subscription_options acquire_options = { + .type = subscription_type, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + // successfully complete subscription + struct aws_protocol_adapter_subscription_event subscription_event = { + .event_type = AWS_PASET_SUBSCRIBE, + .topic_filter = s_hello_world1_cursor, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &subscription_event); + + struct aws_subscription_status_record expected_subscription_event = { + .type = subscription_type == ARRST_REQUEST_RESPONSE ? ARRSET_REQUEST_SUBSCRIBE_SUCCESS + : ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }; + ASSERT_TRUE(s_contains_subscription_event_record(&fixture, &expected_subscription_event)); + + // trigger online -> offline + struct aws_protocol_adapter_connection_event offline_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &offline_event); + + // release + struct aws_rr_release_subscription_options release_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + aws_rr_subscription_manager_release_subscription(manager, &release_options); + + // acquire something different, normally that triggers an unsubscribe, but we're offline + struct aws_rr_acquire_subscription_options acquire2_options = { + .type = subscription_type, + .topic_filters = &s_hello_world2_cursor, + .topic_filter_count = 1, + .operation_id = 2, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire2_options)); + + // verify no unsubscribe has been sent + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // online, verify unsubscribe + struct aws_protocol_adapter_connection_event online_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = true, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &online_event); + + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +/* + * Verify: Releasing an active request subscription while offline should not invoke an unsubscribe until back online + */ +static int s_rrsm_acquire_request_success_offline_release_acquire2_no_unsubscribe_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_do_acquire_success_offline_release_acquire2_no_unsubscribe_test(allocator, ARRST_REQUEST_RESPONSE); +} + +AWS_TEST_CASE( + rrsm_acquire_request_success_offline_release_acquire2_no_unsubscribe, + s_rrsm_acquire_request_success_offline_release_acquire2_no_unsubscribe_fn) + +/* + * Verify: Releasing an active streaming subscription while offline should not invoke an unsubscribe until back online + */ +static int s_rrsm_acquire_streaming_success_offline_release_acquire2_no_unsubscribe_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_do_acquire_success_offline_release_acquire2_no_unsubscribe_test(allocator, ARRST_REQUEST_RESPONSE); +} + +AWS_TEST_CASE( + rrsm_acquire_streaming_success_offline_release_acquire2_no_unsubscribe, + s_rrsm_acquire_streaming_success_offline_release_acquire2_no_unsubscribe_fn) + +static int s_do_rrsm_acquire_clean_up_test( + struct aws_allocator *allocator, + enum aws_rr_subscription_type subscription_type, + bool complete_subscribe, + bool clean_up_while_connected) { + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + // acquire + struct aws_rr_acquire_subscription_options acquire_options = { + .type = subscription_type, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + // successfully complete subscription if desired + if (complete_subscribe) { + struct aws_protocol_adapter_subscription_event subscription_event = { + .event_type = AWS_PASET_SUBSCRIBE, + .topic_filter = s_hello_world1_cursor, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &subscription_event); + + struct aws_subscription_status_record expected_subscription_event = { + .type = subscription_type == ARRST_REQUEST_RESPONSE ? ARRSET_REQUEST_SUBSCRIBE_SUCCESS + : ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }; + ASSERT_TRUE(s_contains_subscription_event_record(&fixture, &expected_subscription_event)); + } + + if (!clean_up_while_connected) { + // trigger online -> offline + struct aws_protocol_adapter_connection_event offline_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &offline_event); + } + + // clean up subscription manager + aws_rr_subscription_manager_clean_up(manager); + + // verify an unsubscribe was sent even though we are offline + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // prevent the fixture cleanup from double freeing the subscription manager that was already cleaned up by + // reinitializing the subscription manager + struct aws_rr_subscription_manager_options subscription_manager_options = { + .max_request_response_subscriptions = 2, + .max_streaming_subscriptions = 2, + .operation_timeout_seconds = 5, + .subscription_status_callback = s_aws_rr_subscription_status_event_test_callback_fn, + .userdata = &fixture, + }; + aws_rr_subscription_manager_init( + &fixture.subscription_manager, allocator, fixture.mock_protocol_adapter, &subscription_manager_options); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +/* + * Verify: Calling clean up while a request subscription is active triggers an immediate unsubscribe + */ +static int s_rrsm_acquire_request_success_clean_up_unsubscribe_override_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_rrsm_acquire_clean_up_test(allocator, ARRST_REQUEST_RESPONSE, true, true); +} + +AWS_TEST_CASE( + rrsm_acquire_request_success_clean_up_unsubscribe_override, + s_rrsm_acquire_request_success_clean_up_unsubscribe_override_fn) + +/* + * Verify: Calling clean up while a streaming subscription is active triggers an immediate unsubscribe + */ +static int s_rrsm_acquire_streaming_success_clean_up_unsubscribe_override_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_do_rrsm_acquire_clean_up_test(allocator, ARRST_EVENT_STREAM, true, true); +} + +AWS_TEST_CASE( + rrsm_acquire_streaming_success_clean_up_unsubscribe_override, + s_rrsm_acquire_streaming_success_clean_up_unsubscribe_override_fn) + +/* + * Verify: Calling clean up while a request subscription is pending triggers an immediate unsubscribe + */ +static int s_rrsm_acquire_request_pending_clean_up_unsubscribe_override_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_rrsm_acquire_clean_up_test(allocator, ARRST_REQUEST_RESPONSE, false, true); +} + +AWS_TEST_CASE( + rrsm_acquire_request_pending_clean_up_unsubscribe_override, + s_rrsm_acquire_request_pending_clean_up_unsubscribe_override_fn) + +/* + * Verify: Calling clean up while a streaming subscription is pending triggers an immediate unsubscribe + */ +static int s_rrsm_acquire_streaming_pending_clean_up_unsubscribe_override_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_do_rrsm_acquire_clean_up_test(allocator, ARRST_EVENT_STREAM, false, true); +} + +AWS_TEST_CASE( + rrsm_acquire_streaming_pending_clean_up_unsubscribe_override, + s_rrsm_acquire_streaming_pending_clean_up_unsubscribe_override_fn) + +/* + * Verify: Calling clean up while offline and a request subscription is pending triggers an immediate unsubscribe + */ +static int s_rrsm_offline_acquire_request_pending_clean_up_unsubscribe_override_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_do_rrsm_acquire_clean_up_test(allocator, ARRST_REQUEST_RESPONSE, false, false); +} + +AWS_TEST_CASE( + rrsm_offline_acquire_request_pending_clean_up_unsubscribe_override, + s_rrsm_offline_acquire_request_pending_clean_up_unsubscribe_override_fn) + +/* + * Verify: Calling clean up while offline and a streaming subscription is pending triggers an immediate unsubscribe + */ +static int s_rrsm_offline_acquire_streaming_pending_clean_up_unsubscribe_override_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_do_rrsm_acquire_clean_up_test(allocator, ARRST_EVENT_STREAM, false, false); +} + +AWS_TEST_CASE( + rrsm_offline_acquire_streaming_pending_clean_up_unsubscribe_override, + s_rrsm_offline_acquire_streaming_pending_clean_up_unsubscribe_override_fn) + +static int s_rrsm_do_no_session_subscription_ended_test( + struct aws_allocator *allocator, + bool offline_while_unsubscribing) { + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + // acquire + struct aws_rr_acquire_subscription_options acquire_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + // successfully complete subscription + struct aws_protocol_adapter_subscription_event subscription_event = { + .event_type = AWS_PASET_SUBSCRIBE, + .topic_filter = s_hello_world1_cursor, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &subscription_event); + + struct aws_subscription_status_record expected_subscription_event = { + .type = ARRSET_REQUEST_SUBSCRIBE_SUCCESS, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }; + ASSERT_TRUE(s_contains_subscription_event_record(&fixture, &expected_subscription_event)); + + // release if appropriate + if (offline_while_unsubscribing) { + struct aws_rr_release_subscription_options release_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + aws_rr_subscription_manager_release_subscription(manager, &release_options); + + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // trigger an event that would cull unused subscriptions, causing an unsubscribe + aws_rr_subscription_manager_purge_unused(manager); + + // now the unsubscribe should be present + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + } + + // online -> offline + struct aws_protocol_adapter_connection_event offline_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &offline_event); + + // offline -> online (no session) + struct aws_protocol_adapter_connection_event online_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = false, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &online_event); + + // verify subscription lost emitted + if (!offline_while_unsubscribing) { + struct aws_subscription_status_record expected_subscription_ended_event = { + .type = ARRSET_REQUEST_SUBSCRIPTION_ENDED, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }; + ASSERT_TRUE(s_contains_subscription_event_record(&fixture, &expected_subscription_ended_event)); + } + + // if we were unsubscribing, verify reacquire is blocked and then complete the unsubscribe + struct aws_rr_acquire_subscription_options reacquire_options = { + .type = ARRST_REQUEST_RESPONSE, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 3, + }; + + if (offline_while_unsubscribing) { + ASSERT_INT_EQUALS(AASRT_BLOCKED, aws_rr_subscription_manager_acquire_subscription(manager, &reacquire_options)); + + struct aws_protocol_adapter_subscription_event unsubscribe_event = { + .event_type = AWS_PASET_UNSUBSCRIBE, + .topic_filter = s_hello_world1_cursor, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &unsubscribe_event); + } + + // verify we can reacquire + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &reacquire_options)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +/* + * Verify: If the client fails to rejoin a session, a SUBSCRIPTION_ENDED event is emitted for active request + * subscriptions and that subscription can successfully be reacquired + */ +static int s_rrsm_acquire_request_success_offline_online_no_session_subscription_ended_can_reacquire_fn( + struct aws_allocator *allocator, + void *ctx) { + (void)ctx; + + return s_rrsm_do_no_session_subscription_ended_test(allocator, false); +} + +AWS_TEST_CASE( + rrsm_acquire_request_success_offline_online_no_session_subscription_ended_can_reacquire, + s_rrsm_acquire_request_success_offline_online_no_session_subscription_ended_can_reacquire_fn) + +/* + * Verify: If the client fails to rejoin a session, a SUBSCRIPTION_ENDED event is emitted for unsubscribing + * request subscriptions + */ +static int s_rrsm_request_subscription_ended_while_unsubscribing_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_rrsm_do_no_session_subscription_ended_test(allocator, true); +} + +AWS_TEST_CASE( + rrsm_request_subscription_ended_while_unsubscribing, + s_rrsm_request_subscription_ended_while_unsubscribing_fn) + +/* + * Verify: If the client fails to rejoin a session, a SUBSCRIPTION_LOST event is emitted for streaming subscriptions, + * and a resubscribe is triggered + */ +static int s_rrsm_streaming_subscription_lost_resubscribe_on_no_session_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + // acquire + struct aws_rr_acquire_subscription_options acquire_options = { + .type = ARRST_EVENT_STREAM, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire_options)); + + // successfully complete subscription + struct aws_protocol_adapter_subscription_event subscription_event = { + .event_type = AWS_PASET_SUBSCRIBE, + .topic_filter = s_hello_world1_cursor, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &subscription_event); + + struct aws_subscription_status_record expected_subscription_event = { + .type = ARRSET_STREAMING_SUBSCRIPTION_ESTABLISHED, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }; + ASSERT_TRUE(s_contains_subscription_event_record(&fixture, &expected_subscription_event)); + + // online -> offline + struct aws_protocol_adapter_connection_event offline_event = { + .event_type = AWS_PACET_DISCONNECTED, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &offline_event); + + // offline -> online (no session) + struct aws_protocol_adapter_connection_event online_event = { + .event_type = AWS_PACET_CONNECTED, + .joined_session = false, + }; + aws_rr_subscription_manager_on_protocol_adapter_connection_event(manager, &online_event); + + // verify subscription lost on rejoin + struct aws_subscription_status_record expected_subscription_ended_event = { + .type = ARRSET_STREAMING_SUBSCRIPTION_LOST, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 1, + }; + ASSERT_TRUE(s_contains_subscription_event_record(&fixture, &expected_subscription_ended_event)); + + // verify resubscribe submitted to the protocol adapter + struct aws_protocol_adapter_api_record expected_subscribes[] = { + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + { + .type = PAAT_SUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }, + }; + + ASSERT_TRUE( + s_api_records_equals(fixture.mock_protocol_adapter, AWS_ARRAY_SIZE(expected_subscribes), expected_subscribes)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + rrsm_streaming_subscription_lost_resubscribe_on_no_session, + s_rrsm_streaming_subscription_lost_resubscribe_on_no_session_fn) + +static int s_do_purge_test(struct aws_allocator *allocator, enum aws_rr_subscription_type subscription_type) { + aws_mqtt_library_init(allocator); + + struct aws_subscription_manager_test_fixture fixture; + ASSERT_SUCCESS(s_aws_subscription_manager_test_fixture_init(&fixture, allocator, NULL)); + + struct aws_rr_subscription_manager *manager = &fixture.subscription_manager; + + struct aws_rr_acquire_subscription_options acquire1_options = { + .type = subscription_type, + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + ASSERT_INT_EQUALS(AASRT_SUBSCRIBING, aws_rr_subscription_manager_acquire_subscription(manager, &acquire1_options)); + + struct aws_protocol_adapter_subscription_event successful_subscription_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_SUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_subscription_event); + + struct aws_subscription_status_record expected_empty_subscription_events[] = { + { + .type = ARRSET_SUBSCRIPTION_EMPTY, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 0, + }, + }; + + struct aws_subscription_status_record expected_unsubscribe_events[] = { + { + .type = ARRSET_UNSUBSCRIBE_COMPLETE, + .topic_filter_cursor = s_hello_world1_cursor, + .operation_id = 0, + }, + }; + + ASSERT_FALSE(s_contains_subscription_event_records(&fixture, 1, expected_empty_subscription_events)); + ASSERT_FALSE(s_contains_subscription_event_records(&fixture, 1, expected_unsubscribe_events)); + + // verify no unsubscribes + struct aws_protocol_adapter_api_record expected_unsubscribe = { + .type = PAAT_UNSUBSCRIBE, + .topic_filter_cursor = s_hello_world1_cursor, + .timeout = DEFAULT_SM_TEST_TIMEOUT, + }; + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // release once, verify no unsubscribe + struct aws_rr_release_subscription_options release1_options = { + .topic_filters = &s_hello_world1_cursor, + .topic_filter_count = 1, + .operation_id = 1, + }; + aws_rr_subscription_manager_release_subscription(manager, &release1_options); + ASSERT_FALSE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // verify empty event, but no unsubscribe event yet + ASSERT_TRUE(s_contains_subscription_event_records(&fixture, 1, expected_empty_subscription_events)); + ASSERT_FALSE(s_contains_subscription_event_records(&fixture, 1, expected_unsubscribe_events)); + + // purge + aws_rr_subscription_manager_purge_unused(manager); + + // now the unsubscribe should be present + ASSERT_TRUE(s_api_records_contains_record(fixture.mock_protocol_adapter, &expected_unsubscribe)); + + // complete the unsubscribe + struct aws_protocol_adapter_subscription_event successful_unsubscribe_event = { + .topic_filter = s_hello_world1_cursor, + .event_type = AWS_PASET_UNSUBSCRIBE, + .error_code = AWS_ERROR_SUCCESS, + }; + aws_rr_subscription_manager_on_protocol_adapter_subscription_event(manager, &successful_unsubscribe_event); + + // verify unsubscribe attempt emission + ASSERT_TRUE(s_contains_subscription_event_records(&fixture, 1, expected_unsubscribe_events)); + + s_aws_subscription_manager_test_fixture_clean_up(&fixture); + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_rrsm_request_subscription_purge_events_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_purge_test(allocator, ARRST_REQUEST_RESPONSE); +} + +AWS_TEST_CASE(rrsm_request_subscription_purge_events, s_rrsm_request_subscription_purge_events_fn) + +static int s_rrsm_streaming_subscription_purge_events_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_purge_test(allocator, ARRST_EVENT_STREAM); +} + +AWS_TEST_CASE(rrsm_streaming_subscription_purge_events, s_rrsm_streaming_subscription_purge_events_fn) diff --git a/tests/v3/connection_state_test.c b/tests/v3/connection_state_test.c index 4a64b210..d895b44a 100644 --- a/tests/v3/connection_state_test.c +++ b/tests/v3/connection_state_test.c @@ -3,661 +3,23 @@ * SPDX-License-Identifier: Apache-2.0. */ -#include "mqtt_mock_server_handler.h" - -#include +#include -#include -#include -#include +#include "mqtt311_testing_utils.h" +#include "mqtt_mock_server_handler.h" #include -#include - +#include #include -#include - -static const int TEST_LOG_SUBJECT = 60000; -static const int ONE_SEC = 1000000000; -// The value is extract from aws-c-mqtt/source/client.c -static const int AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS = 10; -static const uint64_t RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS = 500000000; -#define DEFAULT_MIN_RECONNECT_DELAY_SECONDS 1 - -#define DEFAULT_TEST_PING_TIMEOUT_MS 1000 -#define DEFAULT_TEST_KEEP_ALIVE_S 2 - -struct received_publish_packet { - struct aws_byte_buf topic; - struct aws_byte_buf payload; - bool dup; - enum aws_mqtt_qos qos; - bool retain; -}; - -struct mqtt_connection_state_test { - struct aws_allocator *allocator; - struct aws_channel *server_channel; - struct aws_channel_handler *mock_server; - struct aws_client_bootstrap *client_bootstrap; - struct aws_server_bootstrap *server_bootstrap; - struct aws_event_loop_group *el_group; - struct aws_host_resolver *host_resolver; - struct aws_socket_endpoint endpoint; - struct aws_socket *listener; - struct aws_mqtt_client *mqtt_client; - struct aws_mqtt_client_connection *mqtt_connection; - struct aws_socket_options socket_options; - - bool session_present; - bool connection_completed; - bool connection_success; - bool connection_failure; - bool client_disconnect_completed; - bool server_disconnect_completed; - bool connection_interrupted; - bool connection_resumed; - bool subscribe_completed; - bool listener_destroyed; - bool connection_terminated; - int interruption_error; - int subscribe_complete_error; - int op_complete_error; - enum aws_mqtt_connect_return_code mqtt_return_code; - int error; - struct aws_condition_variable cvar; - struct aws_mutex lock; - /* any published messages from mock server, that you may not subscribe to. (Which should not happen in real life) */ - struct aws_array_list any_published_messages; /* list of struct received_publish_packet */ - size_t any_publishes_received; - size_t expected_any_publishes; - /* the published messages from mock server, that you did subscribe to. */ - struct aws_array_list published_messages; /* list of struct received_publish_packet */ - size_t publishes_received; - size_t expected_publishes; - /* The returned QoS from mock server */ - struct aws_array_list qos_returned; /* list of uint_8 */ - size_t ops_completed; - size_t expected_ops_completed; - size_t connection_close_calls; /* All of the times on_connection_closed has been called */ - - size_t connection_termination_calls; /* How many times on_connection_termination has been called, should be 1 */ -}; - static struct mqtt_connection_state_test test_data = {0}; -static void s_on_any_publish_received( - struct aws_mqtt_client_connection *connection, - const struct aws_byte_cursor *topic, - const struct aws_byte_cursor *payload, - bool dup, - enum aws_mqtt_qos qos, - bool retain, - void *userdata); - -static void s_on_incoming_channel_setup_fn( - struct aws_server_bootstrap *bootstrap, - int error_code, - struct aws_channel *channel, - void *user_data) { - (void)bootstrap; - struct mqtt_connection_state_test *state_test_data = user_data; - - state_test_data->error = error_code; - - if (!error_code) { - aws_mutex_lock(&state_test_data->lock); - state_test_data->server_disconnect_completed = false; - aws_mutex_unlock(&state_test_data->lock); - AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "server channel setup completed"); - - state_test_data->server_channel = channel; - struct aws_channel_slot *test_handler_slot = aws_channel_slot_new(channel); - aws_channel_slot_insert_end(channel, test_handler_slot); - mqtt_mock_server_handler_update_slot(state_test_data->mock_server, test_handler_slot); - aws_channel_slot_set_handler(test_handler_slot, state_test_data->mock_server); - } -} - -static void s_on_incoming_channel_shutdown_fn( - struct aws_server_bootstrap *bootstrap, - int error_code, - struct aws_channel *channel, - void *user_data) { - (void)bootstrap; - (void)error_code; - (void)channel; - struct mqtt_connection_state_test *state_test_data = user_data; - aws_mutex_lock(&state_test_data->lock); - state_test_data->server_disconnect_completed = true; - AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "server channel shutdown completed"); - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static void s_on_listener_destroy(struct aws_server_bootstrap *bootstrap, void *user_data) { - (void)bootstrap; - struct mqtt_connection_state_test *state_test_data = user_data; - aws_mutex_lock(&state_test_data->lock); - state_test_data->listener_destroyed = true; - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_listener_destroyed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->listener_destroyed; -} - -static void s_wait_on_listener_cleanup(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_listener_destroyed, state_test_data); - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_on_connection_interrupted(struct aws_mqtt_client_connection *connection, int error_code, void *userdata) { - (void)connection; - (void)error_code; - struct mqtt_connection_state_test *state_test_data = userdata; - - aws_mutex_lock(&state_test_data->lock); - state_test_data->connection_interrupted = true; - state_test_data->interruption_error = error_code; - aws_mutex_unlock(&state_test_data->lock); - AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "connection interrupted"); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_connection_interrupted(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->connection_interrupted; -} - -static void s_wait_for_interrupt_to_complete(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_connection_interrupted, state_test_data); - state_test_data->connection_interrupted = false; - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_on_connection_resumed( - struct aws_mqtt_client_connection *connection, - enum aws_mqtt_connect_return_code return_code, - bool session_present, - void *userdata) { - (void)connection; - (void)return_code; - (void)session_present; - AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "reconnect completed"); - - struct mqtt_connection_state_test *state_test_data = userdata; - - aws_mutex_lock(&state_test_data->lock); - state_test_data->connection_resumed = true; - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_connection_resumed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->connection_resumed; -} - -static void s_wait_for_reconnect_to_complete(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_connection_resumed, state_test_data); - state_test_data->connection_resumed = false; - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_on_connection_success( - struct aws_mqtt_client_connection *connection, - enum aws_mqtt_connect_return_code return_code, - bool session_present, - void *userdata) { - (void)connection; - struct mqtt_connection_state_test *state_test_data = userdata; - aws_mutex_lock(&state_test_data->lock); - - state_test_data->session_present = session_present; - state_test_data->mqtt_return_code = return_code; - state_test_data->connection_success = true; - aws_mutex_unlock(&state_test_data->lock); - - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static void s_on_connection_failure(struct aws_mqtt_client_connection *connection, int error_code, void *userdata) { - (void)connection; - struct mqtt_connection_state_test *state_test_data = userdata; - aws_mutex_lock(&state_test_data->lock); - - state_test_data->error = error_code; - state_test_data->connection_failure = true; - aws_mutex_unlock(&state_test_data->lock); - - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_connection_succeed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->connection_success; -} - -static bool s_is_connection_failed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->connection_failure; -} - -static void s_wait_for_connection_to_succeed(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_connection_succeed, state_test_data); - state_test_data->connection_success = false; - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_wait_for_connection_to_fail(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_connection_failed, state_test_data); - state_test_data->connection_failure = false; - aws_mutex_unlock(&state_test_data->lock); -} - -static bool s_is_termination_completed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->connection_terminated; -} - -static void s_wait_for_termination_to_complete(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_termination_completed, state_test_data); - state_test_data->connection_terminated = false; - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_on_connection_termination_fn(void *userdata) { - struct mqtt_connection_state_test *state_test_data = (struct mqtt_connection_state_test *)userdata; - - aws_mutex_lock(&state_test_data->lock); - state_test_data->connection_termination_calls += 1; - state_test_data->connection_terminated = true; - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -/** sets up a unix domain socket server and socket options. Creates an mqtt connection configured to use - * the domain socket. - */ static int s_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx) { - aws_mqtt_library_init(allocator); - - struct mqtt_connection_state_test *state_test_data = ctx; - - AWS_ZERO_STRUCT(*state_test_data); - - state_test_data->allocator = allocator; - state_test_data->el_group = aws_event_loop_group_new_default(allocator, 1, NULL); - - state_test_data->mock_server = new_mqtt_mock_server(allocator); - ASSERT_NOT_NULL(state_test_data->mock_server); - - state_test_data->server_bootstrap = aws_server_bootstrap_new(allocator, state_test_data->el_group); - ASSERT_NOT_NULL(state_test_data->server_bootstrap); - - struct aws_socket_options socket_options = { - .connect_timeout_ms = 100, - .domain = AWS_SOCKET_LOCAL, - }; - - state_test_data->socket_options = socket_options; - ASSERT_SUCCESS(aws_condition_variable_init(&state_test_data->cvar)); - ASSERT_SUCCESS(aws_mutex_init(&state_test_data->lock)); - - aws_socket_endpoint_init_local_address_for_test(&state_test_data->endpoint); - - struct aws_server_socket_channel_bootstrap_options server_bootstrap_options = { - .bootstrap = state_test_data->server_bootstrap, - .host_name = state_test_data->endpoint.address, - .port = state_test_data->endpoint.port, - .socket_options = &state_test_data->socket_options, - .incoming_callback = s_on_incoming_channel_setup_fn, - .shutdown_callback = s_on_incoming_channel_shutdown_fn, - .destroy_callback = s_on_listener_destroy, - .user_data = state_test_data, - }; - state_test_data->listener = aws_server_bootstrap_new_socket_listener(&server_bootstrap_options); - - ASSERT_NOT_NULL(state_test_data->listener); - - struct aws_host_resolver_default_options resolver_options = { - .el_group = state_test_data->el_group, - .max_entries = 1, - }; - state_test_data->host_resolver = aws_host_resolver_new_default(allocator, &resolver_options); - - struct aws_client_bootstrap_options bootstrap_options = { - .event_loop_group = state_test_data->el_group, - .user_data = state_test_data, - .host_resolver = state_test_data->host_resolver, - }; - - state_test_data->client_bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); - - state_test_data->mqtt_client = aws_mqtt_client_new(allocator, state_test_data->client_bootstrap); - state_test_data->mqtt_connection = aws_mqtt_client_connection_new(state_test_data->mqtt_client); - ASSERT_NOT_NULL(state_test_data->mqtt_connection); - - ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_interruption_handlers( - state_test_data->mqtt_connection, - s_on_connection_interrupted, - state_test_data, - s_on_connection_resumed, - state_test_data)); - - ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_result_handlers( - state_test_data->mqtt_connection, - s_on_connection_success, - state_test_data, - s_on_connection_failure, - state_test_data)); - - ASSERT_SUCCESS(aws_mqtt_client_connection_set_on_any_publish_handler( - state_test_data->mqtt_connection, s_on_any_publish_received, state_test_data)); - - ASSERT_SUCCESS(aws_array_list_init_dynamic( - &state_test_data->published_messages, allocator, 4, sizeof(struct received_publish_packet))); - ASSERT_SUCCESS(aws_array_list_init_dynamic( - &state_test_data->any_published_messages, allocator, 4, sizeof(struct received_publish_packet))); - ASSERT_SUCCESS(aws_array_list_init_dynamic(&state_test_data->qos_returned, allocator, 2, sizeof(uint8_t))); - - ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_termination_handler( - state_test_data->mqtt_connection, s_on_connection_termination_fn, state_test_data)); - - return AWS_OP_SUCCESS; -} - -static void s_received_publish_packet_list_clean_up(struct aws_array_list *list) { - for (size_t i = 0; i < aws_array_list_length(list); ++i) { - struct received_publish_packet *val_ptr = NULL; - aws_array_list_get_at_ptr(list, (void **)&val_ptr, i); - aws_byte_buf_clean_up(&val_ptr->payload); - aws_byte_buf_clean_up(&val_ptr->topic); - } - aws_array_list_clean_up(list); + return aws_test311_setup_mqtt_server_fn(allocator, ctx); } static int s_clean_up_mqtt_server_fn(struct aws_allocator *allocator, int setup_result, void *ctx) { - (void)allocator; - - if (!setup_result) { - struct mqtt_connection_state_test *state_test_data = ctx; - - s_received_publish_packet_list_clean_up(&state_test_data->published_messages); - s_received_publish_packet_list_clean_up(&state_test_data->any_published_messages); - aws_array_list_clean_up(&state_test_data->qos_returned); - aws_mqtt_client_connection_release(state_test_data->mqtt_connection); - - s_wait_for_termination_to_complete(state_test_data); - ASSERT_UINT_EQUALS(1, state_test_data->connection_termination_calls); - - aws_mqtt_client_release(state_test_data->mqtt_client); - aws_client_bootstrap_release(state_test_data->client_bootstrap); - aws_host_resolver_release(state_test_data->host_resolver); - aws_server_bootstrap_destroy_socket_listener(state_test_data->server_bootstrap, state_test_data->listener); - s_wait_on_listener_cleanup(state_test_data); - aws_server_bootstrap_release(state_test_data->server_bootstrap); - aws_event_loop_group_release(state_test_data->el_group); - destroy_mqtt_mock_server(state_test_data->mock_server); - } - - aws_mqtt_library_clean_up(); - return AWS_OP_SUCCESS; -} - -static void s_on_connection_complete_fn( - struct aws_mqtt_client_connection *connection, - int error_code, - enum aws_mqtt_connect_return_code return_code, - bool session_present, - void *userdata) { - (void)connection; - struct mqtt_connection_state_test *state_test_data = userdata; - aws_mutex_lock(&state_test_data->lock); - - state_test_data->session_present = session_present; - state_test_data->mqtt_return_code = return_code; - state_test_data->error = error_code; - state_test_data->connection_completed = true; - aws_mutex_unlock(&state_test_data->lock); - - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_connection_completed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->connection_completed; -} - -static void s_wait_for_connection_to_complete(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_connection_completed, state_test_data); - state_test_data->connection_completed = false; - aws_mutex_unlock(&state_test_data->lock); -} - -void s_on_disconnect_fn(struct aws_mqtt_client_connection *connection, void *userdata) { - (void)connection; - struct mqtt_connection_state_test *state_test_data = userdata; - AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "disconnect completed"); - aws_mutex_lock(&state_test_data->lock); - state_test_data->client_disconnect_completed = true; - aws_mutex_unlock(&state_test_data->lock); - - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_disconnect_completed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->client_disconnect_completed && state_test_data->server_disconnect_completed; -} - -static void s_wait_for_disconnect_to_complete(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_disconnect_completed, state_test_data); - state_test_data->client_disconnect_completed = false; - state_test_data->server_disconnect_completed = false; - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_on_any_publish_received( - struct aws_mqtt_client_connection *connection, - const struct aws_byte_cursor *topic, - const struct aws_byte_cursor *payload, - bool dup, - enum aws_mqtt_qos qos, - bool retain, - void *userdata) { - (void)connection; - struct mqtt_connection_state_test *state_test_data = userdata; - - struct aws_byte_buf payload_cp; - aws_byte_buf_init_copy_from_cursor(&payload_cp, state_test_data->allocator, *payload); - struct aws_byte_buf topic_cp; - aws_byte_buf_init_copy_from_cursor(&topic_cp, state_test_data->allocator, *topic); - struct received_publish_packet received_packet = { - .payload = payload_cp, - .topic = topic_cp, - .dup = dup, - .qos = qos, - .retain = retain, - }; - - aws_mutex_lock(&state_test_data->lock); - aws_array_list_push_back(&state_test_data->any_published_messages, &received_packet); - state_test_data->any_publishes_received++; - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_any_publish_received(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->any_publishes_received == state_test_data->expected_any_publishes; -} - -static void s_wait_for_any_publish(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_any_publish_received, state_test_data); - state_test_data->any_publishes_received = 0; - state_test_data->expected_any_publishes = 0; - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_on_publish_received( - struct aws_mqtt_client_connection *connection, - const struct aws_byte_cursor *topic, - const struct aws_byte_cursor *payload, - bool dup, - enum aws_mqtt_qos qos, - bool retain, - void *userdata) { - - (void)connection; - (void)topic; - struct mqtt_connection_state_test *state_test_data = userdata; - - struct aws_byte_buf payload_cp; - aws_byte_buf_init_copy_from_cursor(&payload_cp, state_test_data->allocator, *payload); - struct aws_byte_buf topic_cp; - aws_byte_buf_init_copy_from_cursor(&topic_cp, state_test_data->allocator, *topic); - struct received_publish_packet received_packet = { - .payload = payload_cp, - .topic = topic_cp, - .dup = dup, - .qos = qos, - .retain = retain, - }; - - aws_mutex_lock(&state_test_data->lock); - aws_array_list_push_back(&state_test_data->published_messages, &received_packet); - state_test_data->publishes_received++; - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_publish_received(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->publishes_received == state_test_data->expected_publishes; -} - -static void s_wait_for_publish(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_publish_received, state_test_data); - state_test_data->publishes_received = 0; - state_test_data->expected_publishes = 0; - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_on_suback( - struct aws_mqtt_client_connection *connection, - uint16_t packet_id, - const struct aws_byte_cursor *topic, - enum aws_mqtt_qos qos, - int error_code, - void *userdata) { - (void)connection; - (void)packet_id; - (void)topic; - - struct mqtt_connection_state_test *state_test_data = userdata; - - aws_mutex_lock(&state_test_data->lock); - if (!error_code) { - aws_array_list_push_back(&state_test_data->qos_returned, &qos); - } - state_test_data->subscribe_completed = true; - state_test_data->subscribe_complete_error = error_code; - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_subscribe_completed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->subscribe_completed; -} - -static void s_wait_for_subscribe_to_complete(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_pred( - &state_test_data->cvar, &state_test_data->lock, s_is_subscribe_completed, state_test_data); - state_test_data->subscribe_completed = false; - aws_mutex_unlock(&state_test_data->lock); -} - -static void s_on_multi_suback( - struct aws_mqtt_client_connection *connection, - uint16_t packet_id, - const struct aws_array_list *topic_subacks, /* contains aws_mqtt_topic_subscription pointers */ - int error_code, - void *userdata) { - (void)connection; - (void)packet_id; - (void)topic_subacks; - (void)error_code; - - struct mqtt_connection_state_test *state_test_data = userdata; - - aws_mutex_lock(&state_test_data->lock); - state_test_data->subscribe_completed = true; - if (!error_code) { - size_t length = aws_array_list_length(topic_subacks); - for (size_t i = 0; i < length; ++i) { - struct aws_mqtt_topic_subscription *subscription = NULL; - aws_array_list_get_at(topic_subacks, &subscription, i); - aws_array_list_push_back(&state_test_data->qos_returned, &subscription->qos); - } - } - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static void s_on_op_complete( - struct aws_mqtt_client_connection *connection, - uint16_t packet_id, - int error_code, - void *userdata) { - (void)connection; - (void)packet_id; - - struct mqtt_connection_state_test *state_test_data = userdata; - AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "pub op completed"); - aws_mutex_lock(&state_test_data->lock); - state_test_data->ops_completed++; - state_test_data->op_complete_error = error_code; - aws_mutex_unlock(&state_test_data->lock); - aws_condition_variable_notify_one(&state_test_data->cvar); -} - -static bool s_is_ops_completed(void *arg) { - struct mqtt_connection_state_test *state_test_data = arg; - return state_test_data->ops_completed == state_test_data->expected_ops_completed; -} - -static void s_wait_for_ops_completed(struct mqtt_connection_state_test *state_test_data) { - aws_mutex_lock(&state_test_data->lock); - aws_condition_variable_wait_for_pred( - &state_test_data->cvar, &state_test_data->lock, 10000000000, s_is_ops_completed, state_test_data); - aws_mutex_unlock(&state_test_data->lock); + return aws_test311_clean_up_mqtt_server_fn(allocator, setup_result, ctx); } /* @@ -673,14 +35,14 @@ static int s_test_mqtt_connect_disconnect_fn(struct aws_allocator *allocator, vo .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -730,14 +92,14 @@ static int s_test_mqtt_connect_set_will_login_fn(struct aws_allocator *allocator .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -766,7 +128,7 @@ static int s_test_mqtt_connect_set_will_login_fn(struct aws_allocator *allocator /* Connect to the mock server again. If set will&loggin message is not called before the next connect, the * will&loggin message will still be there and be sent to the server again */ ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); /* The second CONNECT packet */ @@ -784,9 +146,9 @@ static int s_test_mqtt_connect_set_will_login_fn(struct aws_allocator *allocator ASSERT_TRUE(aws_byte_cursor_eq(&received_packet->password, &password)); /* disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* set new will & loggin message, before next connect, the next CONNECT packet will contain the new information */ struct aws_byte_cursor new_will_payload = aws_byte_cursor_from_c_str("this is a new will."); @@ -803,7 +165,7 @@ static int s_test_mqtt_connect_set_will_login_fn(struct aws_allocator *allocator /* connect again */ ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); /* The third CONNECT packet */ @@ -821,9 +183,9 @@ static int s_test_mqtt_connect_set_will_login_fn(struct aws_allocator *allocator ASSERT_TRUE(aws_byte_cursor_eq(&received_packet->password, &new_password)); /* disconnect. FINISHED */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -852,14 +214,14 @@ static int s_test_mqtt_connection_interrupted_fn(struct aws_allocator *allocator .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; aws_mqtt_client_connection_set_reconnect_timeout( state_test_data->mqtt_connection, MIN_RECONNECT_DELAY_SECONDS, MAX_RECONNECT_DELAY_SECONDS); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* shut it down and make sure the client automatically reconnects.*/ uint64_t now = 0; @@ -867,7 +229,7 @@ static int s_test_mqtt_connection_interrupted_fn(struct aws_allocator *allocator uint64_t start_shutdown = now; aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); aws_high_res_clock_get_ticks(&now); uint64_t reconnect_complete = now; @@ -877,9 +239,9 @@ static int s_test_mqtt_connection_interrupted_fn(struct aws_allocator *allocator aws_timestamp_convert(elapsed_time, AWS_TIMESTAMP_NANOS, AWS_TIMESTAMP_SECS, NULL) >= MIN_RECONNECT_DELAY_SECONDS); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -922,21 +284,21 @@ static int s_test_mqtt_connection_timeout_fn(struct aws_allocator *allocator, vo .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, }; mqtt_mock_server_set_max_ping_resp(state_test_data->mock_server, 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* this should take about 1.1 seconds for the timeout and reconnect.*/ - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); ASSERT_INT_EQUALS(AWS_ERROR_MQTT_TIMEOUT, state_test_data->interruption_error); @@ -962,14 +324,14 @@ static int s_test_mqtt_connection_any_publish_fn(struct aws_allocator *allocator .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; struct aws_byte_cursor topic_1 = aws_byte_cursor_from_c_str("/test/topic1"); struct aws_byte_cursor topic_2 = aws_byte_cursor_from_c_str("/test/topic2"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* NOTE: mock server sends to client with no subscription at all, which should not happen in the real world! */ state_test_data->expected_any_publishes = 2; @@ -990,12 +352,12 @@ static int s_test_mqtt_connection_any_publish_fn(struct aws_allocator *allocator AWS_MQTT_QOS_AT_LEAST_ONCE, false /*retain*/)); - s_wait_for_any_publish(state_test_data); + aws_test311_wait_for_any_publish(state_test_data); mqtt_mock_server_wait_for_pubacks(state_test_data->mock_server, 2); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -1048,14 +410,14 @@ static int s_test_mqtt_connection_connack_timeout_fn(struct aws_allocator *alloc .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, }; mqtt_mock_server_set_max_connack(state_test_data->mock_server, 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); ASSERT_INT_EQUALS(AWS_ERROR_MQTT_TIMEOUT, state_test_data->error); @@ -1080,14 +442,14 @@ static int s_test_mqtt_connection_failure_callback_fn(struct aws_allocator *allo .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, }; mqtt_mock_server_set_max_connack(state_test_data->mock_server, 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_fail(state_test_data); + aws_test311_wait_for_connection_to_fail(state_test_data); ASSERT_INT_EQUALS(AWS_ERROR_MQTT_TIMEOUT, state_test_data->error); @@ -1112,11 +474,11 @@ static int s_test_mqtt_connection_success_callback_fn(struct aws_allocator *allo .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_succeed(state_test_data); + aws_test311_wait_for_connection_to_succeed(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -1129,9 +491,9 @@ static int s_test_mqtt_connection_success_callback_fn(struct aws_allocator *allo ASSERT_TRUE(aws_byte_cursor_eq(&received_packet->client_identifier, &connection_options.client_id)); // Disconnect and finish - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -1155,7 +517,7 @@ static int s_test_mqtt_subscribe_fn(struct aws_allocator *allocator, void *ctx) .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; struct aws_byte_cursor sub_topic = aws_byte_cursor_from_c_str("/test/topic"); @@ -1164,17 +526,17 @@ static int s_test_mqtt_subscribe_fn(struct aws_allocator *allocator, void *ctx) state_test_data->mqtt_connection, &sub_topic, AWS_MQTT_QOS_AT_LEAST_ONCE, - s_on_publish_received, + aws_test311_on_publish_received, state_test_data, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data); ASSERT_TRUE(packet_id > 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); state_test_data->expected_publishes = 2; state_test_data->expected_any_publishes = 2; @@ -1195,13 +557,13 @@ static int s_test_mqtt_subscribe_fn(struct aws_allocator *allocator, void *ctx) AWS_MQTT_QOS_AT_LEAST_ONCE, false /*retain*/)); - s_wait_for_publish(state_test_data); - s_wait_for_any_publish(state_test_data); + aws_test311_wait_for_publish(state_test_data); + aws_test311_wait_for_any_publish(state_test_data); mqtt_mock_server_wait_for_pubacks(state_test_data->mock_server, 2); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -1282,7 +644,7 @@ static int s_test_mqtt_subscribe_incoming_dup_fn(struct aws_allocator *allocator .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; struct aws_byte_cursor subscribed_topic = aws_byte_cursor_from_c_str("/test/topic"); @@ -1292,16 +654,16 @@ static int s_test_mqtt_subscribe_incoming_dup_fn(struct aws_allocator *allocator state_test_data->mqtt_connection, &subscribed_topic, AWS_MQTT_QOS_AT_LEAST_ONCE, - s_on_publish_received, + aws_test311_on_publish_received, state_test_data, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data); ASSERT_TRUE(packet_id > 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); state_test_data->expected_publishes = 4; state_test_data->expected_any_publishes = 8; @@ -1330,13 +692,13 @@ static int s_test_mqtt_subscribe_incoming_dup_fn(struct aws_allocator *allocator false /*retain*/)); } - s_wait_for_publish(state_test_data); - s_wait_for_any_publish(state_test_data); + aws_test311_wait_for_publish(state_test_data); + aws_test311_wait_for_any_publish(state_test_data); mqtt_mock_server_wait_for_pubacks(state_test_data->mock_server, 8); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -1419,11 +781,11 @@ static int s_test_mqtt_connect_subscribe_fail_from_broker_fn(struct aws_allocato .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Disable the auto ACK packets sent by the server, we will send failure SUBACK */ mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); @@ -1434,15 +796,15 @@ static int s_test_mqtt_connect_subscribe_fail_from_broker_fn(struct aws_allocato state_test_data->mqtt_connection, &sub_topic, AWS_MQTT_QOS_AT_LEAST_ONCE, - s_on_publish_received, + aws_test311_on_publish_received, state_test_data, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data); ASSERT_TRUE(packet_id > 0); ASSERT_SUCCESS(mqtt_mock_server_send_single_suback(state_test_data->mock_server, packet_id, AWS_MQTT_QOS_FAILURE)); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); /* Check the subscribe returned QoS is failure */ size_t length = aws_array_list_length(&state_test_data->qos_returned); ASSERT_UINT_EQUALS(1, length); @@ -1450,9 +812,9 @@ static int s_test_mqtt_connect_subscribe_fail_from_broker_fn(struct aws_allocato ASSERT_SUCCESS(aws_array_list_get_at(&state_test_data->qos_returned, &qos, 0)); ASSERT_UINT_EQUALS(AWS_MQTT_QOS_FAILURE, qos); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -1476,7 +838,7 @@ static int s_test_mqtt_subscribe_multi_fn(struct aws_allocator *allocator, void .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; struct aws_byte_cursor sub_topic_1 = aws_byte_cursor_from_c_str("/test/topic1"); @@ -1485,14 +847,14 @@ static int s_test_mqtt_subscribe_multi_fn(struct aws_allocator *allocator, void struct aws_mqtt_topic_subscription sub1 = { .topic = sub_topic_1, .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, - .on_publish = s_on_publish_received, + .on_publish = aws_test311_on_publish_received, .on_cleanup = NULL, .on_publish_ud = state_test_data, }; struct aws_mqtt_topic_subscription sub2 = { .topic = sub_topic_2, .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, - .on_publish = s_on_publish_received, + .on_publish = aws_test311_on_publish_received, .on_cleanup = NULL, .on_publish_ud = state_test_data, }; @@ -1506,13 +868,13 @@ static int s_test_mqtt_subscribe_multi_fn(struct aws_allocator *allocator, void aws_array_list_push_back(&topic_filters, &sub2); uint16_t packet_id = aws_mqtt_client_connection_subscribe_multiple( - state_test_data->mqtt_connection, &topic_filters, s_on_multi_suback, state_test_data); + state_test_data->mqtt_connection, &topic_filters, aws_test311_on_multi_suback, state_test_data); ASSERT_TRUE(packet_id > 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); /* Check the subscribe returned QoS is expected */ size_t length = aws_array_list_length(&state_test_data->qos_returned); ASSERT_UINT_EQUALS(2, length); @@ -1539,7 +901,7 @@ static int s_test_mqtt_subscribe_multi_fn(struct aws_allocator *allocator, void false /*dup*/, AWS_MQTT_QOS_AT_LEAST_ONCE, false /*retain*/)); - s_wait_for_publish(state_test_data); + aws_test311_wait_for_publish(state_test_data); /* Let's do another publish on a topic that is not subscribed by client. * This can happen if the Server automatically assigned a subscription to the Client */ @@ -1553,13 +915,13 @@ static int s_test_mqtt_subscribe_multi_fn(struct aws_allocator *allocator, void false /*dup*/, AWS_MQTT_QOS_AT_LEAST_ONCE, false /*retain*/)); - s_wait_for_any_publish(state_test_data); + aws_test311_wait_for_any_publish(state_test_data); mqtt_mock_server_wait_for_pubacks(state_test_data->mock_server, 3); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -1627,7 +989,7 @@ static int s_test_mqtt_unsubscribe_fn(struct aws_allocator *allocator, void *ctx .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; struct aws_byte_cursor sub_topic_1 = aws_byte_cursor_from_c_str("/test/topic1"); @@ -1636,14 +998,14 @@ static int s_test_mqtt_unsubscribe_fn(struct aws_allocator *allocator, void *ctx struct aws_mqtt_topic_subscription sub1 = { .topic = sub_topic_1, .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, - .on_publish = s_on_publish_received, + .on_publish = aws_test311_on_publish_received, .on_cleanup = NULL, .on_publish_ud = state_test_data, }; struct aws_mqtt_topic_subscription sub2 = { .topic = sub_topic_2, .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, - .on_publish = s_on_publish_received, + .on_publish = aws_test311_on_publish_received, .on_cleanup = NULL, .on_publish_ud = state_test_data, }; @@ -1657,13 +1019,13 @@ static int s_test_mqtt_unsubscribe_fn(struct aws_allocator *allocator, void *ctx aws_array_list_push_back(&topic_filters, &sub2); uint16_t sub_packet_id = aws_mqtt_client_connection_subscribe_multiple( - state_test_data->mqtt_connection, &topic_filters, s_on_multi_suback, state_test_data); + state_test_data->mqtt_connection, &topic_filters, aws_test311_on_multi_suback, state_test_data); ASSERT_TRUE(sub_packet_id > 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); state_test_data->expected_any_publishes = 2; struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); @@ -1682,7 +1044,7 @@ static int s_test_mqtt_unsubscribe_fn(struct aws_allocator *allocator, void *ctx false /*dup*/, AWS_MQTT_QOS_AT_LEAST_ONCE, false /*retain*/)); - s_wait_for_any_publish(state_test_data); + aws_test311_wait_for_any_publish(state_test_data); mqtt_mock_server_wait_for_pubacks(state_test_data->mock_server, 2); aws_mutex_lock(&state_test_data->lock); @@ -1690,7 +1052,7 @@ static int s_test_mqtt_unsubscribe_fn(struct aws_allocator *allocator, void *ctx aws_mutex_unlock(&state_test_data->lock); /* unsubscribe to the first topic */ uint16_t unsub_packet_id = aws_mqtt_client_connection_unsubscribe( - state_test_data->mqtt_connection, &sub_topic_1, s_on_op_complete, state_test_data); + state_test_data->mqtt_connection, &sub_topic_1, aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(unsub_packet_id > 0); /* Even when the UNSUBACK has not received, the client will not invoke the on_pub callback for that topic */ ASSERT_SUCCESS(mqtt_mock_server_send_publish( @@ -1708,13 +1070,13 @@ static int s_test_mqtt_unsubscribe_fn(struct aws_allocator *allocator, void *ctx AWS_MQTT_QOS_AT_LEAST_ONCE, false /*retain*/)); state_test_data->expected_any_publishes = 2; - s_wait_for_any_publish(state_test_data); + aws_test311_wait_for_any_publish(state_test_data); mqtt_mock_server_wait_for_pubacks(state_test_data->mock_server, 2); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -1797,7 +1159,7 @@ static int s_test_mqtt_resubscribe_fn(struct aws_allocator *allocator, void *ctx .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; struct aws_byte_cursor sub_topic_1 = aws_byte_cursor_from_c_str("/test/topic1"); @@ -1807,21 +1169,21 @@ static int s_test_mqtt_resubscribe_fn(struct aws_allocator *allocator, void *ctx struct aws_mqtt_topic_subscription sub1 = { .topic = sub_topic_1, .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, - .on_publish = s_on_publish_received, + .on_publish = aws_test311_on_publish_received, .on_cleanup = NULL, .on_publish_ud = state_test_data, }; struct aws_mqtt_topic_subscription sub2 = { .topic = sub_topic_2, .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, - .on_publish = s_on_publish_received, + .on_publish = aws_test311_on_publish_received, .on_cleanup = NULL, .on_publish_ud = state_test_data, }; struct aws_mqtt_topic_subscription sub3 = { .topic = sub_topic_3, .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, - .on_publish = s_on_publish_received, + .on_publish = aws_test311_on_publish_received, .on_cleanup = NULL, .on_publish_ud = state_test_data, }; @@ -1836,31 +1198,31 @@ static int s_test_mqtt_resubscribe_fn(struct aws_allocator *allocator, void *ctx aws_array_list_push_back(&topic_filters, &sub3); uint16_t sub_packet_id = aws_mqtt_client_connection_subscribe_multiple( - state_test_data->mqtt_connection, &topic_filters, s_on_multi_suback, state_test_data); + state_test_data->mqtt_connection, &topic_filters, aws_test311_on_multi_suback, state_test_data); ASSERT_TRUE(sub_packet_id > 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); aws_mutex_lock(&state_test_data->lock); state_test_data->expected_ops_completed = 1; aws_mutex_unlock(&state_test_data->lock); /* unsubscribe to the first topic */ uint16_t unsub_packet_id = aws_mqtt_client_connection_unsubscribe( - state_test_data->mqtt_connection, &sub_topic_1, s_on_op_complete, state_test_data); + state_test_data->mqtt_connection, &sub_topic_1, aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(unsub_packet_id > 0); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); /* client still subscribes to topic_2 & topic_3 */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* reconnection to the same server */ ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Get all the packets out of the way */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -1870,14 +1232,14 @@ static int s_test_mqtt_resubscribe_fn(struct aws_allocator *allocator, void *ctx ASSERT_UINT_EQUALS(AWS_MQTT_PACKET_CONNECT, t_received_packet->type); /* resubscribes to topic_2 & topic_3 */ - uint16_t resub_packet_id = - aws_mqtt_resubscribe_existing_topics(state_test_data->mqtt_connection, s_on_multi_suback, state_test_data); + uint16_t resub_packet_id = aws_mqtt_resubscribe_existing_topics( + state_test_data->mqtt_connection, aws_test311_on_multi_suback, state_test_data); ASSERT_TRUE(resub_packet_id > 0); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); struct mqtt_decoded_packet *received_packet = @@ -1915,7 +1277,7 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); @@ -1923,7 +1285,7 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { struct aws_byte_cursor payload_2 = aws_byte_cursor_from_c_str("Test Message 2"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); aws_mutex_lock(&state_test_data->lock); state_test_data->expected_ops_completed = 3; @@ -1934,7 +1296,7 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); uint16_t packet_id_2 = aws_mqtt_client_connection_publish( @@ -1943,7 +1305,7 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_2, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_2 > 0); @@ -1954,15 +1316,15 @@ static int s_test_mqtt_publish_fn(struct aws_allocator *allocator, void *ctx) { AWS_MQTT_QOS_AT_LEAST_ONCE, false, NULL, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_3 > 0); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -2012,7 +1374,7 @@ static int s_test_mqtt_publish_payload_fn(struct aws_allocator *allocator, void .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); @@ -2022,7 +1384,7 @@ static int s_test_mqtt_publish_payload_fn(struct aws_allocator *allocator, void struct aws_byte_cursor payload_curser = aws_byte_cursor_from_buf(&buf_payload); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); aws_mutex_lock(&state_test_data->lock); state_test_data->expected_ops_completed = 1; @@ -2033,17 +1395,17 @@ static int s_test_mqtt_publish_payload_fn(struct aws_allocator *allocator, void AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_curser, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id > 0); /* clean up the payload buf, as user don't need to manage the buf and keep it valid until publish completes */ aws_byte_buf_clean_up(&buf_payload); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -2087,19 +1449,19 @@ static int s_test_mqtt_connection_offline_publish_fn(struct aws_allocator *alloc .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); mqtt_mock_server_set_max_connack(state_test_data->mock_server, 0); /* shut it down and make sure the client automatically reconnects.*/ aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_interrupt_to_complete(state_test_data); + aws_test311_wait_for_interrupt_to_complete(state_test_data); state_test_data->server_disconnect_completed = false; @@ -2118,7 +1480,7 @@ static int s_test_mqtt_connection_offline_publish_fn(struct aws_allocator *alloc AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data) > 0); ASSERT_TRUE( aws_mqtt_client_connection_publish( @@ -2127,21 +1489,21 @@ static int s_test_mqtt_connection_offline_publish_fn(struct aws_allocator *alloc AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_2, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data) > 0); aws_mutex_lock(&state_test_data->lock); ASSERT_FALSE(state_test_data->connection_resumed); aws_mutex_unlock(&state_test_data->lock); mqtt_mock_server_set_max_connack(state_test_data->mock_server, SIZE_MAX); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); aws_mutex_lock(&state_test_data->lock); ASSERT_TRUE(state_test_data->connection_resumed); aws_mutex_unlock(&state_test_data->lock); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Decode all received packets by mock server */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); @@ -2205,19 +1567,19 @@ static int s_test_mqtt_connection_disconnect_while_reconnecting(struct aws_alloc .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); mqtt_mock_server_set_max_connack(state_test_data->mock_server, 0); /* shut it down and the client automatically reconnects.*/ aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_interrupt_to_complete(state_test_data); + aws_test311_wait_for_interrupt_to_complete(state_test_data); struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); @@ -2234,7 +1596,7 @@ static int s_test_mqtt_connection_disconnect_while_reconnecting(struct aws_alloc AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data) > 0); ASSERT_TRUE( aws_mqtt_client_connection_publish( @@ -2243,15 +1605,15 @@ static int s_test_mqtt_connection_disconnect_while_reconnecting(struct aws_alloc AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_2, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data) > 0); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); aws_mqtt_client_connection_release(state_test_data->mqtt_connection); state_test_data->mqtt_connection = NULL; - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); return AWS_OP_SUCCESS; } @@ -2281,7 +1643,7 @@ static int s_test_mqtt_connection_closes_while_making_requests_fn(struct aws_all .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; @@ -2290,7 +1652,7 @@ static int s_test_mqtt_connection_closes_while_making_requests_fn(struct aws_all struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); aws_mutex_lock(&state_test_data->lock); state_test_data->expected_ops_completed = 1; @@ -2310,16 +1672,16 @@ static int s_test_mqtt_connection_closes_while_making_requests_fn(struct aws_all AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); - s_wait_for_reconnect_to_complete(state_test_data); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -2381,7 +1743,7 @@ static int s_test_mqtt_connection_resend_packets_fn(struct aws_allocator *alloca .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; @@ -2393,7 +1755,7 @@ static int s_test_mqtt_connection_resend_packets_fn(struct aws_allocator *alloca struct aws_byte_cursor payload_3 = aws_byte_cursor_from_c_str("Test Message 3"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); @@ -2408,10 +1770,10 @@ static int s_test_mqtt_connection_resend_packets_fn(struct aws_allocator *alloca state_test_data->mqtt_connection, &sub_topic, AWS_MQTT_QOS_AT_LEAST_ONCE, - s_on_publish_received, + aws_test311_on_publish_received, state_test_data, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data); ASSERT_TRUE(packet_ids[1] > 0); @@ -2435,15 +1797,15 @@ static int s_test_mqtt_connection_resend_packets_fn(struct aws_allocator *alloca /* shutdown the channel for some error */ aws_channel_shutdown(state_test_data->server_channel, AWS_ERROR_INVALID_STATE); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); /* Wait again, and ensure the publishes have been resent */ aws_thread_current_sleep(ONE_SEC); ASSERT_SUCCESS(s_check_resend_packets( state_test_data->mock_server, packet_count, true, packet_ids, AWS_ARRAY_SIZE(packet_ids))); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -2468,7 +1830,7 @@ static int s_test_mqtt_connection_not_retry_publish_QoS_0_fn(struct aws_allocato .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2477,7 +1839,7 @@ static int s_test_mqtt_connection_not_retry_publish_QoS_0_fn(struct aws_allocato struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* kill the connection */ aws_channel_shutdown(state_test_data->server_channel, AWS_ERROR_INVALID_STATE); @@ -2493,23 +1855,23 @@ static int s_test_mqtt_connection_not_retry_publish_QoS_0_fn(struct aws_allocato AWS_MQTT_QOS_AT_MOST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); /* publish should complete after the shutdown */ - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); /* wait for reconnect */ - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); /* Check all received packets, no publish packets ever received by the server. Because the connection lost before it * ever get sent. */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(state_test_data->mock_server)); ASSERT_NULL( mqtt_mock_server_find_decoded_packet_by_type(state_test_data->mock_server, 0, AWS_MQTT_PACKET_PUBLISH, NULL)); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -2535,7 +1897,7 @@ static int s_test_mqtt_connection_consistent_retry_policy_fn(struct aws_allocato .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .protocol_operation_timeout_ms = 3000, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ @@ -2546,7 +1908,7 @@ static int s_test_mqtt_connection_consistent_retry_policy_fn(struct aws_allocato struct aws_byte_cursor sub_topic = aws_byte_cursor_from_c_str("/test/topic"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_channel_handler *handler = state_test_data->mock_server; /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ @@ -2563,7 +1925,7 @@ static int s_test_mqtt_connection_consistent_retry_policy_fn(struct aws_allocato AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); /* make another subscribe */ @@ -2574,12 +1936,12 @@ static int s_test_mqtt_connection_consistent_retry_policy_fn(struct aws_allocato NULL, NULL, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data); ASSERT_TRUE(packet_id_2 > 0); /* wait for reconnect */ - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); /* Wait for 1 sec. ensure all the requests have been received by the server */ aws_thread_current_sleep(ONE_SEC); @@ -2594,9 +1956,9 @@ static int s_test_mqtt_connection_consistent_retry_policy_fn(struct aws_allocato mqtt_mock_server_enable_auto_ack(handler); /* Kill the connection again, the requests will be retried since the response has not been received yet. */ aws_channel_shutdown(state_test_data->server_channel, AWS_ERROR_INVALID_STATE); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); /* subscribe should be able to completed now */ - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); /* Check all received packets, subscribe and publish has been resent */ ASSERT_SUCCESS(mqtt_mock_server_decode_packets(handler)); @@ -2606,9 +1968,9 @@ static int s_test_mqtt_connection_consistent_retry_policy_fn(struct aws_allocato mqtt_mock_server_find_decoded_packet_by_type(handler, packet_count, AWS_MQTT_PACKET_SUBSCRIBE, NULL)); ASSERT_NOT_NULL(mqtt_mock_server_find_decoded_packet_by_type(handler, packet_count, AWS_MQTT_PACKET_PUBLISH, NULL)); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -2635,7 +1997,7 @@ static int s_test_mqtt_connection_not_resend_packets_on_healthy_connection_fn( .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; @@ -2645,7 +2007,7 @@ static int s_test_mqtt_connection_not_resend_packets_on_healthy_connection_fn( struct aws_byte_cursor sub_topic = aws_byte_cursor_from_c_str("/test/topic"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_channel_handler *handler = state_test_data->mock_server; /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ @@ -2661,7 +2023,7 @@ static int s_test_mqtt_connection_not_resend_packets_on_healthy_connection_fn( AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); /* make another subscribe */ @@ -2672,7 +2034,7 @@ static int s_test_mqtt_connection_not_resend_packets_on_healthy_connection_fn( NULL, NULL, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data); ASSERT_TRUE(packet_id_2 > 0); @@ -2693,9 +2055,9 @@ static int s_test_mqtt_connection_not_resend_packets_on_healthy_connection_fn( mqtt_mock_server_find_decoded_packet_by_type(handler, pre_index + 1, AWS_MQTT_PACKET_SUBSCRIBE, NULL)); } - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -2723,17 +2085,17 @@ static int s_test_mqtt_connection_destory_pending_requests_fn(struct aws_allocat AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data) > 0); ASSERT_TRUE( aws_mqtt_client_connection_subscribe( state_test_data->mqtt_connection, &topic, AWS_MQTT_QOS_AT_LEAST_ONCE, - s_on_publish_received, + aws_test311_on_publish_received, state_test_data, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data) > 0); return AWS_OP_SUCCESS; @@ -2757,13 +2119,13 @@ static int s_test_mqtt_clean_session_not_retry_fn(struct aws_allocator *allocato .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_channel_handler *handler = state_test_data->mock_server; mqtt_mock_server_disable_auto_ack(handler); @@ -2777,33 +2139,33 @@ static int s_test_mqtt_clean_session_not_retry_fn(struct aws_allocator *allocato AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data) > 0); ASSERT_TRUE( aws_mqtt_client_connection_subscribe( state_test_data->mqtt_connection, &topic, AWS_MQTT_QOS_AT_LEAST_ONCE, - s_on_publish_received, + aws_test311_on_publish_received, state_test_data, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data) > 0); aws_mutex_lock(&state_test_data->lock); state_test_data->expected_ops_completed = 1; aws_mutex_unlock(&state_test_data->lock); /* Shutdown the connection */ aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_interrupt_to_complete(state_test_data); + aws_test311_wait_for_interrupt_to_complete(state_test_data); /* Once the connection lost, the requests will fail */ ASSERT_UINT_EQUALS(state_test_data->op_complete_error, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION); ASSERT_UINT_EQUALS(state_test_data->subscribe_complete_error, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION); /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -2826,7 +2188,7 @@ static int s_test_mqtt_clean_session_discard_previous_fn(struct aws_allocator *a .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2844,25 +2206,25 @@ static int s_test_mqtt_clean_session_discard_previous_fn(struct aws_allocator *a AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data) > 0); ASSERT_TRUE( aws_mqtt_client_connection_subscribe( state_test_data->mqtt_connection, &topic, AWS_MQTT_QOS_AT_LEAST_ONCE, - s_on_publish_received, + aws_test311_on_publish_received, state_test_data, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data) > 0); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_channel_handler *handler = state_test_data->mock_server; - s_wait_for_ops_completed(state_test_data); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); ASSERT_UINT_EQUALS(state_test_data->op_complete_error, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION); ASSERT_UINT_EQUALS(state_test_data->subscribe_complete_error, AWS_ERROR_MQTT_CANCELLED_FOR_CLEAN_SESSION); @@ -2872,9 +2234,9 @@ static int s_test_mqtt_clean_session_discard_previous_fn(struct aws_allocator *a ASSERT_NULL(mqtt_mock_server_find_decoded_packet_by_type(handler, 0, AWS_MQTT_PACKET_SUBSCRIBE, NULL)); /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -2896,7 +2258,7 @@ static int s_test_mqtt_clean_session_keep_next_session_fn(struct aws_allocator * .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ }; @@ -2916,23 +2278,23 @@ static int s_test_mqtt_clean_session_keep_next_session_fn(struct aws_allocator * AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data) > 0); ASSERT_TRUE( aws_mqtt_client_connection_subscribe( state_test_data->mqtt_connection, &topic, AWS_MQTT_QOS_AT_LEAST_ONCE, - s_on_publish_received, + aws_test311_on_publish_received, state_test_data, NULL, - s_on_suback, + aws_test311_on_suback, state_test_data) > 0); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_channel_handler *handler = state_test_data->mock_server; - s_wait_for_ops_completed(state_test_data); - s_wait_for_subscribe_to_complete(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_subscribe_to_complete(state_test_data); ASSERT_UINT_EQUALS(state_test_data->op_complete_error, AWS_ERROR_SUCCESS); ASSERT_UINT_EQUALS(state_test_data->subscribe_complete_error, AWS_ERROR_SUCCESS); @@ -2942,9 +2304,9 @@ static int s_test_mqtt_clean_session_keep_next_session_fn(struct aws_allocator * ASSERT_NOT_NULL(mqtt_mock_server_find_decoded_packet_by_type(handler, 0, AWS_MQTT_PACKET_SUBSCRIBE, NULL)); /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -2968,7 +2330,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_fn(struct aws_allocator * .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .protocol_operation_timeout_ms = 3000, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ @@ -2978,7 +2340,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_fn(struct aws_allocator * struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); @@ -2993,17 +2355,17 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_fn(struct aws_allocator * AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); /* publish should complete after the shutdown */ - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); /* Check the publish has been completed with timeout error */ ASSERT_UINT_EQUALS(state_test_data->op_complete_error, AWS_ERROR_MQTT_TIMEOUT); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3028,7 +2390,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_with_ping_fn(struct aws_a .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = 99, .protocol_operation_timeout_ms = 20, .keep_alive_time_secs = 3, // connection->keep_alive_time_ns, pushoff the timestamp by that amount @@ -3038,7 +2400,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_with_ping_fn(struct aws_a struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); @@ -3055,7 +2417,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_with_ping_fn(struct aws_a AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); @@ -3072,11 +2434,11 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_with_ping_fn(struct aws_a aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); /* publish should complete after the shutdown */ - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3088,10 +2450,70 @@ AWS_TEST_CASE_FIXTURE( s_clean_up_mqtt_server_fn, &test_data) +/** + * Test that connection is healthy, user set the timeout for request, and timeout happens and the publish failed. + */ +static int s_test_mqtt_connection_publish_QoS1_timeout_override_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(state_test_data); + + /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + /* make a publish with QoS 1 immediate. */ + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + uint16_t packet_id_1 = aws_mqtt_client_connection_311_publish( + state_test_data->mqtt_connection->impl, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + false, + &payload_1, + aws_test311_on_op_complete, + state_test_data, + aws_timestamp_convert(3, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + ASSERT_TRUE(packet_id_1 > 0); + + /* publish should complete after the shutdown */ + aws_test311_wait_for_ops_completed(state_test_data); + /* Check the publish has been completed with timeout error */ + ASSERT_UINT_EQUALS(state_test_data->op_complete_error, AWS_ERROR_MQTT_TIMEOUT); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_publish_QoS1_timeout_override, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_publish_QoS1_timeout_override_fn, + s_clean_up_mqtt_server_fn, + &test_data) + /** * Test that connection is healthy, user set the timeout for request, and timeout happens and the unsubscribe failed. */ -static int s_test_mqtt_connection_unsub_timeout_fn(struct aws_allocator *allocator, void *ctx) { +static int s_test_mqtt_connection_unsubscribe_timeout_fn(struct aws_allocator *allocator, void *ctx) { (void)allocator; struct mqtt_connection_state_test *state_test_data = ctx; @@ -3101,7 +2523,7 @@ static int s_test_mqtt_connection_unsub_timeout_fn(struct aws_allocator *allocat .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .protocol_operation_timeout_ms = 3000, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ @@ -3110,7 +2532,7 @@ static int s_test_mqtt_connection_unsub_timeout_fn(struct aws_allocator *allocat struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); @@ -3121,24 +2543,343 @@ static int s_test_mqtt_connection_unsub_timeout_fn(struct aws_allocator *allocat /* unsubscribe to the first topic */ uint16_t unsub_packet_id = aws_mqtt_client_connection_unsubscribe( - state_test_data->mqtt_connection, &pub_topic, s_on_op_complete, state_test_data); + state_test_data->mqtt_connection, &pub_topic, aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(unsub_packet_id > 0); - /* publish should complete after the shutdown */ - s_wait_for_ops_completed(state_test_data); - /* Check the publish has been completed with timeout error */ + /* unsubscribe should complete after the timeout */ + aws_test311_wait_for_ops_completed(state_test_data); + /* Check that the unsubscribe has been completed with a timeout error */ ASSERT_UINT_EQUALS(state_test_data->op_complete_error, AWS_ERROR_MQTT_TIMEOUT); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } AWS_TEST_CASE_FIXTURE( - mqtt_connection_unsub_timeout, + mqtt_connection_unsubscribe_timeout, s_setup_mqtt_server_fn, - s_test_mqtt_connection_unsub_timeout_fn, + s_test_mqtt_connection_unsubscribe_timeout_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Test that connection is healthy, user set the timeout for request, and timeout happens and the unsubscribe failed. + */ +static int s_test_mqtt_connection_unsubscribe_timeout_override_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(state_test_data); + + /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + aws_mutex_lock(&state_test_data->lock); + state_test_data->expected_ops_completed = 1; + aws_mutex_unlock(&state_test_data->lock); + + /* unsubscribe to the first topic */ + uint16_t unsub_packet_id = aws_mqtt_client_connection_311_unsubscribe( + state_test_data->mqtt_connection->impl, + &pub_topic, + aws_test311_on_op_complete, + state_test_data, + aws_timestamp_convert(3, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + ASSERT_TRUE(unsub_packet_id > 0); + + /* unsubscribe should complete after the timeout */ + aws_test311_wait_for_ops_completed(state_test_data); + /* Check that the unsubscribe has been completed with a timeout error */ + ASSERT_UINT_EQUALS(state_test_data->op_complete_error, AWS_ERROR_MQTT_TIMEOUT); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_unsubscribe_timeout_override, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_unsubscribe_timeout_override_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Test that connection is healthy, user set the timeout for request, and timeout happens and the subscribe failed. + */ +static int s_test_mqtt_connection_subscribe_single_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(state_test_data); + + /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + /* subscribe */ + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + uint16_t sub_packet_id = aws_mqtt_client_connection_subscribe( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + aws_test311_on_publish_received, + state_test_data, + NULL, + aws_test311_on_suback, + state_test_data); + ASSERT_TRUE(sub_packet_id > 0); + + /* subscribe should complete after the timeout */ + aws_test311_wait_for_subscribe_to_complete(state_test_data); + /* Check that the subscribe has been completed with a timeout error */ + ASSERT_UINT_EQUALS(state_test_data->subscribe_complete_error, AWS_ERROR_MQTT_TIMEOUT); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_subscribe_single_timeout, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_subscribe_single_timeout_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Test that connection is healthy, user set the timeout for request, and timeout happens and the subscribe failed. + */ +static int s_test_mqtt_connection_subscribe_single_timeout_override_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(state_test_data); + + /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + /* subscribe */ + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + uint16_t sub_packet_id = aws_mqtt_client_connection_311_subscribe( + state_test_data->mqtt_connection->impl, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + aws_test311_on_publish_received, + state_test_data, + NULL, + aws_test311_on_suback, + state_test_data, + aws_timestamp_convert(3, AWS_TIMESTAMP_SECS, AWS_TIMESTAMP_NANOS, NULL)); + ASSERT_TRUE(sub_packet_id > 0); + + /* subscribe should complete after the timeout */ + aws_test311_wait_for_subscribe_to_complete(state_test_data); + /* Check that the subscribe has been completed with a timeout error */ + ASSERT_UINT_EQUALS(state_test_data->subscribe_complete_error, AWS_ERROR_MQTT_TIMEOUT); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_subscribe_single_timeout_override, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_subscribe_single_timeout_override_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Test that connection is healthy, user set the timeout for request, and timeout happens and the multi-subscribe + * failed. + */ +static int s_test_mqtt_connection_subscribe_multi_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(state_test_data); + + /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + struct aws_byte_cursor sub_topic_1 = aws_byte_cursor_from_c_str("/test/topic1"); + struct aws_byte_cursor sub_topic_2 = aws_byte_cursor_from_c_str("/test/topic2"); + + struct aws_mqtt_topic_subscription sub1 = { + .topic = sub_topic_1, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .on_publish = aws_test311_on_publish_received, + .on_cleanup = NULL, + .on_publish_ud = state_test_data, + }; + struct aws_mqtt_topic_subscription sub2 = { + .topic = sub_topic_2, + .qos = AWS_MQTT_QOS_AT_LEAST_ONCE, + .on_publish = aws_test311_on_publish_received, + .on_cleanup = NULL, + .on_publish_ud = state_test_data, + }; + + struct aws_array_list topic_filters; + size_t list_len = 2; + AWS_VARIABLE_LENGTH_ARRAY(uint8_t, static_buf, list_len * sizeof(struct aws_mqtt_topic_subscription)); + aws_array_list_init_static(&topic_filters, static_buf, list_len, sizeof(struct aws_mqtt_topic_subscription)); + + aws_array_list_push_back(&topic_filters, &sub1); + aws_array_list_push_back(&topic_filters, &sub2); + + uint16_t sub_packet_id = aws_mqtt_client_connection_subscribe_multiple( + state_test_data->mqtt_connection, &topic_filters, aws_test311_on_multi_suback, state_test_data); + + ASSERT_TRUE(sub_packet_id > 0); + + /* subscribe should complete after the timeout */ + aws_test311_wait_for_subscribe_to_complete(state_test_data); + /* Check that the subscribe has been completed with a timeout error */ + ASSERT_UINT_EQUALS(state_test_data->subscribe_complete_error, AWS_ERROR_MQTT_TIMEOUT); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_subscribe_multi_timeout, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_subscribe_multi_timeout_fn, + s_clean_up_mqtt_server_fn, + &test_data) + +/** + * Test that connection is healthy, user set the timeout for request, and timeout happens and the subscribe failed. + */ +static int s_test_mqtt_connection_resubscribe_timeout_fn(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + struct mqtt_connection_state_test *state_test_data = ctx; + + struct aws_mqtt_connection_options connection_options = { + .user_data = state_test_data, + .clean_session = false, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), + .socket_options = &state_test_data->socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + .protocol_operation_timeout_ms = 3000, + .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(state_test_data); + + /* subscribe */ + struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); + uint16_t sub_packet_id = aws_mqtt_client_connection_subscribe( + state_test_data->mqtt_connection, + &pub_topic, + AWS_MQTT_QOS_AT_LEAST_ONCE, + aws_test311_on_publish_received, + state_test_data, + NULL, + aws_test311_on_suback, + state_test_data); + ASSERT_TRUE(sub_packet_id > 0); + + /* subscribe should complete after the timeout */ + aws_test311_wait_for_subscribe_to_complete(state_test_data); + ASSERT_UINT_EQUALS(state_test_data->subscribe_complete_error, AWS_ERROR_SUCCESS); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); + + /* reconnection to the same server */ + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(state_test_data); + + /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ + mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); + + uint16_t resub_packet_id = aws_mqtt_resubscribe_existing_topics( + state_test_data->mqtt_connection, aws_test311_on_multi_suback, state_test_data); + ASSERT_TRUE(resub_packet_id > 0); + aws_test311_wait_for_subscribe_to_complete(state_test_data); + + /* Check that the resubscribe has been completed with a timeout error */ + ASSERT_UINT_EQUALS(state_test_data->subscribe_complete_error, AWS_ERROR_MQTT_TIMEOUT); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_resubscribe_timeout, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_resubscribe_timeout_fn, s_clean_up_mqtt_server_fn, &test_data) @@ -3157,7 +2898,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_connection_lost_reset_tim .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, .protocol_operation_timeout_ms = 3000, .keep_alive_time_secs = 16960, /* basically stop automatically sending PINGREQ */ @@ -3167,7 +2908,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_connection_lost_reset_tim struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Disable the auto ACK packets sent by the server, which blocks the requests to complete */ mqtt_mock_server_disable_auto_ack(state_test_data->mock_server); @@ -3182,7 +2923,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_connection_lost_reset_tim AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); @@ -3190,7 +2931,7 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_connection_lost_reset_tim aws_thread_current_sleep((uint64_t)ONE_SEC * 2); /* Kill the connection, the requests will be retried and the timeout will be reset. */ aws_channel_shutdown(state_test_data->server_channel, AWS_ERROR_INVALID_STATE); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); /* sleep for 2 sec again, in total the response has not received for more than 4 sec, timeout should happen if the * lost of connection not reset the timeout */ aws_thread_current_sleep((uint64_t)ONE_SEC * 2); @@ -3198,12 +2939,12 @@ static int s_test_mqtt_connection_publish_QoS1_timeout_connection_lost_reset_tim ASSERT_SUCCESS(mqtt_mock_server_send_puback(state_test_data->mock_server, packet_id_1)); /* publish should complete after the shutdown */ - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); /* Check the publish has been completed successfully since the lost of the connection reset the timeout */ ASSERT_UINT_EQUALS(state_test_data->op_complete_error, AWS_ERROR_SUCCESS); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3243,21 +2984,21 @@ static int s_test_mqtt_connection_close_callback_simple_fn(struct aws_allocator .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; aws_mqtt_client_connection_set_connection_closed_handler( state_test_data->mqtt_connection, s_on_connection_closed_fn, state_test_data); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* sleep for 2 sec, just to make sure the connection is stable */ aws_thread_current_sleep((uint64_t)ONE_SEC * 2); /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Make sure the callback was called and the value is what we expect */ ASSERT_UINT_EQUALS(1, state_test_data->connection_close_calls); @@ -3285,25 +3026,25 @@ static int s_test_mqtt_connection_close_callback_interrupted_fn(struct aws_alloc .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; aws_mqtt_client_connection_set_connection_closed_handler( state_test_data->mqtt_connection, s_on_connection_closed_fn, state_test_data); ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Kill the connection */ aws_channel_shutdown(state_test_data->server_channel, AWS_ERROR_INVALID_STATE); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); /* sleep for 2 sec, just to make sure the connection is stable */ aws_thread_current_sleep((uint64_t)ONE_SEC * 2); /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* Make sure the callback was called only ONCE and the value is what we expect */ ASSERT_UINT_EQUALS(1, state_test_data->connection_close_calls); @@ -3331,7 +3072,7 @@ static int s_test_mqtt_connection_close_callback_multi_fn(struct aws_allocator * .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; aws_mqtt_client_connection_set_connection_closed_handler( state_test_data->mqtt_connection, s_on_connection_closed_fn, state_test_data); @@ -3339,12 +3080,12 @@ static int s_test_mqtt_connection_close_callback_multi_fn(struct aws_allocator * int disconnect_amount = 10; for (int i = 0; i < disconnect_amount; i++) { ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* Disconnect */ ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( - state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); } /* Make sure the callback was called disconnect_amount times */ @@ -3372,11 +3113,11 @@ static int s_test_mqtt_connection_reconnection_backoff_stable(struct aws_allocat .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); uint64_t time_before = 0; uint64_t time_after = 0; @@ -3390,7 +3131,7 @@ static int s_test_mqtt_connection_reconnection_backoff_stable(struct aws_allocat /* shut it down and make sure the client automatically reconnects.*/ aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); aws_high_res_clock_get_ticks(&time_after); @@ -3403,9 +3144,9 @@ static int s_test_mqtt_connection_reconnection_backoff_stable(struct aws_allocat } /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3429,11 +3170,11 @@ static int s_test_mqtt_connection_reconnection_backoff_unstable(struct aws_alloc .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); uint64_t time_before = 0; uint64_t time_after = 0; @@ -3444,7 +3185,7 @@ static int s_test_mqtt_connection_reconnection_backoff_unstable(struct aws_alloc /* shut it down and make sure the client automatically reconnects.*/ aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); aws_high_res_clock_get_ticks(&time_after); @@ -3460,9 +3201,9 @@ static int s_test_mqtt_connection_reconnection_backoff_unstable(struct aws_alloc } /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3486,11 +3227,11 @@ static int s_test_mqtt_connection_reconnection_backoff_reset(struct aws_allocato .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); uint64_t time_before = 0; uint64_t time_after = 0; @@ -3502,7 +3243,7 @@ static int s_test_mqtt_connection_reconnection_backoff_reset(struct aws_allocato /* shut it down and make sure the client automatically reconnects.*/ aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); aws_high_res_clock_get_ticks(&time_after); reconnection_backoff = time_after - time_before; @@ -3522,7 +3263,7 @@ static int s_test_mqtt_connection_reconnection_backoff_reset(struct aws_allocato /* shut it down and make sure the client automatically reconnects.*/ aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); aws_high_res_clock_get_ticks(&time_after); reconnection_backoff = time_after - time_before; @@ -3533,9 +3274,9 @@ static int s_test_mqtt_connection_reconnection_backoff_reset(struct aws_allocato ASSERT_TRUE(remainder <= RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS); /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3561,11 +3302,11 @@ static int s_test_mqtt_connection_reconnection_backoff_reset_after_disconnection .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); uint64_t time_before = 0; uint64_t time_after = 0; @@ -3576,7 +3317,7 @@ static int s_test_mqtt_connection_reconnection_backoff_reset_after_disconnection /* shut it down and make sure the client automatically reconnects.*/ aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); aws_high_res_clock_get_ticks(&time_after); reconnection_backoff = time_after - time_before; @@ -3587,18 +3328,18 @@ static int s_test_mqtt_connection_reconnection_backoff_reset_after_disconnection expected_reconnect_backoff = aws_min_u64(expected_reconnect_backoff * 2, 10); } /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); /* connect again */ ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); aws_high_res_clock_get_ticks(&time_before); aws_channel_shutdown(state_test_data->server_channel, AWS_OP_SUCCESS); - s_wait_for_reconnect_to_complete(state_test_data); + aws_test311_wait_for_reconnect_to_complete(state_test_data); aws_high_res_clock_get_ticks(&time_after); reconnection_backoff = time_after - time_before; @@ -3609,9 +3350,9 @@ static int s_test_mqtt_connection_reconnection_backoff_reset_after_disconnection ASSERT_TRUE(remainder <= RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS); /* Disconnect */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3636,7 +3377,7 @@ static int s_test_mqtt_connection_ping_norm_fn(struct aws_allocator *allocator, .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .keep_alive_time_secs = 1, .ping_timeout_ms = 100, }; @@ -3649,9 +3390,9 @@ static int s_test_mqtt_connection_ping_norm_fn(struct aws_allocator *allocator, /* Ensure the server got 4 PING packets */ ASSERT_INT_EQUALS(4, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3677,13 +3418,13 @@ static int s_test_mqtt_connection_ping_no_fn(struct aws_allocator *allocator, vo .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .keep_alive_time_secs = 1, .ping_timeout_ms = 100, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); @@ -3703,7 +3444,7 @@ static int s_test_mqtt_connection_ping_no_fn(struct aws_allocator *allocator, vo AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id > 0); @@ -3718,9 +3459,9 @@ static int s_test_mqtt_connection_ping_no_fn(struct aws_allocator *allocator, vo /* Ensure the server got 0 PING packets */ ASSERT_INT_EQUALS(0, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3746,13 +3487,13 @@ static int s_test_mqtt_connection_ping_noack_fn(struct aws_allocator *allocator, .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .keep_alive_time_secs = 1, .ping_timeout_ms = 100, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_byte_cursor pub_topic = aws_byte_cursor_from_c_str("/test/topic"); struct aws_byte_cursor payload_1 = aws_byte_cursor_from_c_str("Test Message 1"); @@ -3772,7 +3513,7 @@ static int s_test_mqtt_connection_ping_noack_fn(struct aws_allocator *allocator, AWS_MQTT_QOS_AT_MOST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id > 0); @@ -3786,9 +3527,9 @@ static int s_test_mqtt_connection_ping_noack_fn(struct aws_allocator *allocator, /* Ensure the server got 4 PING packets */ ASSERT_INT_EQUALS(4, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3820,13 +3561,13 @@ static int s_test_mqtt_connection_ping_basic_scenario_fn(struct aws_allocator *a .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .keep_alive_time_secs = 4, .ping_timeout_ms = 100, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* PING will be in 4 seconds */ aws_thread_current_sleep(3000000000); /* Wait 3 seconds */ @@ -3844,10 +3585,10 @@ static int s_test_mqtt_connection_ping_basic_scenario_fn(struct aws_allocator *a AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); /* Publish packet written at 3 seconds */ aws_thread_current_sleep(1250000000); /* Wait 1.25 second (the extra 0.25 is to account for jitter) */ @@ -3870,9 +3611,9 @@ static int s_test_mqtt_connection_ping_basic_scenario_fn(struct aws_allocator *a ASSERT_INT_EQUALS(2, mqtt_mock_server_get_ping_count(state_test_data->mock_server)); /* Disconnect and finish! */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3898,13 +3639,13 @@ static int s_test_mqtt_connection_ping_double_scenario_fn(struct aws_allocator * .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, .keep_alive_time_secs = 4, .ping_timeout_ms = 100, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); /* PING will be in 4 seconds */ aws_thread_current_sleep(3000000000); /* Wait 3 seconds */ @@ -3922,10 +3663,10 @@ static int s_test_mqtt_connection_ping_double_scenario_fn(struct aws_allocator * AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_1 > 0); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); /* Publish packet written at 3 seconds */ aws_thread_current_sleep(1250000000); /* Wait 1.25 second (the extra 0.25 is to account for jitter) */ @@ -3947,10 +3688,10 @@ static int s_test_mqtt_connection_ping_double_scenario_fn(struct aws_allocator * AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload_1, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data); ASSERT_TRUE(packet_id_2 > 0); - s_wait_for_ops_completed(state_test_data); + aws_test311_wait_for_ops_completed(state_test_data); /* Publish packet written at 2 seconds (relative to PING that was scheduled above) */ aws_thread_current_sleep(4250000000); /* Wait 4.25 (the extra 0.25 is to account for jitter) seconds */ @@ -3968,9 +3709,9 @@ static int s_test_mqtt_connection_ping_double_scenario_fn(struct aws_allocator * */ /* Disconnect and finish! */ - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -3991,7 +3732,7 @@ static int s_test_mqtt_connection_termination_callback_simple_fn(struct aws_allo struct mqtt_connection_state_test *state_test_data = ctx; ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_termination_handler( - state_test_data->mqtt_connection, s_on_connection_termination_fn, state_test_data)); + state_test_data->mqtt_connection, aws_test311_on_connection_termination_fn, state_test_data)); return AWS_OP_SUCCESS; } @@ -4016,11 +3757,11 @@ static int s_test_mqtt_validation_failure_publish_qos_fn(struct aws_allocator *a .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_byte_cursor topic = aws_byte_cursor_from_c_str("a/b"); ASSERT_INT_EQUALS( @@ -4031,14 +3772,14 @@ static int s_test_mqtt_validation_failure_publish_qos_fn(struct aws_allocator *a (enum aws_mqtt_qos)3, true, NULL, - s_on_op_complete, + aws_test311_on_op_complete, state_test_data)); int error_code = aws_last_error(); ASSERT_INT_EQUALS(AWS_ERROR_MQTT_INVALID_QOS, error_code); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -4063,11 +3804,11 @@ static int s_test_mqtt_validation_failure_subscribe_empty_fn(struct aws_allocato .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); struct aws_array_list topic_filters; size_t list_len = 2; @@ -4077,13 +3818,13 @@ static int s_test_mqtt_validation_failure_subscribe_empty_fn(struct aws_allocato ASSERT_INT_EQUALS( 0, aws_mqtt_client_connection_subscribe_multiple( - state_test_data->mqtt_connection, &topic_filters, s_on_multi_suback, state_test_data)); + state_test_data->mqtt_connection, &topic_filters, aws_test311_on_multi_suback, state_test_data)); int error_code = aws_last_error(); ASSERT_INT_EQUALS(AWS_ERROR_INVALID_ARGUMENT, error_code); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -4108,22 +3849,22 @@ static int s_test_mqtt_validation_failure_unsubscribe_null_fn(struct aws_allocat .client_id = aws_byte_cursor_from_c_str("client1234"), .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_SUCCESS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); - s_wait_for_connection_to_complete(state_test_data); + aws_test311_wait_for_connection_to_complete(state_test_data); ASSERT_INT_EQUALS( 0, aws_mqtt_client_connection_unsubscribe( - state_test_data->mqtt_connection, NULL, s_on_op_complete, state_test_data)); + state_test_data->mqtt_connection, NULL, aws_test311_on_op_complete, state_test_data)); int error_code = aws_last_error(); ASSERT_INT_EQUALS(AWS_ERROR_MQTT_INVALID_TOPIC, error_code); - ASSERT_SUCCESS( - aws_mqtt_client_connection_disconnect(state_test_data->mqtt_connection, s_on_disconnect_fn, state_test_data)); - s_wait_for_disconnect_to_complete(state_test_data); + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + state_test_data->mqtt_connection, aws_test311_on_disconnect_fn, state_test_data)); + aws_test311_wait_for_disconnect_to_complete(state_test_data); return AWS_OP_SUCCESS; } @@ -4151,7 +3892,7 @@ static int s_test_mqtt_validation_failure_connect_invalid_client_id_utf8_fn( .client_id = s_bad_client_id_utf8, .host_name = aws_byte_cursor_from_c_str(state_test_data->endpoint.address), .socket_options = &state_test_data->socket_options, - .on_connection_complete = s_on_connection_complete_fn, + .on_connection_complete = aws_test311_on_connection_complete_fn, }; ASSERT_FAILS(aws_mqtt_client_connection_connect(state_test_data->mqtt_connection, &connection_options)); diff --git a/tests/v3/mqtt311_listener_test.c b/tests/v3/mqtt311_listener_test.c new file mode 100644 index 00000000..e908ff9c --- /dev/null +++ b/tests/v3/mqtt311_listener_test.c @@ -0,0 +1,488 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include + +#include + +#include "mqtt311_testing_utils.h" +#include "mqtt_mock_server_handler.h" + +#include + +struct mqtt311_listener_connection_success_record { + bool joined_session; +}; + +struct mqtt311_listener_connection_interrupted_record { + int error_code; +}; + +struct mqtt311_listener_publish_record { + struct aws_byte_buf topic; + struct aws_byte_buf payload; +}; + +struct mqtt311_listener_test_context { + struct aws_allocator *allocator; + + struct aws_mqtt311_listener *listener; + + struct mqtt_connection_state_test *mqtt311_test_context; + int mqtt311_test_context_setup_result; + + struct aws_array_list connection_success_events; + struct aws_array_list connection_interrupted_events; + struct aws_array_list publish_events; + size_t disconnect_event_count; + bool terminated; + + struct aws_mutex lock; + struct aws_condition_variable signal; +}; + +static void s_311_listener_test_on_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + (void)connection; + (void)dup; + (void)qos; + (void)retain; + + struct mqtt311_listener_test_context *context = userdata; + + struct mqtt311_listener_publish_record publish_record; + AWS_ZERO_STRUCT(publish_record); + + aws_byte_buf_init_copy_from_cursor(&publish_record.topic, context->allocator, *topic); + aws_byte_buf_init_copy_from_cursor(&publish_record.payload, context->allocator, *payload); + + aws_mutex_lock(&context->lock); + aws_array_list_push_back(&context->publish_events, &publish_record); + aws_mutex_unlock(&context->lock); + aws_condition_variable_notify_all(&context->signal); +} + +static void s_311_listener_test_on_connection_success( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + (void)connection; + (void)return_code; + + struct mqtt311_listener_test_context *context = userdata; + + struct mqtt311_listener_connection_success_record connection_success_record = { + .joined_session = session_present, + }; + + aws_mutex_lock(&context->lock); + aws_array_list_push_back(&context->connection_success_events, &connection_success_record); + aws_mutex_unlock(&context->lock); + aws_condition_variable_notify_all(&context->signal); +} + +static void s_311_listener_test_on_connection_interrupted( + struct aws_mqtt_client_connection *connection, + int error_code, + void *userdata) { + (void)connection; + + struct mqtt311_listener_test_context *context = userdata; + + struct mqtt311_listener_connection_interrupted_record connection_interrupted_record = { + .error_code = error_code, + }; + + aws_mutex_lock(&context->lock); + aws_array_list_push_back(&context->connection_interrupted_events, &connection_interrupted_record); + aws_mutex_unlock(&context->lock); + aws_condition_variable_notify_all(&context->signal); +} + +static void s_311_listener_test_on_disconnect(struct aws_mqtt_client_connection *connection, void *userdata) { + (void)connection; + + struct mqtt311_listener_test_context *context = userdata; + + aws_mutex_lock(&context->lock); + ++context->disconnect_event_count; + aws_mutex_unlock(&context->lock); + aws_condition_variable_notify_all(&context->signal); +} + +static void s_311_listener_test_on_termination(void *complete_ctx) { + struct mqtt311_listener_test_context *context = complete_ctx; + + aws_mutex_lock(&context->lock); + context->terminated = true; + aws_mutex_unlock(&context->lock); + aws_condition_variable_notify_all(&context->signal); +} + +static int mqtt311_listener_test_context_init( + struct mqtt311_listener_test_context *context, + struct aws_allocator *allocator, + struct mqtt_connection_state_test *mqtt311_test_context) { + AWS_ZERO_STRUCT(*context); + + context->allocator = allocator; + context->mqtt311_test_context = mqtt311_test_context; + + aws_array_list_init_dynamic( + &context->connection_success_events, allocator, 10, sizeof(struct mqtt311_listener_connection_success_record)); + aws_array_list_init_dynamic( + &context->connection_interrupted_events, + allocator, + 10, + sizeof(struct mqtt311_listener_connection_interrupted_record)); + aws_array_list_init_dynamic( + &context->publish_events, allocator, 10, sizeof(struct mqtt311_listener_publish_record)); + + aws_mutex_init(&context->lock); + aws_condition_variable_init(&context->signal); + + context->mqtt311_test_context_setup_result = aws_test311_setup_mqtt_server_fn(allocator, mqtt311_test_context); + ASSERT_SUCCESS(context->mqtt311_test_context_setup_result); + + struct aws_mqtt311_listener_config listener_config = { + .connection = mqtt311_test_context->mqtt_connection, + .listener_callbacks = + { + .publish_received_handler = s_311_listener_test_on_publish_received, + .connection_success_handler = s_311_listener_test_on_connection_success, + .connection_interrupted_handler = s_311_listener_test_on_connection_interrupted, + .disconnect_handler = s_311_listener_test_on_disconnect, + .user_data = context, + }, + .termination_callback = s_311_listener_test_on_termination, + .termination_callback_user_data = context, + }; + + context->listener = aws_mqtt311_listener_new(allocator, &listener_config); + + return AWS_OP_SUCCESS; +} + +static bool s_is_listener_terminated(void *userdata) { + struct mqtt311_listener_test_context *context = userdata; + + return context->terminated; +} + +static void s_wait_for_listener_termination_callback(struct mqtt311_listener_test_context *context) { + aws_mutex_lock(&context->lock); + aws_condition_variable_wait_pred(&context->signal, &context->lock, s_is_listener_terminated, context); + aws_mutex_unlock(&context->lock); +} + +static void mqtt311_listener_test_context_clean_up(struct mqtt311_listener_test_context *context) { + aws_mqtt311_listener_release(context->listener); + s_wait_for_listener_termination_callback(context); + + aws_test311_clean_up_mqtt_server_fn( + context->allocator, context->mqtt311_test_context_setup_result, context->mqtt311_test_context); + + aws_mutex_clean_up(&context->lock); + aws_condition_variable_clean_up(&context->signal); + + aws_array_list_clean_up(&context->connection_success_events); + aws_array_list_clean_up(&context->connection_interrupted_events); + + for (size_t i = 0; i < aws_array_list_length(&context->publish_events); ++i) { + struct mqtt311_listener_publish_record publish_record; + AWS_ZERO_STRUCT(publish_record); + + aws_array_list_get_at(&context->publish_events, &publish_record, i); + + aws_byte_buf_clean_up(&publish_record.topic); + aws_byte_buf_clean_up(&publish_record.payload); + } + + aws_array_list_clean_up(&context->publish_events); +} + +struct connection_success_event_test_context { + struct mqtt311_listener_test_context *context; + bool joined_session; + size_t expected_count; +}; + +static bool s_contains_connection_success_events(void *userdata) { + struct connection_success_event_test_context *wait_context = userdata; + struct mqtt311_listener_test_context *context = wait_context->context; + + size_t found = 0; + for (size_t i = 0; i < aws_array_list_length(&context->connection_success_events); ++i) { + struct mqtt311_listener_connection_success_record record; + aws_array_list_get_at(&context->connection_success_events, &record, i); + + if (record.joined_session == wait_context->joined_session) { + ++found; + } + } + + return found >= wait_context->expected_count; +} + +static void s_wait_for_connection_success_events( + struct mqtt311_listener_test_context *context, + bool joined_session, + size_t expected_count) { + struct connection_success_event_test_context wait_context = { + .context = context, + .joined_session = joined_session, + .expected_count = expected_count, + }; + + aws_mutex_lock(&context->lock); + aws_condition_variable_wait_pred( + &context->signal, &context->lock, s_contains_connection_success_events, &wait_context); + aws_mutex_unlock(&context->lock); +} + +struct connection_interrupted_event_test_context { + struct mqtt311_listener_test_context *context; + int error_code; + size_t expected_count; +}; + +static bool s_contains_connection_interrupted_events(void *userdata) { + struct connection_interrupted_event_test_context *wait_context = userdata; + struct mqtt311_listener_test_context *context = wait_context->context; + + size_t found = 0; + for (size_t i = 0; i < aws_array_list_length(&context->connection_interrupted_events); ++i) { + struct mqtt311_listener_connection_interrupted_record record; + aws_array_list_get_at(&context->connection_interrupted_events, &record, i); + + if (record.error_code != wait_context->error_code) { + continue; + } + + ++found; + } + + return found >= wait_context->expected_count; +} + +static void s_wait_for_connection_interrupted_events( + struct mqtt311_listener_test_context *context, + int error_code, + size_t expected_count) { + struct connection_interrupted_event_test_context wait_context = { + .context = context, + .error_code = error_code, + .expected_count = expected_count, + }; + + aws_mutex_lock(&context->lock); + aws_condition_variable_wait_pred( + &context->signal, &context->lock, s_contains_connection_interrupted_events, &wait_context); + aws_mutex_unlock(&context->lock); +} + +struct disconnect_event_test_context { + struct mqtt311_listener_test_context *context; + size_t expected_count; +}; + +static bool s_contains_disconnect_events(void *userdata) { + struct disconnect_event_test_context *wait_context = userdata; + + return wait_context->context->disconnect_event_count >= wait_context->expected_count; +} + +static void s_wait_for_disconnect_events(struct mqtt311_listener_test_context *context, size_t expected_count) { + struct disconnect_event_test_context wait_context = { + .context = context, + .expected_count = expected_count, + }; + + aws_mutex_lock(&context->lock); + aws_condition_variable_wait_pred(&context->signal, &context->lock, s_contains_disconnect_events, &wait_context); + aws_mutex_unlock(&context->lock); +} + +static int s_do_mqtt311_listener_connection_events_test(struct aws_allocator *allocator, bool session_present) { + aws_mqtt_library_init(allocator); + + struct mqtt_connection_state_test mqtt311_context; + AWS_ZERO_STRUCT(mqtt311_context); + + struct mqtt311_listener_test_context test_context; + ASSERT_SUCCESS(mqtt311_listener_test_context_init(&test_context, allocator, &mqtt311_context)); + + mqtt_mock_server_set_session_present(mqtt311_context.mock_server, session_present); + + struct aws_mqtt_connection_options connection_options = { + .user_data = &mqtt311_context, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(mqtt311_context.endpoint.address), + .socket_options = &mqtt311_context.socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + .keep_alive_time_secs = DEFAULT_TEST_KEEP_ALIVE_S, + .ping_timeout_ms = DEFAULT_TEST_PING_TIMEOUT_MS, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(mqtt311_context.mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(&mqtt311_context); + + s_wait_for_connection_success_events(&test_context, session_present, 1); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + mqtt311_context.mqtt_connection, aws_test311_on_disconnect_fn, &mqtt311_context)); + aws_test311_wait_for_disconnect_to_complete(&mqtt311_context); + + s_wait_for_disconnect_events(&test_context, 1); + + mqtt_mock_server_set_max_ping_resp(mqtt311_context.mock_server, 0); + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(mqtt311_context.mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(&mqtt311_context); + + // the ping configuration leads to an interruption event after 3 seconds due to ping timeout + s_wait_for_connection_interrupted_events(&test_context, AWS_ERROR_MQTT_TIMEOUT, 1); + + aws_test311_wait_for_reconnect_to_complete(&mqtt311_context); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + mqtt311_context.mqtt_connection, aws_test311_on_disconnect_fn, &mqtt311_context)); + aws_test311_wait_for_disconnect_to_complete(&mqtt311_context); + + s_wait_for_disconnect_events(&test_context, 2); + + mqtt311_listener_test_context_clean_up(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +static int s_mqtt311_listener_connection_events_no_session_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_mqtt311_listener_connection_events_test(allocator, false); +} + +AWS_TEST_CASE(mqtt311_listener_connection_events_no_session, s_mqtt311_listener_connection_events_no_session_fn) + +static int s_mqtt311_listener_connection_events_with_session_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + return s_do_mqtt311_listener_connection_events_test(allocator, true); +} + +AWS_TEST_CASE(mqtt311_listener_connection_events_with_session, s_mqtt311_listener_connection_events_with_session_fn) + +struct publish_event_test_context { + struct mqtt311_listener_test_context *context; + struct aws_byte_cursor expected_topic; + struct aws_byte_cursor expected_payload; + size_t expected_count; +}; + +static bool s_contains_publish_events(void *userdata) { + struct publish_event_test_context *wait_context = userdata; + struct mqtt311_listener_test_context *context = wait_context->context; + + size_t found = 0; + for (size_t i = 0; i < aws_array_list_length(&context->publish_events); ++i) { + struct mqtt311_listener_publish_record record; + aws_array_list_get_at(&context->publish_events, &record, i); + + struct aws_byte_cursor actual_topic = aws_byte_cursor_from_buf(&record.topic); + if (!aws_byte_cursor_eq(&wait_context->expected_topic, &actual_topic)) { + continue; + } + + struct aws_byte_cursor actual_payload = aws_byte_cursor_from_buf(&record.payload); + if (!aws_byte_cursor_eq(&wait_context->expected_payload, &actual_payload)) { + continue; + } + + ++found; + } + + return found >= wait_context->expected_count; +} + +static void s_wait_for_publish_events( + struct mqtt311_listener_test_context *context, + struct aws_byte_cursor topic, + struct aws_byte_cursor payload, + size_t expected_count) { + struct publish_event_test_context wait_context = { + .context = context, + .expected_topic = topic, + .expected_payload = payload, + .expected_count = expected_count, + }; + + aws_mutex_lock(&context->lock); + aws_condition_variable_wait_pred(&context->signal, &context->lock, s_contains_publish_events, &wait_context); + aws_mutex_unlock(&context->lock); +} + +static int s_mqtt311_listener_publish_event_fn(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + aws_mqtt_library_init(allocator); + + struct mqtt_connection_state_test mqtt311_context; + AWS_ZERO_STRUCT(mqtt311_context); + + struct mqtt311_listener_test_context test_context; + ASSERT_SUCCESS(mqtt311_listener_test_context_init(&test_context, allocator, &mqtt311_context)); + + mqtt_mock_server_set_publish_reflection(mqtt311_context.mock_server, true); + + struct aws_mqtt_connection_options connection_options = { + .user_data = &mqtt311_context, + .clean_session = true, + .client_id = aws_byte_cursor_from_c_str("client1234"), + .host_name = aws_byte_cursor_from_c_str(mqtt311_context.endpoint.address), + .socket_options = &mqtt311_context.socket_options, + .on_connection_complete = aws_test311_on_connection_complete_fn, + }; + + ASSERT_SUCCESS(aws_mqtt_client_connection_connect(mqtt311_context.mqtt_connection, &connection_options)); + aws_test311_wait_for_connection_to_complete(&mqtt311_context); + + struct aws_byte_cursor topic1 = aws_byte_cursor_from_c_str("hello/world/1"); + struct aws_byte_cursor payload1 = aws_byte_cursor_from_c_str("payload1"); + aws_mqtt_client_connection_publish( + mqtt311_context.mqtt_connection, &topic1, AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload1, NULL, NULL); + + s_wait_for_publish_events(&test_context, topic1, payload1, 1); + + struct aws_byte_cursor topic2 = aws_byte_cursor_from_c_str("nothing/important"); + struct aws_byte_cursor payload2 = aws_byte_cursor_from_c_str("somethingneeddoing?"); + aws_mqtt_client_connection_publish( + mqtt311_context.mqtt_connection, &topic2, AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload2, NULL, NULL); + aws_mqtt_client_connection_publish( + mqtt311_context.mqtt_connection, &topic2, AWS_MQTT_QOS_AT_LEAST_ONCE, false, &payload2, NULL, NULL); + + s_wait_for_publish_events(&test_context, topic2, payload2, 2); + + ASSERT_SUCCESS(aws_mqtt_client_connection_disconnect( + mqtt311_context.mqtt_connection, aws_test311_on_disconnect_fn, &mqtt311_context)); + aws_test311_wait_for_disconnect_to_complete(&mqtt311_context); + + mqtt311_listener_test_context_clean_up(&test_context); + + aws_mqtt_library_clean_up(); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt311_listener_publish_event, s_mqtt311_listener_publish_event_fn) \ No newline at end of file diff --git a/tests/v3/mqtt311_testing_utils.c b/tests/v3/mqtt311_testing_utils.c new file mode 100644 index 00000000..f6019c0e --- /dev/null +++ b/tests/v3/mqtt311_testing_utils.c @@ -0,0 +1,582 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include "mqtt311_testing_utils.h" + +#include +#include +#include +#include +#include +#include + +#include "mqtt_mock_server_handler.h" + +static void s_on_incoming_channel_setup_fn( + struct aws_server_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + (void)bootstrap; + struct mqtt_connection_state_test *state_test_data = user_data; + + state_test_data->error = error_code; + + if (!error_code) { + aws_mutex_lock(&state_test_data->lock); + state_test_data->server_disconnect_completed = false; + aws_mutex_unlock(&state_test_data->lock); + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "server channel setup completed"); + + state_test_data->server_channel = channel; + struct aws_channel_slot *test_handler_slot = aws_channel_slot_new(channel); + aws_channel_slot_insert_end(channel, test_handler_slot); + mqtt_mock_server_handler_update_slot(state_test_data->mock_server, test_handler_slot); + aws_channel_slot_set_handler(test_handler_slot, state_test_data->mock_server); + } +} + +static void s_on_incoming_channel_shutdown_fn( + struct aws_server_bootstrap *bootstrap, + int error_code, + struct aws_channel *channel, + void *user_data) { + (void)bootstrap; + (void)error_code; + (void)channel; + struct mqtt_connection_state_test *state_test_data = user_data; + aws_mutex_lock(&state_test_data->lock); + state_test_data->server_disconnect_completed = true; + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "server channel shutdown completed"); + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_on_listener_destroy(struct aws_server_bootstrap *bootstrap, void *user_data) { + (void)bootstrap; + struct mqtt_connection_state_test *state_test_data = user_data; + aws_mutex_lock(&state_test_data->lock); + state_test_data->listener_destroyed = true; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_listener_destroyed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->listener_destroyed; +} + +static void s_wait_on_listener_cleanup(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_listener_destroyed, state_test_data); + aws_mutex_unlock(&state_test_data->lock); +} + +static void s_on_connection_interrupted(struct aws_mqtt_client_connection *connection, int error_code, void *userdata) { + (void)connection; + (void)error_code; + struct mqtt_connection_state_test *state_test_data = userdata; + + aws_mutex_lock(&state_test_data->lock); + state_test_data->connection_interrupted = true; + state_test_data->interruption_error = error_code; + aws_mutex_unlock(&state_test_data->lock); + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "connection interrupted"); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_connection_interrupted(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_interrupted; +} + +void aws_test311_wait_for_interrupt_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_connection_interrupted, state_test_data); + state_test_data->connection_interrupted = false; + aws_mutex_unlock(&state_test_data->lock); +} + +static void s_on_connection_resumed( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + (void)connection; + (void)return_code; + (void)session_present; + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "reconnect completed"); + + struct mqtt_connection_state_test *state_test_data = userdata; + + aws_mutex_lock(&state_test_data->lock); + state_test_data->connection_resumed = true; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_connection_resumed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_resumed; +} + +void aws_test311_wait_for_reconnect_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_connection_resumed, state_test_data); + state_test_data->connection_resumed = false; + aws_mutex_unlock(&state_test_data->lock); +} + +static void s_on_connection_success( + struct aws_mqtt_client_connection *connection, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + aws_mutex_lock(&state_test_data->lock); + + state_test_data->session_present = session_present; + state_test_data->mqtt_return_code = return_code; + state_test_data->connection_success = true; + aws_mutex_unlock(&state_test_data->lock); + + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_on_connection_failure(struct aws_mqtt_client_connection *connection, int error_code, void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + aws_mutex_lock(&state_test_data->lock); + + state_test_data->error = error_code; + state_test_data->connection_failure = true; + aws_mutex_unlock(&state_test_data->lock); + + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_connection_succeed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_success; +} + +static bool s_is_connection_failed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_failure; +} + +void aws_test311_wait_for_connection_to_succeed(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_connection_succeed, state_test_data); + state_test_data->connection_success = false; + aws_mutex_unlock(&state_test_data->lock); +} + +void aws_test311_wait_for_connection_to_fail(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_connection_failed, state_test_data); + state_test_data->connection_failure = false; + aws_mutex_unlock(&state_test_data->lock); +} + +static bool s_is_termination_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_terminated; +} + +static void s_wait_for_termination_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_termination_completed, state_test_data); + state_test_data->connection_terminated = false; + aws_mutex_unlock(&state_test_data->lock); +} + +void aws_test311_on_connection_termination_fn(void *userdata) { + struct mqtt_connection_state_test *state_test_data = (struct mqtt_connection_state_test *)userdata; + + aws_mutex_lock(&state_test_data->lock); + state_test_data->connection_termination_calls += 1; + state_test_data->connection_terminated = true; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static void s_on_any_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + + struct aws_byte_buf payload_cp; + aws_byte_buf_init_copy_from_cursor(&payload_cp, state_test_data->allocator, *payload); + struct aws_byte_buf topic_cp; + aws_byte_buf_init_copy_from_cursor(&topic_cp, state_test_data->allocator, *topic); + struct received_publish_packet received_packet = { + .payload = payload_cp, + .topic = topic_cp, + .dup = dup, + .qos = qos, + .retain = retain, + }; + + aws_mutex_lock(&state_test_data->lock); + aws_array_list_push_back(&state_test_data->any_published_messages, &received_packet); + state_test_data->any_publishes_received++; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +/** + * sets up a unix domain socket server and socket options. Creates an mqtt connection configured to use + * the domain socket. + */ +int aws_test311_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx) { + aws_mqtt_library_init(allocator); + + struct mqtt_connection_state_test *state_test_data = ctx; + + AWS_ZERO_STRUCT(*state_test_data); + + state_test_data->allocator = allocator; + state_test_data->el_group = aws_event_loop_group_new_default(allocator, 1, NULL); + + state_test_data->mock_server = new_mqtt_mock_server(allocator); + ASSERT_NOT_NULL(state_test_data->mock_server); + + state_test_data->server_bootstrap = aws_server_bootstrap_new(allocator, state_test_data->el_group); + ASSERT_NOT_NULL(state_test_data->server_bootstrap); + + struct aws_socket_options socket_options = { + .connect_timeout_ms = 100, + .domain = AWS_SOCKET_LOCAL, + }; + + state_test_data->socket_options = socket_options; + ASSERT_SUCCESS(aws_condition_variable_init(&state_test_data->cvar)); + ASSERT_SUCCESS(aws_mutex_init(&state_test_data->lock)); + + aws_socket_endpoint_init_local_address_for_test(&state_test_data->endpoint); + + struct aws_server_socket_channel_bootstrap_options server_bootstrap_options = { + .bootstrap = state_test_data->server_bootstrap, + .host_name = state_test_data->endpoint.address, + .port = state_test_data->endpoint.port, + .socket_options = &state_test_data->socket_options, + .incoming_callback = s_on_incoming_channel_setup_fn, + .shutdown_callback = s_on_incoming_channel_shutdown_fn, + .destroy_callback = s_on_listener_destroy, + .user_data = state_test_data, + }; + state_test_data->listener = aws_server_bootstrap_new_socket_listener(&server_bootstrap_options); + + ASSERT_NOT_NULL(state_test_data->listener); + + struct aws_host_resolver_default_options resolver_options = { + .el_group = state_test_data->el_group, + .max_entries = 1, + }; + state_test_data->host_resolver = aws_host_resolver_new_default(allocator, &resolver_options); + + struct aws_client_bootstrap_options bootstrap_options = { + .event_loop_group = state_test_data->el_group, + .user_data = state_test_data, + .host_resolver = state_test_data->host_resolver, + }; + + state_test_data->client_bootstrap = aws_client_bootstrap_new(allocator, &bootstrap_options); + + state_test_data->mqtt_client = aws_mqtt_client_new(allocator, state_test_data->client_bootstrap); + state_test_data->mqtt_connection = aws_mqtt_client_connection_new(state_test_data->mqtt_client); + ASSERT_NOT_NULL(state_test_data->mqtt_connection); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_interruption_handlers( + state_test_data->mqtt_connection, + s_on_connection_interrupted, + state_test_data, + s_on_connection_resumed, + state_test_data)); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_result_handlers( + state_test_data->mqtt_connection, + s_on_connection_success, + state_test_data, + s_on_connection_failure, + state_test_data)); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_on_any_publish_handler( + state_test_data->mqtt_connection, s_on_any_publish_received, state_test_data)); + + ASSERT_SUCCESS(aws_array_list_init_dynamic( + &state_test_data->published_messages, allocator, 4, sizeof(struct received_publish_packet))); + ASSERT_SUCCESS(aws_array_list_init_dynamic( + &state_test_data->any_published_messages, allocator, 4, sizeof(struct received_publish_packet))); + ASSERT_SUCCESS(aws_array_list_init_dynamic(&state_test_data->qos_returned, allocator, 2, sizeof(uint8_t))); + + ASSERT_SUCCESS(aws_mqtt_client_connection_set_connection_termination_handler( + state_test_data->mqtt_connection, aws_test311_on_connection_termination_fn, state_test_data)); + + return AWS_OP_SUCCESS; +} + +static void s_received_publish_packet_list_clean_up(struct aws_array_list *list) { + for (size_t i = 0; i < aws_array_list_length(list); ++i) { + struct received_publish_packet *val_ptr = NULL; + aws_array_list_get_at_ptr(list, (void **)&val_ptr, i); + aws_byte_buf_clean_up(&val_ptr->payload); + aws_byte_buf_clean_up(&val_ptr->topic); + } + aws_array_list_clean_up(list); +} + +int aws_test311_clean_up_mqtt_server_fn(struct aws_allocator *allocator, int setup_result, void *ctx) { + (void)allocator; + + if (!setup_result) { + struct mqtt_connection_state_test *state_test_data = ctx; + + s_received_publish_packet_list_clean_up(&state_test_data->published_messages); + s_received_publish_packet_list_clean_up(&state_test_data->any_published_messages); + aws_array_list_clean_up(&state_test_data->qos_returned); + aws_mqtt_client_connection_release(state_test_data->mqtt_connection); + + s_wait_for_termination_to_complete(state_test_data); + ASSERT_UINT_EQUALS(1, state_test_data->connection_termination_calls); + + aws_mqtt_client_release(state_test_data->mqtt_client); + aws_client_bootstrap_release(state_test_data->client_bootstrap); + aws_host_resolver_release(state_test_data->host_resolver); + aws_server_bootstrap_destroy_socket_listener(state_test_data->server_bootstrap, state_test_data->listener); + s_wait_on_listener_cleanup(state_test_data); + aws_server_bootstrap_release(state_test_data->server_bootstrap); + aws_event_loop_group_release(state_test_data->el_group); + destroy_mqtt_mock_server(state_test_data->mock_server); + } + + aws_mqtt_library_clean_up(); + return AWS_OP_SUCCESS; +} + +void aws_test311_on_connection_complete_fn( + struct aws_mqtt_client_connection *connection, + int error_code, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + aws_mutex_lock(&state_test_data->lock); + + state_test_data->session_present = session_present; + state_test_data->mqtt_return_code = return_code; + state_test_data->error = error_code; + state_test_data->connection_completed = true; + aws_mutex_unlock(&state_test_data->lock); + + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_connection_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->connection_completed; +} + +void aws_test311_wait_for_connection_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_connection_completed, state_test_data); + state_test_data->connection_completed = false; + aws_mutex_unlock(&state_test_data->lock); +} + +void aws_test311_on_disconnect_fn(struct aws_mqtt_client_connection *connection, void *userdata) { + (void)connection; + struct mqtt_connection_state_test *state_test_data = userdata; + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "disconnect completed"); + aws_mutex_lock(&state_test_data->lock); + state_test_data->client_disconnect_completed = true; + aws_mutex_unlock(&state_test_data->lock); + + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_disconnect_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->client_disconnect_completed && state_test_data->server_disconnect_completed; +} + +void aws_test311_wait_for_disconnect_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_disconnect_completed, state_test_data); + state_test_data->client_disconnect_completed = false; + state_test_data->server_disconnect_completed = false; + aws_mutex_unlock(&state_test_data->lock); +} + +static bool s_is_any_publish_received(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->any_publishes_received == state_test_data->expected_any_publishes; +} + +void aws_test311_wait_for_any_publish(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_any_publish_received, state_test_data); + state_test_data->any_publishes_received = 0; + state_test_data->expected_any_publishes = 0; + aws_mutex_unlock(&state_test_data->lock); +} + +void aws_test311_on_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata) { + + (void)connection; + (void)topic; + struct mqtt_connection_state_test *state_test_data = userdata; + + struct aws_byte_buf payload_cp; + aws_byte_buf_init_copy_from_cursor(&payload_cp, state_test_data->allocator, *payload); + struct aws_byte_buf topic_cp; + aws_byte_buf_init_copy_from_cursor(&topic_cp, state_test_data->allocator, *topic); + struct received_publish_packet received_packet = { + .payload = payload_cp, + .topic = topic_cp, + .dup = dup, + .qos = qos, + .retain = retain, + }; + + aws_mutex_lock(&state_test_data->lock); + aws_array_list_push_back(&state_test_data->published_messages, &received_packet); + state_test_data->publishes_received++; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_publish_received(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->publishes_received == state_test_data->expected_publishes; +} + +void aws_test311_wait_for_publish(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_publish_received, state_test_data); + state_test_data->publishes_received = 0; + state_test_data->expected_publishes = 0; + aws_mutex_unlock(&state_test_data->lock); +} + +void aws_test311_on_suback( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + int error_code, + void *userdata) { + (void)connection; + (void)packet_id; + (void)topic; + + struct mqtt_connection_state_test *state_test_data = userdata; + + aws_mutex_lock(&state_test_data->lock); + if (!error_code) { + aws_array_list_push_back(&state_test_data->qos_returned, &qos); + } + state_test_data->subscribe_completed = true; + state_test_data->subscribe_complete_error = error_code; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_subscribe_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->subscribe_completed; +} + +void aws_test311_wait_for_subscribe_to_complete(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_pred( + &state_test_data->cvar, &state_test_data->lock, s_is_subscribe_completed, state_test_data); + state_test_data->subscribe_completed = false; + aws_mutex_unlock(&state_test_data->lock); +} + +void aws_test311_on_multi_suback( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + const struct aws_array_list *topic_subacks, /* contains aws_mqtt_topic_subscription pointers */ + int error_code, + void *userdata) { + (void)connection; + (void)packet_id; + (void)topic_subacks; + (void)error_code; + + struct mqtt_connection_state_test *state_test_data = userdata; + + aws_mutex_lock(&state_test_data->lock); + state_test_data->subscribe_completed = true; + state_test_data->subscribe_complete_error = error_code; + + if (!error_code) { + size_t length = aws_array_list_length(topic_subacks); + for (size_t i = 0; i < length; ++i) { + struct aws_mqtt_topic_subscription *subscription = NULL; + aws_array_list_get_at(topic_subacks, &subscription, i); + aws_array_list_push_back(&state_test_data->qos_returned, &subscription->qos); + } + } + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +void aws_test311_on_op_complete( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + int error_code, + void *userdata) { + (void)connection; + (void)packet_id; + + struct mqtt_connection_state_test *state_test_data = userdata; + AWS_LOGF_DEBUG(TEST_LOG_SUBJECT, "pub op completed"); + aws_mutex_lock(&state_test_data->lock); + state_test_data->ops_completed++; + state_test_data->op_complete_error = error_code; + aws_mutex_unlock(&state_test_data->lock); + aws_condition_variable_notify_one(&state_test_data->cvar); +} + +static bool s_is_ops_completed(void *arg) { + struct mqtt_connection_state_test *state_test_data = arg; + return state_test_data->ops_completed == state_test_data->expected_ops_completed; +} + +void aws_test311_wait_for_ops_completed(struct mqtt_connection_state_test *state_test_data) { + aws_mutex_lock(&state_test_data->lock); + aws_condition_variable_wait_for_pred( + &state_test_data->cvar, &state_test_data->lock, 10000000000, s_is_ops_completed, state_test_data); + aws_mutex_unlock(&state_test_data->lock); +} \ No newline at end of file diff --git a/tests/v3/mqtt311_testing_utils.h b/tests/v3/mqtt311_testing_utils.h new file mode 100644 index 00000000..e9162c20 --- /dev/null +++ b/tests/v3/mqtt311_testing_utils.h @@ -0,0 +1,155 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#ifndef AWS_C_MQTT_MQTT311_TESTING_UTILS_H +#define AWS_C_MQTT_MQTT311_TESTING_UTILS_H + +#include + +#include +#include +#include +#include +#include + +#include + +#define TEST_LOG_SUBJECT 60000 +#define ONE_SEC 1000000000 +// The value is extract from aws-c-mqtt/source/client.c +#define AWS_RESET_RECONNECT_BACKOFF_DELAY_SECONDS 10 +#define RECONNECT_BACKOFF_DELAY_ERROR_MARGIN_NANO_SECONDS 500000000 +#define DEFAULT_MIN_RECONNECT_DELAY_SECONDS 1 + +#define DEFAULT_TEST_PING_TIMEOUT_MS 1000 +#define DEFAULT_TEST_KEEP_ALIVE_S 2 + +struct received_publish_packet { + struct aws_byte_buf topic; + struct aws_byte_buf payload; + bool dup; + enum aws_mqtt_qos qos; + bool retain; +}; + +struct mqtt_connection_state_test { + struct aws_allocator *allocator; + struct aws_channel *server_channel; + struct aws_channel_handler *mock_server; + struct aws_client_bootstrap *client_bootstrap; + struct aws_server_bootstrap *server_bootstrap; + struct aws_event_loop_group *el_group; + struct aws_host_resolver *host_resolver; + struct aws_socket_endpoint endpoint; + struct aws_socket *listener; + struct aws_mqtt_client *mqtt_client; + struct aws_mqtt_client_connection *mqtt_connection; + struct aws_socket_options socket_options; + + bool session_present; + bool connection_completed; + bool connection_success; + bool connection_failure; + bool client_disconnect_completed; + bool server_disconnect_completed; + bool connection_interrupted; + bool connection_resumed; + bool subscribe_completed; + bool listener_destroyed; + bool connection_terminated; + int interruption_error; + int subscribe_complete_error; + int op_complete_error; + enum aws_mqtt_connect_return_code mqtt_return_code; + int error; + struct aws_condition_variable cvar; + struct aws_mutex lock; + /* any published messages from mock server, that you may not subscribe to. (Which should not happen in real life) */ + struct aws_array_list any_published_messages; /* list of struct received_publish_packet */ + size_t any_publishes_received; + size_t expected_any_publishes; + /* the published messages from mock server, that you did subscribe to. */ + struct aws_array_list published_messages; /* list of struct received_publish_packet */ + size_t publishes_received; + size_t expected_publishes; + /* The returned QoS from mock server */ + struct aws_array_list qos_returned; /* list of uint_8 */ + size_t ops_completed; + size_t expected_ops_completed; + size_t connection_close_calls; /* All of the times on_connection_closed has been called */ + + size_t connection_termination_calls; /* How many times on_connection_termination has been called, should be 1 */ +}; + +AWS_EXTERN_C_BEGIN + +int aws_test311_setup_mqtt_server_fn(struct aws_allocator *allocator, void *ctx); + +int aws_test311_clean_up_mqtt_server_fn(struct aws_allocator *allocator, int setup_result, void *ctx); + +void aws_test311_wait_for_interrupt_to_complete(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_wait_for_reconnect_to_complete(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_wait_for_connection_to_succeed(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_wait_for_connection_to_fail(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_on_connection_complete_fn( + struct aws_mqtt_client_connection *connection, + int error_code, + enum aws_mqtt_connect_return_code return_code, + bool session_present, + void *userdata); + +void aws_test311_wait_for_connection_to_complete(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_on_disconnect_fn(struct aws_mqtt_client_connection *connection, void *userdata); + +void aws_test311_wait_for_disconnect_to_complete(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_wait_for_any_publish(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_on_publish_received( + struct aws_mqtt_client_connection *connection, + const struct aws_byte_cursor *topic, + const struct aws_byte_cursor *payload, + bool dup, + enum aws_mqtt_qos qos, + bool retain, + void *userdata); + +void aws_test311_wait_for_publish(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_on_suback( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + const struct aws_byte_cursor *topic, + enum aws_mqtt_qos qos, + int error_code, + void *userdata); + +void aws_test311_wait_for_subscribe_to_complete(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_on_multi_suback( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + const struct aws_array_list *topic_subacks, + int error_code, + void *userdata); + +void aws_test311_on_op_complete( + struct aws_mqtt_client_connection *connection, + uint16_t packet_id, + int error_code, + void *userdata); + +void aws_test311_wait_for_ops_completed(struct mqtt_connection_state_test *state_test_data); + +void aws_test311_on_connection_termination_fn(void *userdata); + +AWS_EXTERN_C_END + +#endif // AWS_C_MQTT_MQTT311_TESTING_UTILS_H diff --git a/tests/v3/mqtt_mock_server_handler.c b/tests/v3/mqtt_mock_server_handler.c index 73576a7d..8bee4cd6 100644 --- a/tests/v3/mqtt_mock_server_handler.c +++ b/tests/v3/mqtt_mock_server_handler.c @@ -30,7 +30,10 @@ struct mqtt_mock_server_handler { size_t pubacks_received; size_t ping_received; size_t connacks_avail; + bool session_present; + bool reflect_publishes; bool auto_ack; + uint8_t suback_reason_code; /* last ID used when sending PUBLISH (QoS1+) to client */ uint16_t last_packet_id; @@ -77,12 +80,14 @@ static int s_mqtt_mock_server_handler_process_packet( switch (packet_type) { case AWS_MQTT_PACKET_CONNECT: { size_t connacks_available = 0; + bool session_present = false; aws_mutex_lock(&server->synced.lock); AWS_LOGF_DEBUG( MOCK_LOG_SUBJECT, "server, CONNECT received, %llu available connacks.", (long long unsigned)server->synced.connacks_avail); connacks_available = server->synced.connacks_avail > 0 ? server->synced.connacks_avail-- : 0; + session_present = server->synced.session_present; aws_mutex_unlock(&server->synced.lock); if (connacks_available) { @@ -90,7 +95,7 @@ static int s_mqtt_mock_server_handler_process_packet( aws_channel_acquire_message_from_pool(server->slot->channel, AWS_IO_MESSAGE_APPLICATION_DATA, 256); struct aws_mqtt_packet_connack conn_ack; - err |= aws_mqtt_packet_connack_init(&conn_ack, false, AWS_MQTT_CONNECT_ACCEPTED); + err |= aws_mqtt_packet_connack_init(&conn_ack, session_present, AWS_MQTT_CONNECT_ACCEPTED); err |= aws_mqtt_packet_connack_encode(&connack_msg->message_data, &conn_ack); if (aws_channel_slot_send_message(server->slot, connack_msg, AWS_CHANNEL_DIR_WRITE)) { err |= 1; @@ -135,6 +140,7 @@ static int s_mqtt_mock_server_handler_process_packet( aws_mutex_lock(&server->synced.lock); bool auto_ack = server->synced.auto_ack; + uint8_t reason_code = server->synced.suback_reason_code; aws_mutex_unlock(&server->synced.lock); if (auto_ack) { @@ -144,7 +150,7 @@ static int s_mqtt_mock_server_handler_process_packet( err |= aws_mqtt_packet_suback_init(&suback, server->handler.alloc, subscribe_packet.packet_identifier); const size_t num_filters = aws_array_list_length(&subscribe_packet.topic_filters); for (size_t i = 0; i < num_filters; ++i) { - err |= aws_mqtt_packet_suback_add_return_code(&suback, AWS_MQTT_QOS_EXACTLY_ONCE); + err |= aws_mqtt_packet_suback_add_return_code(&suback, reason_code); } err |= aws_mqtt_packet_suback_encode(&suback_msg->message_data, &suback); err |= aws_channel_slot_send_message(server->slot, suback_msg, AWS_CHANNEL_DIR_WRITE); @@ -185,6 +191,7 @@ static int s_mqtt_mock_server_handler_process_packet( aws_mutex_lock(&server->synced.lock); bool auto_ack = server->synced.auto_ack; + bool reflect_publishes = server->synced.reflect_publishes; aws_mutex_unlock(&server->synced.lock); uint8_t qos = (publish_packet.fixed_header.flags >> 1) & 0x3; @@ -197,6 +204,23 @@ static int s_mqtt_mock_server_handler_process_packet( err |= aws_mqtt_packet_ack_encode(&puback_msg->message_data, &puback); err |= aws_channel_slot_send_message(server->slot, puback_msg, AWS_CHANNEL_DIR_WRITE); } + + if (reflect_publishes) { + struct aws_io_message *publish_msg = + aws_channel_acquire_message_from_pool(server->slot->channel, AWS_IO_MESSAGE_APPLICATION_DATA, 1024); + struct aws_mqtt_packet_publish publish; + // reusing the packet identifier here is weird but reasonably safe, they're separate id spaces + err |= aws_mqtt_packet_publish_init( + &publish, + false, + qos, + false, + publish_packet.topic_name, + publish_packet.packet_identifier, + publish_packet.payload); + err |= aws_mqtt_packet_publish_encode(&publish_msg->message_data, &publish); + err |= aws_channel_slot_send_message(server->slot, publish_msg, AWS_CHANNEL_DIR_WRITE); + } break; } @@ -438,6 +462,7 @@ struct aws_channel_handler *new_mqtt_mock_server(struct aws_allocator *allocator server->synced.ping_resp_avail = SIZE_MAX; server->synced.connacks_avail = SIZE_MAX; server->synced.auto_ack = true; + server->synced.suback_reason_code = AWS_MQTT_QOS_EXACTLY_ONCE; aws_mutex_init(&server->synced.lock); aws_condition_variable_init(&server->synced.cvar); @@ -466,6 +491,22 @@ void destroy_mqtt_mock_server(struct aws_channel_handler *handler) { aws_mem_release(handler->alloc, server); } +void mqtt_mock_server_set_session_present(struct aws_channel_handler *handler, bool session_present) { + struct mqtt_mock_server_handler *server = handler->impl; + + aws_mutex_lock(&server->synced.lock); + server->synced.session_present = session_present; + aws_mutex_unlock(&server->synced.lock); +} + +void mqtt_mock_server_set_publish_reflection(struct aws_channel_handler *handler, bool reflect_publishes) { + struct mqtt_mock_server_handler *server = handler->impl; + + aws_mutex_lock(&server->synced.lock); + server->synced.reflect_publishes = reflect_publishes; + aws_mutex_unlock(&server->synced.lock); +} + void mqtt_mock_server_set_max_ping_resp(struct aws_channel_handler *handler, size_t max_ping) { struct mqtt_mock_server_handler *server = handler->impl; @@ -498,6 +539,14 @@ void mqtt_mock_server_enable_auto_ack(struct aws_channel_handler *handler) { aws_mutex_unlock(&server->synced.lock); } +void mqtt_mock_server_suback_reason_code(struct aws_channel_handler *handler, uint8_t reason_code) { + struct mqtt_mock_server_handler *server = handler->impl; + + aws_mutex_lock(&server->synced.lock); + server->synced.suback_reason_code = reason_code; + aws_mutex_unlock(&server->synced.lock); +} + struct mqtt_mock_server_ack_args { struct aws_channel_task task; struct aws_mqtt_packet_ack ack; diff --git a/tests/v3/mqtt_mock_server_handler.h b/tests/v3/mqtt_mock_server_handler.h index 170201e6..70306311 100644 --- a/tests/v3/mqtt_mock_server_handler.h +++ b/tests/v3/mqtt_mock_server_handler.h @@ -68,10 +68,21 @@ int mqtt_mock_server_send_publish_by_id( enum aws_mqtt_qos qos, bool retain); +/** + * Sets whether or not connacks return session present + */ +void mqtt_mock_server_set_session_present(struct aws_channel_handler *handler, bool session_present); + +/** + * Sets whether or not connacks return session present + */ +void mqtt_mock_server_set_publish_reflection(struct aws_channel_handler *handler, bool reflect_publishes); + /** * Set max number of PINGRESP that mock server will send back to client */ void mqtt_mock_server_set_max_ping_resp(struct aws_channel_handler *handler, size_t max_ping); + /** * Set max number of CONACK that mock server will send back to client */ @@ -81,10 +92,17 @@ void mqtt_mock_server_set_max_connack(struct aws_channel_handler *handler, size_ * Disable the automatically response (suback/unsuback/puback) to the client */ void mqtt_mock_server_disable_auto_ack(struct aws_channel_handler *handler); + /** * Enable the automatically response (suback/unsuback/puback) to the client */ void mqtt_mock_server_enable_auto_ack(struct aws_channel_handler *handler); + +/** + * Sets what reason code to return in subacks + */ +void mqtt_mock_server_suback_reason_code(struct aws_channel_handler *handler, uint8_t reason_code); + /** * Send response back the client given the packet ID */ diff --git a/tests/v5/mqtt5_client_tests.c b/tests/v5/mqtt5_client_tests.c index fe869ecc..943a52d1 100644 --- a/tests/v5/mqtt5_client_tests.c +++ b/tests/v5/mqtt5_client_tests.c @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -18,40 +17,13 @@ #include -#include #include -#define TEST_IO_MESSAGE_LENGTH 4096 - static bool s_is_within_percentage_of(uint64_t expected_time, uint64_t actual_time, double percentage) { double actual_percent = 1.0 - (double)actual_time / (double)expected_time; return fabs(actual_percent) <= percentage; } -int aws_mqtt5_mock_server_send_packet( - struct aws_mqtt5_server_mock_connection_context *connection, - enum aws_mqtt5_packet_type packet_type, - void *packet) { - aws_mqtt5_encoder_append_packet_encoding(&connection->encoder, packet_type, packet); - - struct aws_io_message *message = aws_channel_acquire_message_from_pool( - connection->slot->channel, AWS_IO_MESSAGE_APPLICATION_DATA, TEST_IO_MESSAGE_LENGTH); - if (message == NULL) { - return AWS_OP_ERR; - } - - enum aws_mqtt5_encoding_result result = - aws_mqtt5_encoder_encode_to_buffer(&connection->encoder, &message->message_data); - AWS_FATAL_ASSERT(result == AWS_MQTT5_ER_FINISHED); - - if (aws_channel_slot_send_message(connection->slot, message, AWS_CHANNEL_DIR_WRITE)) { - aws_mem_release(message->allocator, message); - return AWS_OP_ERR; - } - - return AWS_OP_SUCCESS; -} - int aws_mqtt5_mock_server_handle_connect_always_succeed( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, @@ -2041,7 +2013,7 @@ static void s_wait_for_suback_received(struct aws_mqtt5_client_mock_test_fixture aws_mutex_unlock(&test_context->lock); } -static int s_aws_mqtt5_server_send_suback_on_subscribe( +int aws_mqtt5_server_send_suback_on_subscribe( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -2069,7 +2041,7 @@ static int s_mqtt5_client_subscribe_success_fn(struct aws_allocator *allocator, aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = - s_aws_mqtt5_server_send_suback_on_subscribe; + aws_mqtt5_server_send_suback_on_subscribe; struct aws_mqtt5_client_mock_test_fixture test_context; @@ -2887,7 +2859,7 @@ static int s_aws_mqtt5_mock_server_handle_connect_honor_session_after_success( return aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_CONNACK, &connack_view); } -static int s_aws_mqtt5_mock_server_handle_connect_honor_session_unconditional( +int aws_mqtt5_mock_server_handle_connect_honor_session_unconditional( void *packet, struct aws_mqtt5_server_mock_connection_context *connection, void *user_data) { @@ -2929,7 +2901,7 @@ static bool s_received_n_lifecycle_events(void *arg) { return matching_events >= context->expected_event_count; } -static void s_wait_for_n_lifecycle_events( +void aws_wait_for_n_lifecycle_events( struct aws_mqtt5_client_mock_test_fixture *test_fixture, enum aws_mqtt5_client_lifecycle_event_type event_type, size_t expected_event_count) { @@ -2986,7 +2958,7 @@ static int s_do_mqtt5_client_session_resumption_test( test_options.client_options.session_behavior = session_behavior; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_CONNECT] = - s_aws_mqtt5_mock_server_handle_connect_honor_session_unconditional; + aws_mqtt5_mock_server_handle_connect_honor_session_unconditional; struct aws_mqtt5_client_mqtt5_mock_test_fixture_options test_fixture_options = { .client_options = &test_options.client_options, @@ -3000,7 +2972,7 @@ static int s_do_mqtt5_client_session_resumption_test( for (size_t i = 0; i < SESSION_RESUMPTION_CONNECT_COUNT; ++i) { ASSERT_SUCCESS(aws_mqtt5_client_start(client)); - s_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_SUCCESS, i + 1); + aws_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_CONNECTION_SUCCESS, i + 1); /* not technically truly safe to query depending on memory model. Remove if it becomes a problem. */ bool expected_rejoined_session = s_compute_expected_rejoined_session(session_behavior, i); @@ -3009,7 +2981,7 @@ static int s_do_mqtt5_client_session_resumption_test( /* can't use stop as that wipes session state */ aws_channel_shutdown(test_context.server_channel, AWS_ERROR_UNKNOWN); - s_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_DISCONNECTION, i + 1); + aws_wait_for_n_lifecycle_events(&test_context, AWS_MQTT5_CLET_DISCONNECTION, i + 1); } struct aws_mqtt5_packet_connect_storage clean_start_connect_storage; @@ -3280,7 +3252,7 @@ int aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success( struct aws_mqtt5_packet_unsuback_view unsuback_view = { .packet_id = unsubscribe_view->packet_id, - .reason_code_count = AWS_ARRAY_SIZE(mqtt5_unsuback_codes), + .reason_code_count = unsubscribe_view->topic_filter_count, .reason_codes = mqtt5_unsuback_codes, }; @@ -3418,7 +3390,7 @@ static int s_do_sub_pub_unsub_test(struct aws_allocator *allocator, enum aws_mqt test_options.client_options.publish_received_handler_user_data = &full_test_context; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = - s_aws_mqtt5_server_send_suback_on_subscribe; + aws_mqtt5_server_send_suback_on_subscribe; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_PUBLISH] = aws_mqtt5_mock_server_handle_publish_puback_and_forward; test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_UNSUBSCRIBE] = @@ -4316,7 +4288,7 @@ static int s_mqtt5_client_statistics_subscribe_fn(struct aws_allocator *allocato aws_mqtt5_client_test_init_default_options(&test_options); test_options.server_function_table.packet_handlers[AWS_MQTT5_PT_SUBSCRIBE] = - s_aws_mqtt5_server_send_suback_on_subscribe; + aws_mqtt5_server_send_suback_on_subscribe; struct aws_mqtt5_client_mock_test_fixture test_context; @@ -5530,7 +5502,10 @@ static int s_aws_mqtt5_server_send_aliased_publish_sequence( struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet; struct aws_mqtt5_packet_suback_view suback_view = { - .packet_id = subscribe_view->packet_id, .reason_code_count = 1, .reason_codes = s_alias_reason_codes}; + .packet_id = subscribe_view->packet_id, + .reason_code_count = 1, + .reason_codes = s_alias_reason_codes, + }; // just to be thorough, send a suback if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view)) { @@ -5725,7 +5700,10 @@ static int s_aws_mqtt5_server_send_aliased_publish_failure( struct aws_mqtt5_packet_subscribe_view *subscribe_view = packet; struct aws_mqtt5_packet_suback_view suback_view = { - .packet_id = subscribe_view->packet_id, .reason_code_count = 1, .reason_codes = s_alias_reason_codes}; + .packet_id = subscribe_view->packet_id, + .reason_code_count = 1, + .reason_codes = s_alias_reason_codes, + }; // just to be thorough, send a suback if (aws_mqtt5_mock_server_send_packet(connection, AWS_MQTT5_PT_SUBACK, &suback_view)) { diff --git a/tests/v5/mqtt5_testing_utils.c b/tests/v5/mqtt5_testing_utils.c index e8e72f3f..9d1163c9 100644 --- a/tests/v5/mqtt5_testing_utils.c +++ b/tests/v5/mqtt5_testing_utils.c @@ -1740,3 +1740,29 @@ size_t aws_mqtt5_linked_list_length(struct aws_linked_list *list) { return length; } + +#define TEST_IO_MESSAGE_LENGTH 4096 + +int aws_mqtt5_mock_server_send_packet( + struct aws_mqtt5_server_mock_connection_context *connection, + enum aws_mqtt5_packet_type packet_type, + void *packet) { + aws_mqtt5_encoder_append_packet_encoding(&connection->encoder, packet_type, packet); + + struct aws_io_message *message = aws_channel_acquire_message_from_pool( + connection->slot->channel, AWS_IO_MESSAGE_APPLICATION_DATA, TEST_IO_MESSAGE_LENGTH); + if (message == NULL) { + return AWS_OP_ERR; + } + + enum aws_mqtt5_encoding_result result = + aws_mqtt5_encoder_encode_to_buffer(&connection->encoder, &message->message_data); + AWS_FATAL_ASSERT(result == AWS_MQTT5_ER_FINISHED); + + if (aws_channel_slot_send_message(connection->slot, message, AWS_CHANNEL_DIR_WRITE)) { + aws_mem_release(message->allocator, message); + return AWS_OP_ERR; + } + + return AWS_OP_SUCCESS; +} diff --git a/tests/v5/mqtt5_testing_utils.h b/tests/v5/mqtt5_testing_utils.h index e4fa6de8..b1f016b8 100644 --- a/tests/v5/mqtt5_testing_utils.h +++ b/tests/v5/mqtt5_testing_utils.h @@ -218,6 +218,26 @@ int aws_mqtt5_mock_server_handle_unsubscribe_unsuback_success( struct aws_mqtt5_server_mock_connection_context *connection, void *user_data); +int aws_mqtt5_mock_server_send_packet( + struct aws_mqtt5_server_mock_connection_context *connection, + enum aws_mqtt5_packet_type packet_type, + void *packet); + +int aws_mqtt5_server_send_suback_on_subscribe( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data); + +int aws_mqtt5_mock_server_handle_connect_honor_session_unconditional( + void *packet, + struct aws_mqtt5_server_mock_connection_context *connection, + void *user_data); + +void aws_wait_for_n_lifecycle_events( + struct aws_mqtt5_client_mock_test_fixture *test_fixture, + enum aws_mqtt5_client_lifecycle_event_type event_type, + size_t expected_event_count); + extern const struct aws_string *g_default_client_id; #define RECONNECT_TEST_MIN_BACKOFF 500 diff --git a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c index dd295680..ee5fea34 100644 --- a/tests/v5/mqtt5_to_mqtt3_adapter_tests.c +++ b/tests/v5/mqtt5_to_mqtt3_adapter_tests.c @@ -9,18 +9,14 @@ #include #include #include -#include #include #include #include -#include #include -#include #include #include -#include enum aws_mqtt3_lifecycle_event_type { AWS_MQTT3_LET_CONNECTION_COMPLETE,