Files
2026-08-03 18:42:47 +08:00

265 lines
9.9 KiB
C

#ifndef AWS_TESTING_ASYNC_STREAM_TESTER_H
#define AWS_TESTING_ASYNC_STREAM_TESTER_H
/**
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
#include <aws/io/async_stream.h>
#include <aws/common/byte_buf.h>
#include <aws/common/condition_variable.h>
#include <aws/common/mutex.h>
#include <aws/common/thread.h>
#include <aws/io/future.h>
#include <aws/testing/stream_tester.h>
#ifndef AWS_UNSTABLE_TESTING_API
# error This code is designed for use by AWS owned libraries for the AWS C99 SDK. \
You are welcome to use it, but we make no promises on the stability of this API. \
To enable use of this code, set the AWS_UNSTABLE_TESTING_API compiler flag.
#endif
/**
* Use aws_async_input_stream_tester to test edge cases in systems that take async streams.
* You can customize its behavior (e.g. fail on 3rd read, always complete async, always complete synchronously, etc)
*/
enum aws_async_read_completion_strategy {
/* the tester has its own thread, and reads always complete from there */
AWS_ASYNC_READ_COMPLETES_ON_ANOTHER_THREAD,
/* reads complete before read() even returns */
AWS_ASYNC_READ_COMPLETES_IMMEDIATELY,
/* sometimes reads complete immediately, sometimes they complete on another thread */
AWS_ASYNC_READ_COMPLETES_ON_RANDOM_THREAD,
};
struct aws_async_input_stream_tester_options {
/* the async tester uses the synchronous tester under the hood,
* so here are those options */
struct aws_input_stream_tester_options base;
enum aws_async_read_completion_strategy completion_strategy;
/* if non-zero, a read will take at least this long to complete */
uint64_t read_duration_ns;
};
struct aws_async_input_stream_tester {
struct aws_async_input_stream base;
struct aws_allocator *alloc;
struct aws_async_input_stream_tester_options options;
struct aws_input_stream *source_stream;
struct aws_thread thread;
struct {
struct aws_mutex lock;
struct aws_condition_variable cvar;
/* when thread should perform a read, these are set */
struct aws_byte_buf *read_dest;
struct aws_future_bool *read_future;
/* if true, thread should shut down */
bool do_shutdown;
} synced_data;
struct aws_atomic_var num_outstanding_reads;
};
static inline void s_async_input_stream_tester_do_actual_read(
struct aws_async_input_stream_tester *impl,
struct aws_byte_buf *dest,
struct aws_future_bool *read_future) {
int error_code = 0;
/* delay, if that's how we're configured */
if (impl->options.read_duration_ns != 0) {
aws_thread_current_sleep(impl->options.read_duration_ns);
}
/* Keep calling read() until we get some data, or hit EOF.
* We do this because the synchronous aws_input_stream API allows
* 0 byte reads, but the aws_async_input_stream API does not. */
size_t prev_len = dest->len;
struct aws_stream_status status = {.is_end_of_stream = false, .is_valid = true};
while ((dest->len == prev_len) && !status.is_end_of_stream) {
/* read from stream */
if (aws_input_stream_read(impl->source_stream, dest) != AWS_OP_SUCCESS) {
error_code = aws_last_error();
goto done;
}
/* check if stream is done */
if (aws_input_stream_get_status(impl->source_stream, &status) != AWS_OP_SUCCESS) {
error_code = aws_last_error();
goto done;
}
}
done:
aws_atomic_fetch_sub(&impl->num_outstanding_reads, 1);
if (error_code != 0) {
aws_future_bool_set_error(read_future, error_code);
} else {
aws_future_bool_set_result(read_future, status.is_end_of_stream);
}
aws_future_bool_release(read_future);
}
static inline struct aws_future_bool *s_async_input_stream_tester_read(
struct aws_async_input_stream *stream,
struct aws_byte_buf *dest) {
struct aws_async_input_stream_tester *impl = (struct aws_async_input_stream_tester *)stream->impl;
size_t prev_outstanding_reads = aws_atomic_fetch_add(&impl->num_outstanding_reads, 1);
AWS_FATAL_ASSERT(prev_outstanding_reads == 0 && "Overlapping read() calls are forbidden");
struct aws_future_bool *read_future = aws_future_bool_new(stream->alloc);
bool do_on_thread = false;
switch (impl->options.completion_strategy) {
case AWS_ASYNC_READ_COMPLETES_ON_ANOTHER_THREAD:
do_on_thread = true;
break;
case AWS_ASYNC_READ_COMPLETES_IMMEDIATELY:
do_on_thread = false;
break;
case AWS_ASYNC_READ_COMPLETES_ON_RANDOM_THREAD:
do_on_thread = (rand() % 2 == 0);
break;
}
if (do_on_thread) {
/* BEGIN CRITICAL SECTION */
aws_mutex_lock(&impl->synced_data.lock);
impl->synced_data.read_dest = dest;
impl->synced_data.read_future = aws_future_bool_acquire(read_future);
AWS_FATAL_ASSERT(aws_condition_variable_notify_all(&impl->synced_data.cvar) == AWS_OP_SUCCESS);
aws_mutex_unlock(&impl->synced_data.lock);
/* END CRITICAL SECTION */
} else {
/* acquire additional refcount on future, since we call release once it's complete */
aws_future_bool_acquire(read_future);
s_async_input_stream_tester_do_actual_read(impl, dest, read_future);
}
return read_future;
}
static inline void s_async_input_stream_tester_do_actual_destroy(struct aws_async_input_stream_tester *impl) {
if (impl->options.completion_strategy != AWS_ASYNC_READ_COMPLETES_IMMEDIATELY) {
aws_condition_variable_clean_up(&impl->synced_data.cvar);
aws_mutex_clean_up(&impl->synced_data.lock);
}
aws_input_stream_release(impl->source_stream);
aws_mem_release(impl->base.alloc, impl);
}
/* refcount has reached zero */
static inline void s_async_input_stream_tester_destroy(struct aws_async_input_stream *async_stream) {
struct aws_async_input_stream_tester *impl = (struct aws_async_input_stream_tester *)async_stream->impl;
if (impl->options.completion_strategy == AWS_ASYNC_READ_COMPLETES_IMMEDIATELY) {
s_async_input_stream_tester_do_actual_destroy(impl);
} else {
/* signal thread to finish cleaning things up */
/* BEGIN CRITICAL SECTION */
aws_mutex_lock(&impl->synced_data.lock);
impl->synced_data.do_shutdown = true;
AWS_FATAL_ASSERT(aws_condition_variable_notify_all(&impl->synced_data.cvar) == AWS_OP_SUCCESS);
aws_mutex_unlock(&impl->synced_data.lock);
/* END CRITICAL SECTION */
}
}
static inline bool s_async_input_stream_tester_thread_pred(void *arg) {
struct aws_async_input_stream_tester *impl = (struct aws_async_input_stream_tester *)arg;
return impl->synced_data.do_shutdown || (impl->synced_data.read_dest != NULL);
}
static inline void s_async_input_stream_tester_thread(void *arg) {
struct aws_async_input_stream_tester *impl = (struct aws_async_input_stream_tester *)arg;
bool do_shutdown = false;
struct aws_byte_buf *read_dest = NULL;
struct aws_future_bool *read_future = NULL;
while (!do_shutdown) {
/* BEGIN CRITICAL SECTION */
aws_mutex_lock(&impl->synced_data.lock);
AWS_FATAL_ASSERT(
aws_condition_variable_wait_pred(
&impl->synced_data.cvar, &impl->synced_data.lock, s_async_input_stream_tester_thread_pred, impl) ==
AWS_OP_SUCCESS);
/* acquire work */
do_shutdown = impl->synced_data.do_shutdown;
read_dest = impl->synced_data.read_dest;
impl->synced_data.read_dest = NULL;
read_future = impl->synced_data.read_future;
impl->synced_data.read_future = NULL;
aws_mutex_unlock(&impl->synced_data.lock);
/* END CRITICAL SECTION */
if (read_dest != NULL) {
s_async_input_stream_tester_do_actual_read(impl, read_dest, read_future);
}
}
/* thread has shut down, finish destruction */
s_async_input_stream_tester_do_actual_destroy(impl);
}
static inline uint64_t aws_async_input_stream_tester_total_bytes_read(
const struct aws_async_input_stream *async_stream) {
const struct aws_async_input_stream_tester *async_impl =
(const struct aws_async_input_stream_tester *)async_stream->impl;
const struct aws_input_stream_tester *synchronous_impl =
(const struct aws_input_stream_tester *)async_impl->source_stream->impl;
return synchronous_impl->total_bytes_read;
}
static struct aws_async_input_stream_vtable s_async_input_stream_tester_vtable = {
.destroy = s_async_input_stream_tester_destroy,
.read = s_async_input_stream_tester_read,
};
static inline struct aws_async_input_stream *aws_async_input_stream_new_tester(
struct aws_allocator *alloc,
const struct aws_async_input_stream_tester_options *options) {
struct aws_async_input_stream_tester *impl =
(struct aws_async_input_stream_tester *)aws_mem_calloc(alloc, 1, sizeof(struct aws_async_input_stream_tester));
aws_async_input_stream_init_base(&impl->base, alloc, &s_async_input_stream_tester_vtable, impl);
impl->options = *options;
aws_atomic_init_int(&impl->num_outstanding_reads, 0);
impl->source_stream = aws_input_stream_new_tester(alloc, &options->base);
AWS_FATAL_ASSERT(impl->source_stream);
if (options->completion_strategy != AWS_ASYNC_READ_COMPLETES_IMMEDIATELY) {
aws_mutex_init(&impl->synced_data.lock);
aws_condition_variable_init(&impl->synced_data.cvar);
AWS_FATAL_ASSERT(aws_thread_init(&impl->thread, alloc) == AWS_OP_SUCCESS);
struct aws_thread_options thread_options = *aws_default_thread_options();
thread_options.name = aws_byte_cursor_from_c_str("AsyncStream");
thread_options.join_strategy = AWS_TJS_MANAGED;
AWS_FATAL_ASSERT(
aws_thread_launch(&impl->thread, s_async_input_stream_tester_thread, impl, &thread_options) ==
AWS_OP_SUCCESS);
}
return &impl->base;
}
#endif /* AWS_TESTING_ASYNC_STREAM_TESTER_H */