fix: #254 bound receiver queue by aggregate payload bytes
The per-connection memory budget (MAX_CONNECTION_MEMORY, 256 MiB) only charged wire buffers via receive_data_limited. Decompression buffers and per-file chunk copies were not accounted for, and the multithreaded receiver could enqueue up to 100 files (each up to 64 MiB uncompressed) ahead of a slow disk writer, retaining ~6.4 GiB per connection. A client sending highly compressible chunks with little bandwidth could OOM the host while the reserve never tripped. Bound the receive pipeline by aggregate payload bytes instead of item count alone: - Export MAX_CONNECTION_MEMORY from protocol.h. - PipelineContextReceiver tracks queued_bytes (payload bytes received but not yet released by the disk writer, i.e. queued or in the writer's hand) under the existing mutex. - receiver enqueue now blocks while the queue is full by count OR when adding the file would push queued_bytes over the configured byte limit, applying backpressure to the sender instead of failing the transfer. - The disk writer releases the byte budget after each file is freed and signals the not-full condition. - The server sets the byte ceiling to MAX_CONNECTION_MEMORY - 2*MAX_CHUNK_SIZE so that the queued payloads plus the transient wire/decompression buffers of the one in-flight chunk stay within the per-connection budget. The single-threaded receive path is already bounded: it writes files to disk before reading the next chunk, so its transient is at most one chunk's wire + decompressed + copied payload (~3 * MAX_CHUNK_SIZE, below the budget). Wire buffers remain charged exactly once by receive_data_limited; this change does not double charge them. Adds a deterministic unit test in test_multiprocessing.c proving that an enqueue which would exceed the byte budget blocks until the writer releases bytes.
This commit is contained in:
@@ -108,6 +108,8 @@ PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue*
|
||||
protocol_session_init(&context->session, file_descriptor, file_descriptor);
|
||||
protocol_session_set_ssl(&context->session, ssl);
|
||||
context->receiver_done = false;
|
||||
context->queued_bytes = 0;
|
||||
context->max_queue_bytes = 0;
|
||||
atomic_init(&context->cancelled, false);
|
||||
int init = 0;
|
||||
if (mtx_init(&context->mutex, mtx_plain) != thrd_success)
|
||||
@@ -143,14 +145,73 @@ void pipeline_context_receiver_destroy(PipelineContextReceiver* context) {
|
||||
free(context);
|
||||
}
|
||||
|
||||
void pipeline_context_receiver_set_queue_byte_limit(PipelineContextReceiver* context,
|
||||
size_t max_bytes) {
|
||||
if (context == NULL)
|
||||
return;
|
||||
mtx_lock(&context->mutex);
|
||||
context->max_queue_bytes = max_bytes;
|
||||
context->queued_bytes = 0;
|
||||
cnd_broadcast(&context->condition_not_full);
|
||||
mtx_unlock(&context->mutex);
|
||||
}
|
||||
|
||||
void pipeline_context_receiver_note_bytes_released(PipelineContextReceiver* context,
|
||||
size_t released_bytes) {
|
||||
if (context == NULL || context->max_queue_bytes == 0 || released_bytes == 0)
|
||||
return;
|
||||
mtx_lock(&context->mutex);
|
||||
if (released_bytes >= context->queued_bytes)
|
||||
context->queued_bytes = 0;
|
||||
else
|
||||
context->queued_bytes -= released_bytes;
|
||||
cnd_signal(&context->condition_not_full);
|
||||
mtx_unlock(&context->mutex);
|
||||
}
|
||||
|
||||
bool pipeline_context_receiver_enqueue_file(PipelineContextReceiver* context, File* file) {
|
||||
if (context == NULL || file == NULL)
|
||||
return false;
|
||||
size_t file_bytes = file->data ? file->data->size : 0;
|
||||
mtx_lock(&context->mutex);
|
||||
while (!atomic_load(&context->cancelled)) {
|
||||
bool blocked_by_count = queue_is_full(context->queue);
|
||||
bool blocked_by_budget = false;
|
||||
if (context->max_queue_bytes > 0) {
|
||||
size_t budget = context->max_queue_bytes;
|
||||
size_t used = context->queued_bytes;
|
||||
if (used >= budget) {
|
||||
blocked_by_budget = true;
|
||||
} else if (file_bytes > budget - used) {
|
||||
/* A single payload larger than the whole budget (not possible with
|
||||
the per-file receive cap) is only admitted to an empty pipeline so
|
||||
the wait can never deadlock. */
|
||||
blocked_by_budget = used != 0;
|
||||
}
|
||||
}
|
||||
if (!blocked_by_count && !blocked_by_budget)
|
||||
break;
|
||||
cnd_wait(&context->condition_not_full, &context->mutex);
|
||||
}
|
||||
if (atomic_load(&context->cancelled)) {
|
||||
mtx_unlock(&context->mutex);
|
||||
file_destroy(file);
|
||||
return false;
|
||||
}
|
||||
if (!queue_enqueue(context->queue, file)) {
|
||||
mtx_unlock(&context->mutex);
|
||||
file_destroy(file);
|
||||
return false;
|
||||
}
|
||||
context->queued_bytes += file_bytes;
|
||||
cnd_signal(&context->condition_not_empty);
|
||||
mtx_unlock(&context->mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool receiver_enqueue_file(File* file, void* context_pointer) {
|
||||
PipelineContextReceiver* context = context_pointer;
|
||||
if (queue_enqueue_multithreaded_cancel(context->queue, file, &context->mutex,
|
||||
&context->condition_not_empty,
|
||||
&context->condition_not_full, &context->cancelled))
|
||||
return true;
|
||||
file_destroy(file);
|
||||
return false;
|
||||
PipelineContextReceiver* context = (PipelineContextReceiver*)context_pointer;
|
||||
return pipeline_context_receiver_enqueue_file(context, file);
|
||||
}
|
||||
|
||||
static void receiver_thread_fail(PipelineContextReceiver* context) {
|
||||
@@ -211,8 +272,10 @@ int write_thread(void* pipeline_context) {
|
||||
protocol_session_unbind();
|
||||
return thrd_success;
|
||||
}
|
||||
size_t file_bytes = file->data ? file->data->size : 0;
|
||||
if (save_to_disk && !file_save_to_disk(root_directory, file, context->config)) {
|
||||
file_destroy(file);
|
||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||
mtx_lock(&context->mutex);
|
||||
atomic_store(&context->cancelled, true);
|
||||
context->receiver_done = true;
|
||||
@@ -224,5 +287,6 @@ int write_thread(void* pipeline_context) {
|
||||
return thrd_error;
|
||||
}
|
||||
file_destroy(file);
|
||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user