Compare commits

..
2 Commits
Author SHA1 Message Date
weaselbot 4b168c8688 Add basic Gitea Actions CI workflow
CI / pre-commit (pull_request) Successful in 56s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64) (pull_request) Failing after 1m16s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64) (pull_request) Failing after 1m27s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64) (pull_request) Failing after 1m55s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64) (pull_request) Failing after 1m47s
Run pre-commit checks and build/test the project on clang and gcc for
both amd64 and arm64. weaseljson is built and installed locally because
weaseldb depends on it via find_package.
2026-06-30 15:23:25 -04:00
weaselbot ad2650b0a6 Switch llhttp FetchContent to git repository
The tarball download redirects to codeload.github.com, which can be
unreliable in restricted network environments. Fetching the same release
via git uses github.com directly and pins the exact commit.
2026-06-30 15:23:23 -04:00
14 changed files with 189 additions and 282 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
- name: Install deps - name: Install deps
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential clang cmake gperf sudo apt-get install -y build-essential clang cmake
- name: Build and install weaseljson - name: Build and install weaseljson
run: | run: |
+6 -6
View File
@@ -264,7 +264,7 @@ CommitRequest {
1. **Request Processing**: Handler creates request-scoped arena for parsing request data 1. **Request Processing**: Handler creates request-scoped arena for parsing request data
1. **Response Generation**: Handler uses same arena for response construction (headers, JSON, etc.) 1. **Response Generation**: Handler uses same arena for response construction (headers, JSON, etc.)
1. **Response Queuing**: Handler calls `conn->send_response()` passing span + arena ownership 1. **Response Queuing**: Handler calls `conn->append_message()` passing span + arena ownership
1. **Response Writing**: I/O thread writes messages to socket, arena freed after completion 1. **Response Writing**: I/O thread writes messages to socket, arena freed after completion
> **Note**: Call `conn->reset()` periodically to reclaim arena memory. Best practice is after all outgoing bytes have been written. > **Note**: Call `conn->reset()` periodically to reclaim arena memory. Best practice is after all outgoing bytes have been written.
@@ -411,7 +411,7 @@ public:
Arena& arena = conn.get_arena(); Arena& arena = conn.get_arena();
// Generate response // Generate response
conn.send_response("HTTP/1.1 200 OK\r\n\r\nHello World"); conn.append_message("HTTP/1.1 200 OK\r\n\r\nHello World");
// Server retains ownership // Server retains ownership
} }
@@ -430,7 +430,7 @@ public:
work_queue.push([weak_conn, data = std::string(data)]() { work_queue.push([weak_conn, data = std::string(data)]() {
// Process asynchronously - connection may be closed // Process asynchronously - connection may be closed
if (auto conn_ref = weak_conn.lock()) { if (auto conn_ref = weak_conn.lock()) {
conn_ref->send_response("Async response"); conn_ref->append_message("Async response");
} }
}); });
} }
@@ -483,7 +483,7 @@ class YesHandler : ConnectionHandler {
public: public:
void on_connection_established(Connection &conn) override { void on_connection_established(Connection &conn) override {
// Write an initial "y\n" // Write an initial "y\n"
conn.send_response("y\n"); conn.append_message("y\n");
} }
void on_write_progress(Connection &conn) override { void on_write_progress(Connection &conn) override {
@@ -491,7 +491,7 @@ public:
// Don't use an unbounded amount of memory // Don't use an unbounded amount of memory
conn.reset(); conn.reset();
// Write "y\n" repeatedly // Write "y\n" repeatedly
conn.send_response("y\n"); conn.append_message("y\n");
} }
} }
}; };
@@ -519,7 +519,7 @@ auto weak_conn = conn.get_weak_ref();
background_processor.submit([weak_conn]() { background_processor.submit([weak_conn]() {
// Do work... // Do work...
if (auto conn_ref = weak_conn.lock()) { if (auto conn_ref = weak_conn.lock()) {
conn_ref->send_response("Background result"); conn_ref->append_message("Background result");
} }
// Connection automatically cleaned up by server // Connection automatically cleaned up by server
}); });
+6 -4
View File
@@ -335,7 +335,7 @@ void CommitPipeline::run_release_stage(int thread_index) {
// Send the JSON response using protocol-agnostic interface // Send the JSON response using protocol-agnostic interface
// HTTP formatting will happen in on_preprocess_writes() // HTTP formatting will happen in on_preprocess_writes()
conn_ref->send_response(commit_entry.handle, conn_ref->send_response(commit_entry.protocol_context,
commit_entry.response_json, commit_entry.response_json,
std::move(commit_entry.request_arena)); std::move(commit_entry.request_arena));
} else if constexpr (std::is_same_v<T, StatusEntry>) { } else if constexpr (std::is_same_v<T, StatusEntry>) {
@@ -349,7 +349,7 @@ void CommitPipeline::run_release_stage(int thread_index) {
// Send the JSON response using protocol-agnostic interface // Send the JSON response using protocol-agnostic interface
// HTTP formatting will happen in on_preprocess_writes() // HTTP formatting will happen in on_preprocess_writes()
conn_ref->send_response(status_entry.handle, conn_ref->send_response(status_entry.protocol_context,
status_entry.response_json, status_entry.response_json,
std::move(status_entry.request_arena)); std::move(status_entry.request_arena));
} else if constexpr (std::is_same_v<T, HealthCheckEntry>) { } else if constexpr (std::is_same_v<T, HealthCheckEntry>) {
@@ -364,7 +364,8 @@ void CommitPipeline::run_release_stage(int thread_index) {
// Send the response using protocol-agnostic interface // Send the response using protocol-agnostic interface
// HTTP formatting will happen in on_preprocess_writes() // HTTP formatting will happen in on_preprocess_writes()
conn_ref->send_response( conn_ref->send_response(
health_check_entry.handle, health_check_entry.response_json, health_check_entry.protocol_context,
health_check_entry.response_json,
std::move(health_check_entry.request_arena)); std::move(health_check_entry.request_arena));
} else if constexpr (std::is_same_v<T, GetVersionEntry>) { } else if constexpr (std::is_same_v<T, GetVersionEntry>) {
auto &get_version_entry = e; auto &get_version_entry = e;
@@ -377,7 +378,8 @@ void CommitPipeline::run_release_stage(int thread_index) {
// Send the response using protocol-agnostic interface // Send the response using protocol-agnostic interface
// HTTP formatting will happen in on_preprocess_writes() // HTTP formatting will happen in on_preprocess_writes()
conn_ref->send_response( conn_ref->send_response(
get_version_entry.handle, get_version_entry.response_json, get_version_entry.protocol_context,
get_version_entry.response_json,
std::move(get_version_entry.request_arena)); std::move(get_version_entry.request_arena));
} }
}, },
+7 -9
View File
@@ -1,6 +1,7 @@
#include "connection.hpp" #include "connection.hpp"
#include <cerrno> #include <cerrno>
#include <climits>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <sys/epoll.h> #include <sys/epoll.h>
@@ -118,14 +119,13 @@ void Connection::append_bytes(std::span<std::string_view> data_parts,
// I think we have to call epoll_ctl while holding mutex_. Otherwise a // I think we have to call epoll_ctl while holding mutex_. Otherwise a
// call that clears the write interest could get reordered with one that // call that clears the write interest could get reordered with one that
// sets it and we would hang. // sets it and we would hang.
epoll_ctl(server->event_loops_[epoll_index_].epoll_fd_, EPOLL_CTL_MOD, epoll_ctl(server->epoll_fds_[epoll_index_], EPOLL_CTL_MOD, fd_, &event);
fd_, &event);
} }
} }
} }
// May be called from a foreign thread! // May be called from a foreign thread!
void Connection::send_response(ProtocolHandle handle, void Connection::send_response(void *protocol_context,
std::string_view response_json, Arena arena) { std::string_view response_json, Arena arena) {
std::unique_lock lock(mutex_); std::unique_lock lock(mutex_);
@@ -136,7 +136,7 @@ void Connection::send_response(ProtocolHandle handle,
// Store response in queue for protocol handler processing // Store response in queue for protocol handler processing
pending_response_queue_.emplace_back( pending_response_queue_.emplace_back(
PendingResponse{handle, response_json, std::move(arena)}); PendingResponse{protocol_context, response_json, std::move(arena)});
// Trigger epoll interest if this is the first pending response // Trigger epoll interest if this is the first pending response
if (pending_response_queue_.size() == 1) { if (pending_response_queue_.size() == 1) {
@@ -147,13 +147,12 @@ void Connection::send_response(ProtocolHandle handle,
event.data.fd = fd_; event.data.fd = fd_;
event.events = EPOLLIN | EPOLLOUT; event.events = EPOLLIN | EPOLLOUT;
tsan_release(); tsan_release();
epoll_ctl(server->event_loops_[epoll_index_].epoll_fd_, EPOLL_CTL_MOD, epoll_ctl(server->epoll_fds_[epoll_index_], EPOLL_CTL_MOD, fd_, &event);
fd_, &event);
} }
} }
} }
int Connection::read_bytes(char *buf, size_t buffer_size) { int Connection::readBytes(char *buf, size_t buffer_size) {
int r; int r;
for (;;) { for (;;) {
r = read(fd_, buf, buffer_size); r = read(fd_, buf, buffer_size);
@@ -297,8 +296,7 @@ uint32_t Connection::write_bytes() {
// I think we have to call epoll_ctl while holding mutex_. Otherwise a // I think we have to call epoll_ctl while holding mutex_. Otherwise a
// call that clears the write interest could get reordered with one that // call that clears the write interest could get reordered with one that
// sets it and we would hang. // sets it and we would hang.
epoll_ctl(server->event_loops_[epoll_index_].epoll_fd_, EPOLL_CTL_MOD, epoll_ctl(server->epoll_fds_[epoll_index_], EPOLL_CTL_MOD, fd_, &event);
fd_, &event);
} }
// Handle shutdown modes after all messages are sent // Handle shutdown modes after all messages are sent
if (shutdown_requested_ == ConnectionShutdown::WriteOnly) { if (shutdown_requested_ == ConnectionShutdown::WriteOnly) {
+16 -17
View File
@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <atomic>
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
#include <deque> #include <deque>
@@ -33,31 +32,32 @@ enum class ConnectionShutdown {
/** /**
* Base interface for sending messages to a connection. * Base interface for sending messages to a connection.
* This restricted interface is safe for use by pipeline threads, * This restricted interface is safe for use by pipeline threads,
* containing only the send_response method needed for responses. * containing only the append_message method needed for responses.
* Pipeline threads should use WeakRef<MessageSender> to safely * Pipeline threads should use WeakRef<MessageSender> to safely
* send responses without accessing other connection functionality * send responses without accessing other connection functionality
* that should only be used by the I/O thread. * that should only be used by the I/O thread.
*/ */
struct MessageSender { struct MessageSender {
/** /**
* @brief Send response with protocol-specific handle for correlation. * @brief Send response with protocol-specific context for ordering.
* *
* Thread-safe method for pipeline threads to send responses back to clients. * Thread-safe method for pipeline threads to send responses back to clients.
* Delegates to the connection's protocol handler for ordering logic. * Delegates to the connection's protocol handler for ordering logic.
* The protocol handler may queue the response or send it immediately. * The protocol handler may queue the response or send it immediately.
* *
* @param handle Protocol-specific handle for correlating this response * @param protocol_context Arena-allocated protocol-specific context
* @param response_json JSON response body (may be empty for deferred * @param data Response data parts (may be empty for deferred serialization)
* serialization)
* @param arena Arena containing response data and context * @param arena Arena containing response data and context
* *
* Example usage: * Example usage:
* ```cpp * ```cpp
* ProtocolHandle handle = handler.allocate_response_context(arena); * auto* ctx = arena.allocate<HttpResponseContext>();
* conn.send_response(handle, response_json, std::move(arena)); * ctx->sequence_id = 42;
* auto response_data = format_response(arena);
* conn.send_response(ctx, response_data, std::move(arena));
* ``` * ```
*/ */
virtual void send_response(ProtocolHandle handle, virtual void send_response(void *protocol_context,
std::string_view response_json, Arena arena) = 0; std::string_view response_json, Arena arena) = 0;
virtual ~MessageSender() = default; virtual ~MessageSender() = default;
@@ -76,9 +76,9 @@ struct MessageSender {
* *
* Threading model: * Threading model:
* - Single mutex protects state shared with pipeline threads * - Single mutex protects state shared with pipeline threads
* - Pipeline threads call Connection methods (send_response, etc.) * - Pipeline threads call Connection methods (append_message, etc.)
* - I/O thread processes socket events and message queue * - I/O thread processes socket events and message queue
* - Pipeline threads register epoll write interest via send_response * - Pipeline threads register epoll write interest via append_message
* - Connection tracks closed state to prevent EBADF errors * - Connection tracks closed state to prevent EBADF errors
* *
* Arena allocator usage: * Arena allocator usage:
@@ -140,7 +140,7 @@ struct Connection : MessageSender {
append_bytes(std::span<std::string_view> data_parts, Arena arena, append_bytes(std::span<std::string_view> data_parts, Arena arena,
ConnectionShutdown shutdown_mode = ConnectionShutdown::None); ConnectionShutdown shutdown_mode = ConnectionShutdown::None);
void send_response(ProtocolHandle handle, std::string_view response_json, void send_response(void *protocol_context, std::string_view response_json,
Arena arena) override; Arena arena) override;
/** /**
@@ -165,7 +165,7 @@ struct Connection : MessageSender {
* if (auto conn = weak_conn.lock()) { * if (auto conn = weak_conn.lock()) {
* Arena arena; * Arena arena;
* auto response = process_request(request_data, arena); * auto response = process_request(request_data, arena);
* conn->send_response(handle, response_json, std::move(arena)); * conn->append_message({&response, 1}, std::move(arena));
* } * }
* }); * });
* ``` * ```
@@ -261,9 +261,8 @@ private:
* *
* Creates a new connection with the specified network address, file * Creates a new connection with the specified network address, file
* descriptor, and associated handler. Automatically increments the global * descriptor, and associated handler. Automatically increments the global
* active connection counter. The caller (Server) is responsible for * active connection counter and calls the handler's
* initializing the self weak reference and invoking * on_connection_established() method.
* on_connection_established().
* *
* @param addr Network address of the remote client (IPv4/IPv6 compatible) * @param addr Network address of the remote client (IPv4/IPv6 compatible)
* @param fd File descriptor for the socket connection * @param fd File descriptor for the socket connection
@@ -279,7 +278,7 @@ private:
friend Ref<T> make_ref(Args &&...args); friend Ref<T> make_ref(Args &&...args);
// Networking interface - only accessible by Server // Networking interface - only accessible by Server
int read_bytes(char *buf, size_t buffer_size); int readBytes(char *buf, size_t buffer_size);
enum WriteBytesResult { enum WriteBytesResult {
Error = 1 << 0, Error = 1 << 0,
Progress = 1 << 1, Progress = 1 << 1,
+2 -8
View File
@@ -8,19 +8,13 @@ struct Connection;
// Include Arena header since PendingResponse uses Arena by value // Include Arena header since PendingResponse uses Arena by value
#include "arena.hpp" #include "arena.hpp"
#include <cstdint>
// Opaque handle used to correlate responses with protocol-specific state.
// Each ConnectionHandler implementation defines the meaning of a handle value
// and resolves it back to full context in on_preprocess_writes().
using ProtocolHandle = int64_t;
/** /**
* Represents a response queued by pipeline threads for protocol processing. * Represents a response queued by pipeline threads for protocol processing.
* Contains JSON response data that can be wrapped by any protocol. * Contains JSON response data that can be wrapped by any protocol.
*/ */
struct PendingResponse { struct PendingResponse {
ProtocolHandle handle; // Protocol-specific handle for correlation void *protocol_context; // Arena-allocated protocol-specific context
std::string_view response_json; // JSON response body (arena-allocated) std::string_view response_json; // JSON response body (arena-allocated)
Arena arena; // Arena containing response data and context Arena arena; // Arena containing response data and context
}; };
@@ -48,7 +42,7 @@ public:
* Implementation should: * Implementation should:
* - Create request-scoped Arena for parsing and response generation * - Create request-scoped Arena for parsing and response generation
* - Parse incoming data using the request arena * - Parse incoming data using the request arena
* - Use conn.send_response() to queue response data to be sent * - Use conn.append_message() to queue response data to be sent
* - Handle partial messages and streaming protocols appropriately * - Handle partial messages and streaming protocols appropriately
* - Use conn.get_weak_ref() for async processing if needed * - Use conn.get_weak_ref() for async processing if needed
* *
+86 -133
View File
@@ -52,59 +52,6 @@ HttpRequestState::HttpRequestState()
current_header_field_buf(ArenaStlAllocator<char>(&arena)), current_header_field_buf(ArenaStlAllocator<char>(&arena)),
current_header_value_buf(ArenaStlAllocator<char>(&arena)) {} current_header_value_buf(ArenaStlAllocator<char>(&arena)) {}
// HttpConnectionState implementation
HttpConnectionState::~HttpConnectionState() = default;
void HttpConnectionState::register_response_context(
int64_t sequence_id, std::unique_ptr<HttpResponseContext> ctx) {
contexts_[sequence_id] = std::move(ctx);
}
HttpResponseContext *
HttpConnectionState::resolve_response_context(ProtocolHandle handle) {
auto it = contexts_.find(handle);
if (it == contexts_.end()) {
return nullptr;
}
return it->second.get();
}
void HttpConnectionState::send_ordered_response(
Connection &conn, HttpResponseContext *ctx,
std::span<std::string_view> http_response, Arena arena) {
assert(ctx);
int64_t sequence_id = ctx->sequence_id;
std::unique_ptr<HttpResponseContext> owned_ctx;
auto it = contexts_.find(sequence_id);
if (it != contexts_.end()) {
owned_ctx = std::move(it->second);
} else {
owned_ctx.reset(ctx);
}
owned_ctx->ready = true;
owned_ctx->data = http_response;
owned_ctx->arena = std::move(arena);
contexts_[sequence_id] = std::move(owned_ctx);
// Process ready responses in order and send via append_bytes
auto iter = contexts_.begin();
while (iter != contexts_.end() && iter->first == next_sequence_to_send) {
auto *context = iter->second.get();
if (!context || !context->ready) {
break;
}
// Send through append_bytes which handles write interest
conn.append_bytes(context->data, std::move(context->arena),
context->connection_close ? ConnectionShutdown::WriteOnly
: ConnectionShutdown::None);
next_sequence_to_send++;
iter = contexts_.erase(iter);
}
}
// HttpHandler implementation // HttpHandler implementation
void HttpHandler::on_connection_established(Connection &conn) { void HttpHandler::on_connection_established(Connection &conn) {
// Allocate HTTP state using server-provided arena for connection lifecycle // Allocate HTTP state using server-provided arena for connection lifecycle
@@ -125,11 +72,7 @@ void HttpHandler::on_preprocess_writes(
// Process incoming responses and add to reorder queue // Process incoming responses and add to reorder queue
{ {
for (auto &pending : pending_responses) { for (auto &pending : pending_responses) {
auto *ctx = state->resolve_response_context(pending.handle); auto *ctx = static_cast<HttpResponseContext *>(pending.protocol_context);
// Handle stale or invalid handles defensively
if (!ctx) {
continue;
}
// Determine HTTP status code and content type from response content // Determine HTTP status code and content type from response content
int status_code = 200; int status_code = 200;
@@ -153,8 +96,9 @@ void HttpHandler::on_preprocess_writes(
status_code, content_type, pending.response_json, pending.arena, status_code, content_type, pending.response_json, pending.arena,
ctx->http_request_id, ctx->connection_close); ctx->http_request_id, ctx->connection_close);
state->send_ordered_response(conn, ctx, http_response, state->send_ordered_response(conn, ctx->sequence_id, http_response,
std::move(pending.arena)); std::move(pending.arena),
ctx->connection_close);
} }
} }
} }
@@ -172,18 +116,11 @@ void HttpHandler::on_batch_complete(std::span<Connection *const> batch) {
int64_t sequence_id = state->get_next_sequence_id(); int64_t sequence_id = state->get_next_sequence_id();
req.sequence_id = sequence_id; req.sequence_id = sequence_id;
// Create HttpResponseContext for this request; sequence_id doubles as the // Create HttpResponseContext for this request
// ProtocolHandle for async response correlation. The context is auto *ctx = req.arena.allocate<HttpResponseContext>(1);
// registered in HttpConnectionState so async responses can resolve it by
// handle.
auto ctx = std::make_unique<HttpResponseContext>();
ctx->sequence_id = sequence_id; ctx->sequence_id = sequence_id;
ctx->http_request_id = req.http_request_id; ctx->http_request_id = req.http_request_id;
ctx->connection_close = req.connection_close; ctx->connection_close = req.connection_close;
HttpResponseContext *ctx_ptr = ctx.get();
req.response_context = ctx_ptr;
state->register_response_context(sequence_id, std::move(ctx));
ProtocolHandle handle = sequence_id;
RouteMatch route_match; RouteMatch route_match;
auto parse_result = auto parse_result =
@@ -195,9 +132,9 @@ void HttpHandler::on_batch_complete(std::span<Connection *const> batch) {
auto json_response = R"({"error":"Malformed URL encoding"})"; auto json_response = R"({"error":"Malformed URL encoding"})";
auto http_response = auto http_response =
format_json_response(400, json_response, req.arena, 0, true); format_json_response(400, json_response, req.arena, 0, true);
ctx_ptr->connection_close = true; state->send_ordered_response(*conn, ctx->sequence_id, http_response,
state->send_ordered_response(*conn, ctx_ptr, http_response, std::move(req.arena),
std::move(req.arena)); ctx->connection_close);
break; break;
} }
req.route = route_match.route; req.route = route_match.route;
@@ -239,25 +176,25 @@ void HttpHandler::on_batch_complete(std::span<Connection *const> batch) {
// Create CommitEntry for commit requests // Create CommitEntry for commit requests
if (req.route == HttpRoute::PostCommit && req.commit_request && if (req.route == HttpRoute::PostCommit && req.commit_request &&
req.parsing_commit && req.basic_validation_passed) { req.parsing_commit && req.basic_validation_passed) {
g_batch_entries.emplace_back(CommitEntry(conn->get_weak_ref(), handle, g_batch_entries.emplace_back(CommitEntry(conn->get_weak_ref(), ctx,
req.commit_request.get(), req.commit_request.get(),
std::move(req.arena))); std::move(req.arena)));
} }
// Create StatusEntry for status requests // Create StatusEntry for status requests
else if (req.route == HttpRoute::GetStatus) { else if (req.route == HttpRoute::GetStatus) {
g_batch_entries.emplace_back(StatusEntry(conn->get_weak_ref(), handle, g_batch_entries.emplace_back(StatusEntry(conn->get_weak_ref(), ctx,
req.status_request_id, req.status_request_id,
std::move(req.arena))); std::move(req.arena)));
} }
// Create HealthCheckEntry for health check requests // Create HealthCheckEntry for health check requests
else if (req.route == HttpRoute::GetOk) { else if (req.route == HttpRoute::GetOk) {
g_batch_entries.emplace_back(HealthCheckEntry( g_batch_entries.emplace_back(
conn->get_weak_ref(), handle, std::move(req.arena))); HealthCheckEntry(conn->get_weak_ref(), ctx, std::move(req.arena)));
} }
// Create GetVersionEntry for version requests // Create GetVersionEntry for version requests
else if (req.route == HttpRoute::GetVersion) { else if (req.route == HttpRoute::GetVersion) {
g_batch_entries.emplace_back( g_batch_entries.emplace_back(
GetVersionEntry(conn->get_weak_ref(), handle, std::move(req.arena), GetVersionEntry(conn->get_weak_ref(), ctx, std::move(req.arena),
commit_pipeline_.get_committed_version())); commit_pipeline_.get_committed_version()));
} }
} }
@@ -298,17 +235,13 @@ void HttpHandler::on_data_arrived(std::string_view data, Connection &conn) {
break; break;
} }
// Parse error - send response directly since this is before sequence // Parse error - send response directly since this is before sequence
// assignment. Use a temporary response context to carry metadata. // assignment
auto json_response = R"({"error":"Bad request"})"; auto json_response = R"({"error":"Bad request"})";
auto ctx = std::make_unique<HttpResponseContext>();
ctx->sequence_id = state->get_next_sequence_id();
ctx->http_request_id = 0;
ctx->connection_close = true;
auto http_response = auto http_response =
format_json_response(400, json_response, state->pending.arena, format_json_response(400, json_response, state->pending.arena, 0, true);
ctx->http_request_id, ctx->connection_close); state->send_ordered_response(conn, state->get_next_sequence_id(),
state->send_ordered_response(conn, ctx.get(), http_response, http_response, std::move(state->pending.arena),
std::move(state->pending.arena)); true);
return; return;
} }
} }
@@ -322,18 +255,17 @@ void HttpHandler::handle_get_version(Connection &, HttpRequestState &) {
void HttpHandler::handle_post_commit(Connection &conn, void HttpHandler::handle_post_commit(Connection &conn,
HttpRequestState &state) { HttpRequestState &state) {
commit_counter.inc(); commit_counter.inc();
auto *ctx = state.response_context;
assert(ctx);
// Check if streaming parse was successful // Check if streaming parse was successful
if (!state.commit_request || !state.parsing_commit) { if (!state.commit_request || !state.parsing_commit) {
auto json_response = R"({"error":"Parse failed"})"; auto json_response = R"({"error":"Parse failed"})";
auto http_response = auto http_response =
format_json_response(400, json_response, state.arena, format_json_response(400, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
return; return;
} }
@@ -378,11 +310,12 @@ void HttpHandler::handle_post_commit(Connection &conn,
static_cast<int>(error_msg.size()), error_msg.data()); static_cast<int>(error_msg.size()), error_msg.data());
auto http_response = auto http_response =
format_json_response(400, json_response, state.arena, format_json_response(400, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
return; return;
} }
@@ -393,25 +326,22 @@ void HttpHandler::handle_post_commit(Connection &conn,
void HttpHandler::handle_get_subscribe(Connection &conn, void HttpHandler::handle_get_subscribe(Connection &conn,
HttpRequestState &state) { HttpRequestState &state) {
auto *ctx = state.response_context;
assert(ctx);
// TODO: Implement subscription streaming // TODO: Implement subscription streaming
auto json_response = auto json_response =
R"({"message":"Subscription endpoint - streaming not yet implemented"})"; R"({"message":"Subscription endpoint - streaming not yet implemented"})";
auto http_response = auto http_response =
format_json_response(200, json_response, state.arena, format_json_response(200, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
} }
void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state, void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state,
const RouteMatch &route_match) { const RouteMatch &route_match) {
status_counter.inc(); status_counter.inc();
auto *ctx = state.response_context;
assert(ctx);
// Status requests are processed through the pipeline // Status requests are processed through the pipeline
// Response will be generated in the sequence stage // Response will be generated in the sequence stage
// This handler extracts request_id from query parameters and prepares for // This handler extracts request_id from query parameters and prepares for
@@ -424,12 +354,13 @@ void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state,
R"({"error":"Missing required query parameter: request_id"})"; R"({"error":"Missing required query parameter: request_id"})";
auto http_response = auto http_response =
format_json_response(400, json_response, state.arena, format_json_response(400, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
// Add directly to response queue with proper sequencing // Add directly to response queue with proper sequencing
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
return; return;
} }
@@ -437,12 +368,13 @@ void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state,
auto json_response = R"({"error":"Empty request_id parameter"})"; auto json_response = R"({"error":"Empty request_id parameter"})";
auto http_response = auto http_response =
format_json_response(400, json_response, state.arena, format_json_response(400, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
// Add directly to response queue with proper sequencing // Add directly to response queue with proper sequencing
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
return; return;
} }
@@ -455,58 +387,53 @@ void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state,
void HttpHandler::handle_put_retention(Connection &conn, void HttpHandler::handle_put_retention(Connection &conn,
HttpRequestState &state, HttpRequestState &state,
const RouteMatch &) { const RouteMatch &) {
auto *ctx = state.response_context;
assert(ctx);
// TODO: Parse retention policy from body and store // TODO: Parse retention policy from body and store
auto json_response = R"({"policy_id":"example","status":"created"})"; auto json_response = R"({"policy_id":"example","status":"created"})";
auto http_response = auto http_response =
format_json_response(200, json_response, state.arena, format_json_response(200, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
// Send through reorder queue and preprocessing to maintain proper ordering // Send through reorder queue and preprocessing to maintain proper ordering
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
} }
void HttpHandler::handle_get_retention(Connection &conn, void HttpHandler::handle_get_retention(Connection &conn,
HttpRequestState &state, HttpRequestState &state,
const RouteMatch &) { const RouteMatch &) {
auto *ctx = state.response_context;
assert(ctx);
// TODO: Extract policy_id from URL or return all policies // TODO: Extract policy_id from URL or return all policies
auto json_response = R"({"policies":[]})"; auto json_response = R"({"policies":[]})";
auto http_response = auto http_response =
format_json_response(200, json_response, state.arena, format_json_response(200, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
// Send through reorder queue and preprocessing to maintain proper ordering // Send through reorder queue and preprocessing to maintain proper ordering
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
} }
void HttpHandler::handle_delete_retention(Connection &conn, void HttpHandler::handle_delete_retention(Connection &conn,
HttpRequestState &state, HttpRequestState &state,
const RouteMatch &) { const RouteMatch &) {
auto *ctx = state.response_context;
assert(ctx);
// TODO: Extract policy_id from URL and delete // TODO: Extract policy_id from URL and delete
auto json_response = R"({"policy_id":"example","status":"deleted"})"; auto json_response = R"({"policy_id":"example","status":"deleted"})";
auto http_response = auto http_response =
format_json_response(200, json_response, state.arena, format_json_response(200, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
// Send through reorder queue and preprocessing to maintain proper ordering // Send through reorder queue and preprocessing to maintain proper ordering
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
} }
void HttpHandler::handle_get_metrics(Connection &conn, void HttpHandler::handle_get_metrics(Connection &conn,
HttpRequestState &state) { HttpRequestState &state) {
auto *ctx = state.response_context;
assert(ctx);
metrics_counter.inc(); metrics_counter.inc();
auto metrics_span = metric::render(state.arena); auto metrics_span = metric::render(state.arena);
@@ -523,19 +450,19 @@ void HttpHandler::handle_get_metrics(Connection &conn,
// Build HTTP headers // Build HTTP headers
std::string_view headers; std::string_view headers;
if (ctx->connection_close) { if (state.connection_close) {
headers = static_format( headers = static_format(
state.arena, "HTTP/1.1 200 OK\r\n", state.arena, "HTTP/1.1 200 OK\r\n",
"Content-Type: text/plain; version=0.0.4\r\n", "Content-Type: text/plain; version=0.0.4\r\n",
"Content-Length: ", static_cast<uint64_t>(total_size), "\r\n", "Content-Length: ", static_cast<uint64_t>(total_size), "\r\n",
"X-Response-ID: ", static_cast<int64_t>(ctx->http_request_id), "\r\n", "X-Response-ID: ", static_cast<int64_t>(state.http_request_id), "\r\n",
"Connection: close\r\n", "\r\n"); "Connection: close\r\n", "\r\n");
} else { } else {
headers = static_format( headers = static_format(
state.arena, "HTTP/1.1 200 OK\r\n", state.arena, "HTTP/1.1 200 OK\r\n",
"Content-Type: text/plain; version=0.0.4\r\n", "Content-Type: text/plain; version=0.0.4\r\n",
"Content-Length: ", static_cast<uint64_t>(total_size), "\r\n", "Content-Length: ", static_cast<uint64_t>(total_size), "\r\n",
"X-Response-ID: ", static_cast<int64_t>(ctx->http_request_id), "\r\n", "X-Response-ID: ", static_cast<int64_t>(state.http_request_id), "\r\n",
"Connection: keep-alive\r\n", "\r\n"); "Connection: keep-alive\r\n", "\r\n");
} }
@@ -545,7 +472,9 @@ void HttpHandler::handle_get_metrics(Connection &conn,
} }
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, result, std::move(state.arena)); conn_state->send_ordered_response(conn, state.sequence_id, result,
std::move(state.arena),
state.connection_close);
} }
void HttpHandler::handle_get_ok(Connection &, HttpRequestState &) { void HttpHandler::handle_get_ok(Connection &, HttpRequestState &) {
@@ -556,17 +485,41 @@ void HttpHandler::handle_get_ok(Connection &, HttpRequestState &) {
} }
void HttpHandler::handle_not_found(Connection &conn, HttpRequestState &state) { void HttpHandler::handle_not_found(Connection &conn, HttpRequestState &state) {
auto *ctx = state.response_context;
assert(ctx);
not_found_counter.inc(); not_found_counter.inc();
auto json_response = R"({"error":"Not found"})"; auto json_response = R"({"error":"Not found"})";
auto http_response = auto http_response =
format_json_response(404, json_response, state.arena, format_json_response(404, json_response, state.arena,
ctx->http_request_id, ctx->connection_close); state.http_request_id, state.connection_close);
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data); auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response, conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena)); std::move(state.arena),
state.connection_close);
}
void HttpConnectionState::send_ordered_response(
Connection &conn, int64_t sequence_id,
std::span<std::string_view> http_response, Arena arena,
bool close_connection) {
// Add to reorder queue with proper sequencing
ready_responses[sequence_id] =
ResponseData{http_response, std::move(arena), close_connection};
// Process ready responses in order and send via append_bytes
auto iter = ready_responses.begin();
while (iter != ready_responses.end() &&
iter->first == next_sequence_to_send) {
auto &[sequence_id, response_data] = *iter;
// Send through append_bytes which handles write interest
conn.append_bytes(response_data.data, std::move(response_data.arena),
response_data.connection_close
? ConnectionShutdown::WriteOnly
: ConnectionShutdown::None);
next_sequence_to_send++;
iter = ready_responses.erase(iter);
}
} }
std::span<std::string_view> std::span<std::string_view>
+13 -26
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <map> #include <map>
#include <memory>
#include <string_view> #include <string_view>
#include <llhttp.h> #include <llhttp.h>
@@ -20,18 +19,22 @@ struct RouteMatch;
/** /**
* HTTP-specific response context stored in pipeline entries. * HTTP-specific response context stored in pipeline entries.
* Arena-allocated and identified by a ProtocolHandle (sequence_id). * Arena-allocated and passed through pipeline for response correlation.
* Also owns the response data once it becomes ready.
*/ */
struct HttpResponseContext { struct HttpResponseContext {
int64_t sequence_id; // For response ordering in pipelining int64_t sequence_id; // For response ordering in pipelining
int64_t http_request_id; // For X-Response-ID header int64_t http_request_id; // For X-Response-ID header
bool connection_close; // Whether to close connection after response bool connection_close; // Whether to close connection after response
};
// Response payload; populated when the response is ready. /**
bool ready = false; * Response data ready to send (sequence_id -> response data).
* Absence from map indicates response not ready yet.
*/
struct ResponseData {
std::span<std::string_view> data; std::span<std::string_view> data;
Arena arena; Arena arena;
bool connection_close;
}; };
/** /**
@@ -66,10 +69,6 @@ struct HttpRequestState {
0; // X-Request-Id header value (for tracing/logging) 0; // X-Request-Id header value (for tracing/logging)
int64_t sequence_id = 0; // Assigned for response ordering in pipelining int64_t sequence_id = 0; // Assigned for response ordering in pipelining
// HTTP response context for this request (set during batch processing).
// Non-owning: the context is owned by HttpConnectionState::contexts_.
HttpResponseContext *response_context = nullptr;
// Streaming parser for POST requests // Streaming parser for POST requests
Arena::Ptr<JsonCommitRequestParser> commit_parser; Arena::Ptr<JsonCommitRequestParser> commit_parser;
Arena::Ptr<CommitRequest> commit_request; Arena::Ptr<CommitRequest> commit_request;
@@ -90,27 +89,15 @@ struct HttpConnectionState {
int64_t get_next_sequence_id() { return next_sequence_id++; } int64_t get_next_sequence_id() { return next_sequence_id++; }
HttpConnectionState(); HttpConnectionState();
~HttpConnectionState();
// Register an HttpResponseContext so the pipeline can resolve it by handle. void send_ordered_response(Connection &conn, int64_t sequence_id,
void register_response_context(int64_t sequence_id,
std::unique_ptr<HttpResponseContext> ctx);
// Resolve a ProtocolHandle back to its HttpResponseContext.
HttpResponseContext *resolve_response_context(ProtocolHandle handle);
// Mark a context's response data as ready and try to send in-order.
// The context may already be registered in contexts_; if so, it is moved out
// and populated. A transient context pointer (e.g., for parse errors) works
// too.
void send_ordered_response(Connection &conn, HttpResponseContext *ctx,
std::span<std::string_view> http_response, std::span<std::string_view> http_response,
Arena arena); Arena arena, bool close_connection);
private: private:
// All registered response contexts keyed by sequence_id. // Response ordering for HTTP pipelining
// A context stays here until its response has been dispatched. std::map<int64_t, ResponseData>
std::map<int64_t, std::unique_ptr<HttpResponseContext>> contexts_; ready_responses; // sequence_id -> response data
int64_t next_sequence_to_send = 0; int64_t next_sequence_to_send = 0;
int64_t next_sequence_id = 0; int64_t next_sequence_id = 0;
}; };
+1 -1
View File
@@ -3,7 +3,7 @@
#include <memory> #include <memory>
#include <simdutf.h> #include <simdutf.h>
#include <weaseljson.h> #include <weaseljson/weaseljson.h>
#include "commit_request_parser.hpp" #include "commit_request_parser.hpp"
#include "json_token_enum.hpp" #include "json_token_enum.hpp"
+17 -18
View File
@@ -17,8 +17,8 @@ struct CommitEntry {
bool resolve_success = false; // Set by resolve stage bool resolve_success = false; // Set by resolve stage
bool persist_success = false; // Set by persist stage bool persist_success = false; // Set by persist stage
// Protocol-agnostic handle for correlating the response // Protocol-agnostic context (arena-allocated, protocol-specific)
ProtocolHandle handle = -1; void *protocol_context = nullptr;
const CommitRequest *commit_request = nullptr; // Points to request_arena data const CommitRequest *commit_request = nullptr; // Points to request_arena data
// Request arena contains parsed request data and response data // Request arena contains parsed request data and response data
@@ -28,9 +28,9 @@ struct CommitEntry {
std::string_view response_json; std::string_view response_json;
CommitEntry() = default; // Default constructor for variant CommitEntry() = default; // Default constructor for variant
explicit CommitEntry(WeakRef<MessageSender> conn, ProtocolHandle handle, explicit CommitEntry(WeakRef<MessageSender> conn, void *ctx,
const CommitRequest *req, Arena arena) const CommitRequest *req, Arena arena)
: connection(std::move(conn)), handle(handle), commit_request(req), : connection(std::move(conn)), protocol_context(ctx), commit_request(req),
request_arena(std::move(arena)) {} request_arena(std::move(arena)) {}
}; };
@@ -42,8 +42,8 @@ struct StatusEntry {
WeakRef<MessageSender> connection; WeakRef<MessageSender> connection;
int64_t version_upper_bound = 0; // Set by sequence stage int64_t version_upper_bound = 0; // Set by sequence stage
// Protocol-agnostic handle for correlating the response // Protocol-agnostic context (arena-allocated, protocol-specific)
ProtocolHandle handle = -1; void *protocol_context = nullptr;
std::string_view status_request_id; // Points to request_arena data std::string_view status_request_id; // Points to request_arena data
// Request arena for request data // Request arena for request data
@@ -53,9 +53,9 @@ struct StatusEntry {
std::string_view response_json; std::string_view response_json;
StatusEntry() = default; // Default constructor for variant StatusEntry() = default; // Default constructor for variant
explicit StatusEntry(WeakRef<MessageSender> conn, ProtocolHandle handle, explicit StatusEntry(WeakRef<MessageSender> conn, void *ctx,
std::string_view request_id, Arena arena) std::string_view request_id, Arena arena)
: connection(std::move(conn)), handle(handle), : connection(std::move(conn)), protocol_context(ctx),
status_request_id(request_id), request_arena(std::move(arena)) {} status_request_id(request_id), request_arena(std::move(arena)) {}
}; };
@@ -67,8 +67,8 @@ struct StatusEntry {
struct HealthCheckEntry { struct HealthCheckEntry {
WeakRef<MessageSender> connection; WeakRef<MessageSender> connection;
// Protocol-agnostic handle for correlating the response // Protocol-agnostic context (arena-allocated, protocol-specific)
ProtocolHandle handle = -1; void *protocol_context = nullptr;
// Request arena for response data // Request arena for response data
Arena request_arena; Arena request_arena;
@@ -77,9 +77,8 @@ struct HealthCheckEntry {
std::string_view response_json; std::string_view response_json;
HealthCheckEntry() = default; // Default constructor for variant HealthCheckEntry() = default; // Default constructor for variant
explicit HealthCheckEntry(WeakRef<MessageSender> conn, ProtocolHandle handle, explicit HealthCheckEntry(WeakRef<MessageSender> conn, void *ctx, Arena arena)
Arena arena) : connection(std::move(conn)), protocol_context(ctx),
: connection(std::move(conn)), handle(handle),
request_arena(std::move(arena)) {} request_arena(std::move(arena)) {}
}; };
@@ -90,8 +89,8 @@ struct HealthCheckEntry {
struct GetVersionEntry { struct GetVersionEntry {
WeakRef<MessageSender> connection; WeakRef<MessageSender> connection;
// Protocol-agnostic handle for correlating the response // Protocol-agnostic context (arena-allocated, protocol-specific)
ProtocolHandle handle = -1; void *protocol_context = nullptr;
// Request arena for response data // Request arena for response data
Arena request_arena; Arena request_arena;
@@ -103,9 +102,9 @@ struct GetVersionEntry {
int64_t version; int64_t version;
GetVersionEntry() = default; // Default constructor for variant GetVersionEntry() = default; // Default constructor for variant
explicit GetVersionEntry(WeakRef<MessageSender> conn, ProtocolHandle handle, explicit GetVersionEntry(WeakRef<MessageSender> conn, void *ctx, Arena arena,
Arena arena, int64_t version) int64_t version)
: connection(std::move(conn)), handle(handle), : connection(std::move(conn)), protocol_context(ctx),
request_arena(std::move(arena)), version(version) {} request_arena(std::move(arena)), version(version) {}
}; };
+25 -33
View File
@@ -1,5 +1,6 @@
#include "server.hpp" #include "server.hpp"
#include <csignal>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -74,7 +75,7 @@ Server::~Server() {
} }
// Close all epoll instances // Close all epoll instances
for (auto [epollfd] : event_loops_) { for (int epollfd : epoll_fds_) {
if (epollfd != -1) { if (epollfd != -1) {
int e = close(epollfd); int e = close(epollfd);
if (e == -1 && errno != EINTR) { if (e == -1 && errno != EINTR) {
@@ -83,7 +84,7 @@ Server::~Server() {
} }
} }
} }
event_loops_.clear(); epoll_fds_.clear();
// Close all listen sockets (Server always owns them) // Close all listen sockets (Server always owns them)
for (int fd : listen_fds_) { for (int fd : listen_fds_) {
@@ -166,7 +167,7 @@ int Server::create_local_connection() {
// Use round-robin distribution for local connections across epoll instances // Use round-robin distribution for local connections across epoll instances
size_t epoll_index = size_t epoll_index =
connection_distribution_counter_.fetch_add(1, std::memory_order_relaxed) % connection_distribution_counter_.fetch_add(1, std::memory_order_relaxed) %
event_loops_.size(); epoll_fds_.size();
// Create Connection object // Create Connection object
auto connection = make_ref<Connection>( auto connection = make_ref<Connection>(
@@ -183,7 +184,7 @@ int Server::create_local_connection() {
event.events = EPOLLIN; event.events = EPOLLIN;
event.data.fd = server_fd; event.data.fd = server_fd;
int epollfd = event_loops_[epoll_index].epoll_fd_; int epollfd = epoll_fds_[epoll_index];
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_fd, &event) == -1) { if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl ADD local connection"); perror("epoll_ctl ADD local connection");
connection_registry_.remove(server_fd); connection_registry_.remove(server_fd);
@@ -220,11 +221,11 @@ void Server::setup_shutdown_pipe() {
void Server::create_epoll_instances() { void Server::create_epoll_instances() {
// Create one epoll instance per I/O thread (1:1 mapping) to eliminate // Create one epoll instance per I/O thread (1:1 mapping) to eliminate
// contention // contention
event_loops_.resize(config_.server.io_threads); epoll_fds_.resize(config_.server.io_threads);
for (int i = 0; i < config_.server.io_threads; ++i) { for (int i = 0; i < config_.server.io_threads; ++i) {
event_loops_[i].epoll_fd_ = epoll_create1(EPOLL_CLOEXEC); epoll_fds_[i] = epoll_create1(EPOLL_CLOEXEC);
if (event_loops_[i].epoll_fd_ == -1) { if (epoll_fds_[i] == -1) {
perror("epoll_create1"); perror("epoll_create1");
std::abort(); std::abort();
} }
@@ -234,7 +235,7 @@ void Server::create_epoll_instances() {
shutdown_event.events = EPOLLIN; shutdown_event.events = EPOLLIN;
shutdown_event.data.fd = shutdown_pipe_[0]; shutdown_event.data.fd = shutdown_pipe_[0];
if (epoll_ctl(event_loops_[i].epoll_fd_, EPOLL_CTL_ADD, shutdown_pipe_[0], if (epoll_ctl(epoll_fds_[i], EPOLL_CTL_ADD, shutdown_pipe_[0],
&shutdown_event) == -1) { &shutdown_event) == -1) {
perror("epoll_ctl shutdown pipe"); perror("epoll_ctl shutdown pipe");
std::abort(); std::abort();
@@ -246,8 +247,8 @@ void Server::create_epoll_instances() {
struct epoll_event listen_event; struct epoll_event listen_event;
listen_event.events = EPOLLIN | EPOLLEXCLUSIVE; listen_event.events = EPOLLIN | EPOLLEXCLUSIVE;
listen_event.data.fd = listen_fd; listen_event.data.fd = listen_fd;
if (epoll_ctl(event_loops_[i].epoll_fd_, EPOLL_CTL_ADD, listen_fd, if (epoll_ctl(epoll_fds_[i], EPOLL_CTL_ADD, listen_fd, &listen_event) ==
&listen_event) == -1) { -1) {
perror("epoll_ctl listen socket"); perror("epoll_ctl listen socket");
std::abort(); std::abort();
} }
@@ -264,7 +265,7 @@ void Server::start_io_threads(std::vector<std::thread> &threads) {
("io-" + std::to_string(thread_id)).c_str()); ("io-" + std::to_string(thread_id)).c_str());
// Each thread uses its assigned epoll instance (1:1 mapping) // Each thread uses its assigned epoll instance (1:1 mapping)
int epollfd = event_loops_[thread_id].epoll_fd_; int epollfd = epoll_fds_[thread_id];
std::vector<epoll_event> events(config_.server.event_batch_size); std::vector<epoll_event> events(config_.server.event_batch_size);
std::vector<Ref<Connection>> batch(config_.server.event_batch_size); std::vector<Ref<Connection>> batch(config_.server.event_batch_size);
@@ -409,30 +410,21 @@ void Server::process_connection_reads(Ref<Connection> &conn, int events) {
auto buf_size = config_.server.read_buffer_size; auto buf_size = config_.server.read_buffer_size;
g_read_buffer.resize(buf_size); g_read_buffer.resize(buf_size);
char *buf = g_read_buffer.data(); char *buf = g_read_buffer.data();
int r = conn->readBytes(buf, buf_size);
// Once we do EPOLLET we must drain the socket until read returns EAGAIN. if (r < 0) {
for (;;) { // Error or EOF - connection should be closed
int r = conn->read_bytes(buf, buf_size); close_connection(conn);
return;
if (r < 0) {
// Error or EOF - connection should be closed
close_connection(conn);
return;
}
if (r == 0) {
// No data available (EAGAIN) - read side drained
return;
}
// Call handler with connection reference - server retains ownership.
handler_.on_data_arrived(std::string_view{buf, size_t(r)}, *conn);
// The connection may have been closed by the handler; stop reading.
if (!conn) {
return;
}
} }
if (r == 0) {
// No data available (EAGAIN) - skip read processing but continue
return;
}
// Call handler with connection reference - server retains ownership
handler_.on_data_arrived(std::string_view{buf, size_t(r)}, *conn);
} }
} }
+2 -5
View File
@@ -131,17 +131,14 @@ private:
// Shutdown coordination // Shutdown coordination
int shutdown_pipe_[2] = {-1, -1}; int shutdown_pipe_[2] = {-1, -1};
struct EventLoopState {
int epoll_fd_;
};
// Multiple epoll file descriptors (1:1 with I/O threads) to reduce contention // Multiple epoll file descriptors (1:1 with I/O threads) to reduce contention
std::vector<EventLoopState> event_loops_; std::vector<int> epoll_fds_;
std::vector<int> std::vector<int>
listen_fds_; // FDs to accept connections on (Server owns these) listen_fds_; // FDs to accept connections on (Server owns these)
// Private helper methods // Private helper methods
void setup_shutdown_pipe(); void setup_shutdown_pipe();
void setup_signal_handling();
void create_epoll_instances(); void create_epoll_instances();
void start_io_threads(std::vector<std::thread> &threads); void start_io_threads(std::vector<std::thread> &threads);
+6 -20
View File
@@ -3,36 +3,22 @@
#include "connection_handler.hpp" #include "connection_handler.hpp"
#include "server.hpp" #include "server.hpp"
#include <atomic>
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include <latch> #include <latch>
#include <string_view> #include <string_view>
#include <thread> #include <thread>
struct Event {
void wait() { done_.wait(); }
void set() {
if (!set_.exchange(true, std::memory_order_relaxed)) {
done_.count_down();
}
}
private:
std::latch done_{1};
std::atomic<bool> set_;
};
struct EchoHandler : ConnectionHandler { struct EchoHandler : ConnectionHandler {
Arena arena; Arena arena;
std::span<std::string_view> reply; std::span<std::string_view> reply;
WeakRef<MessageSender> wconn; WeakRef<MessageSender> wconn;
Event done; std::latch done{1};
void on_data_arrived(std::string_view data, Connection &conn) override { void on_data_arrived(std::string_view data, Connection &conn) override {
reply = arena.allocate_span<std::string_view>(1); reply = arena.allocate_span<std::string_view>(1);
reply[0] = arena.copy_string(data); reply[0] = arena.copy_string(data);
wconn = conn.get_weak_ref(); wconn = conn.get_weak_ref();
CHECK(wconn.lock()); CHECK(wconn.lock());
done.set(); done.count_down();
} }
}; };
@@ -74,8 +60,8 @@ struct ShutdownTestHandler : ConnectionHandler {
Arena arena; Arena arena;
std::span<std::string_view> reply; std::span<std::string_view> reply;
WeakRef<MessageSender> wconn; WeakRef<MessageSender> wconn;
Event received_data; std::latch received_data{1};
Event connection_closed_latch; std::latch connection_closed_latch{1};
ConnectionShutdown shutdown_mode = ConnectionShutdown::None; ConnectionShutdown shutdown_mode = ConnectionShutdown::None;
std::atomic<bool> connection_closed{false}; std::atomic<bool> connection_closed{false};
@@ -83,12 +69,12 @@ struct ShutdownTestHandler : ConnectionHandler {
reply = arena.allocate_span<std::string_view>(1); reply = arena.allocate_span<std::string_view>(1);
reply[0] = arena.copy_string(data); reply[0] = arena.copy_string(data);
wconn = conn.get_weak_ref(); wconn = conn.get_weak_ref();
received_data.set(); received_data.count_down();
} }
void on_connection_closed(Connection &) override { void on_connection_closed(Connection &) override {
connection_closed = true; connection_closed = true;
connection_closed_latch.set(); connection_closed_latch.count_down();
} }
}; };
+1 -1
View File
@@ -78,7 +78,7 @@ def check_snake_case_violations(filepath, check_new_only=True):
# Common HTTP parser callback names (external API) # Common HTTP parser callback names (external API)
r"\b(onUrl|onHeaderField|onHeaderFieldComplete|onHeaderValue|onHeaderValueComplete|onHeadersComplete|onBody|onMessageComplete)\b", r"\b(onUrl|onHeaderField|onHeaderFieldComplete|onHeaderValue|onHeaderValueComplete|onHeadersComplete|onBody|onMessageComplete)\b",
# Known legacy APIs we can't easily change # Known legacy APIs we can't easily change
r"\b(user_data|get_arena|send_response)\b", r"\b(user_data|get_arena|append_message)\b",
] ]
try: try: