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
run: |
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
run: |
+6 -6
View File
@@ -264,7 +264,7 @@ CommitRequest {
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 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
> **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();
// 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
}
@@ -430,7 +430,7 @@ public:
work_queue.push([weak_conn, data = std::string(data)]() {
// Process asynchronously - connection may be closed
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:
void on_connection_established(Connection &conn) override {
// Write an initial "y\n"
conn.send_response("y\n");
conn.append_message("y\n");
}
void on_write_progress(Connection &conn) override {
@@ -491,7 +491,7 @@ public:
// Don't use an unbounded amount of memory
conn.reset();
// 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]() {
// Do work...
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
});
+6 -4
View File
@@ -335,7 +335,7 @@ void CommitPipeline::run_release_stage(int thread_index) {
// Send the JSON response using protocol-agnostic interface
// 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,
std::move(commit_entry.request_arena));
} 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
// 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,
std::move(status_entry.request_arena));
} 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
// HTTP formatting will happen in on_preprocess_writes()
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));
} else if constexpr (std::is_same_v<T, GetVersionEntry>) {
auto &get_version_entry = e;
@@ -377,7 +378,8 @@ void CommitPipeline::run_release_stage(int thread_index) {
// Send the response using protocol-agnostic interface
// HTTP formatting will happen in on_preprocess_writes()
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));
}
},
+7 -9
View File
@@ -1,6 +1,7 @@
#include "connection.hpp"
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
#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
// call that clears the write interest could get reordered with one that
// sets it and we would hang.
epoll_ctl(server->event_loops_[epoll_index_].epoll_fd_, EPOLL_CTL_MOD,
fd_, &event);
epoll_ctl(server->epoll_fds_[epoll_index_], EPOLL_CTL_MOD, fd_, &event);
}
}
}
// 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::unique_lock lock(mutex_);
@@ -136,7 +136,7 @@ void Connection::send_response(ProtocolHandle handle,
// Store response in queue for protocol handler processing
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
if (pending_response_queue_.size() == 1) {
@@ -147,13 +147,12 @@ void Connection::send_response(ProtocolHandle handle,
event.data.fd = fd_;
event.events = EPOLLIN | EPOLLOUT;
tsan_release();
epoll_ctl(server->event_loops_[epoll_index_].epoll_fd_, EPOLL_CTL_MOD,
fd_, &event);
epoll_ctl(server->epoll_fds_[epoll_index_], EPOLL_CTL_MOD, fd_, &event);
}
}
}
int Connection::read_bytes(char *buf, size_t buffer_size) {
int Connection::readBytes(char *buf, size_t buffer_size) {
int r;
for (;;) {
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
// call that clears the write interest could get reordered with one that
// sets it and we would hang.
epoll_ctl(server->event_loops_[epoll_index_].epoll_fd_, EPOLL_CTL_MOD,
fd_, &event);
epoll_ctl(server->epoll_fds_[epoll_index_], EPOLL_CTL_MOD, fd_, &event);
}
// Handle shutdown modes after all messages are sent
if (shutdown_requested_ == ConnectionShutdown::WriteOnly) {
+16 -17
View File
@@ -1,6 +1,5 @@
#pragma once
#include <atomic>
#include <cassert>
#include <cstring>
#include <deque>
@@ -33,31 +32,32 @@ enum class ConnectionShutdown {
/**
* Base interface for sending messages to a connection.
* 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
* send responses without accessing other connection functionality
* that should only be used by the I/O thread.
*/
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.
* Delegates to the connection's protocol handler for ordering logic.
* The protocol handler may queue the response or send it immediately.
*
* @param handle Protocol-specific handle for correlating this response
* @param response_json JSON response body (may be empty for deferred
* serialization)
* @param protocol_context Arena-allocated protocol-specific context
* @param data Response data parts (may be empty for deferred serialization)
* @param arena Arena containing response data and context
*
* Example usage:
* ```cpp
* ProtocolHandle handle = handler.allocate_response_context(arena);
* conn.send_response(handle, response_json, std::move(arena));
* auto* ctx = arena.allocate<HttpResponseContext>();
* 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;
virtual ~MessageSender() = default;
@@ -76,9 +76,9 @@ struct MessageSender {
*
* Threading model:
* - 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
* - 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
*
* Arena allocator usage:
@@ -140,7 +140,7 @@ struct Connection : MessageSender {
append_bytes(std::span<std::string_view> data_parts, Arena arena,
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;
/**
@@ -165,7 +165,7 @@ struct Connection : MessageSender {
* if (auto conn = weak_conn.lock()) {
* Arena 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
* descriptor, and associated handler. Automatically increments the global
* active connection counter. The caller (Server) is responsible for
* initializing the self weak reference and invoking
* on_connection_established().
* active connection counter and calls the handler's
* on_connection_established() method.
*
* @param addr Network address of the remote client (IPv4/IPv6 compatible)
* @param fd File descriptor for the socket connection
@@ -279,7 +278,7 @@ private:
friend Ref<T> make_ref(Args &&...args);
// 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 {
Error = 1 << 0,
Progress = 1 << 1,
+2 -8
View File
@@ -8,19 +8,13 @@ struct Connection;
// Include Arena header since PendingResponse uses Arena by value
#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.
* Contains JSON response data that can be wrapped by any protocol.
*/
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)
Arena arena; // Arena containing response data and context
};
@@ -48,7 +42,7 @@ public:
* Implementation should:
* - Create request-scoped Arena for parsing and response generation
* - 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
* - 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_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
void HttpHandler::on_connection_established(Connection &conn) {
// 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
{
for (auto &pending : pending_responses) {
auto *ctx = state->resolve_response_context(pending.handle);
// Handle stale or invalid handles defensively
if (!ctx) {
continue;
}
auto *ctx = static_cast<HttpResponseContext *>(pending.protocol_context);
// Determine HTTP status code and content type from response content
int status_code = 200;
@@ -153,8 +96,9 @@ void HttpHandler::on_preprocess_writes(
status_code, content_type, pending.response_json, pending.arena,
ctx->http_request_id, ctx->connection_close);
state->send_ordered_response(conn, ctx, http_response,
std::move(pending.arena));
state->send_ordered_response(conn, ctx->sequence_id, http_response,
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();
req.sequence_id = sequence_id;
// Create HttpResponseContext for this request; sequence_id doubles as the
// ProtocolHandle for async response correlation. The context is
// registered in HttpConnectionState so async responses can resolve it by
// handle.
auto ctx = std::make_unique<HttpResponseContext>();
// Create HttpResponseContext for this request
auto *ctx = req.arena.allocate<HttpResponseContext>(1);
ctx->sequence_id = sequence_id;
ctx->http_request_id = req.http_request_id;
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;
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 http_response =
format_json_response(400, json_response, req.arena, 0, true);
ctx_ptr->connection_close = true;
state->send_ordered_response(*conn, ctx_ptr, http_response,
std::move(req.arena));
state->send_ordered_response(*conn, ctx->sequence_id, http_response,
std::move(req.arena),
ctx->connection_close);
break;
}
req.route = route_match.route;
@@ -239,25 +176,25 @@ void HttpHandler::on_batch_complete(std::span<Connection *const> batch) {
// Create CommitEntry for commit requests
if (req.route == HttpRoute::PostCommit && req.commit_request &&
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(),
std::move(req.arena)));
}
// Create StatusEntry for status requests
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,
std::move(req.arena)));
}
// Create HealthCheckEntry for health check requests
else if (req.route == HttpRoute::GetOk) {
g_batch_entries.emplace_back(HealthCheckEntry(
conn->get_weak_ref(), handle, std::move(req.arena)));
g_batch_entries.emplace_back(
HealthCheckEntry(conn->get_weak_ref(), ctx, std::move(req.arena)));
}
// Create GetVersionEntry for version requests
else if (req.route == HttpRoute::GetVersion) {
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()));
}
}
@@ -298,17 +235,13 @@ void HttpHandler::on_data_arrived(std::string_view data, Connection &conn) {
break;
}
// 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 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 =
format_json_response(400, json_response, state->pending.arena,
ctx->http_request_id, ctx->connection_close);
state->send_ordered_response(conn, ctx.get(), http_response,
std::move(state->pending.arena));
format_json_response(400, json_response, state->pending.arena, 0, true);
state->send_ordered_response(conn, state->get_next_sequence_id(),
http_response, std::move(state->pending.arena),
true);
return;
}
}
@@ -322,18 +255,17 @@ void HttpHandler::handle_get_version(Connection &, HttpRequestState &) {
void HttpHandler::handle_post_commit(Connection &conn,
HttpRequestState &state) {
commit_counter.inc();
auto *ctx = state.response_context;
assert(ctx);
// Check if streaming parse was successful
if (!state.commit_request || !state.parsing_commit) {
auto json_response = R"({"error":"Parse failed"})";
auto http_response =
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);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena),
state.connection_close);
return;
}
@@ -378,11 +310,12 @@ void HttpHandler::handle_post_commit(Connection &conn,
static_cast<int>(error_msg.size()), error_msg.data());
auto http_response =
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);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena),
state.connection_close);
return;
}
@@ -393,25 +326,22 @@ void HttpHandler::handle_post_commit(Connection &conn,
void HttpHandler::handle_get_subscribe(Connection &conn,
HttpRequestState &state) {
auto *ctx = state.response_context;
assert(ctx);
// TODO: Implement subscription streaming
auto json_response =
R"({"message":"Subscription endpoint - streaming not yet implemented"})";
auto http_response =
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);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena),
state.connection_close);
}
void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state,
const RouteMatch &route_match) {
status_counter.inc();
auto *ctx = state.response_context;
assert(ctx);
// Status requests are processed through the pipeline
// Response will be generated in the sequence stage
// 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"})";
auto http_response =
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
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena),
state.connection_close);
return;
}
@@ -437,12 +368,13 @@ void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state,
auto json_response = R"({"error":"Empty request_id parameter"})";
auto http_response =
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
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena),
state.connection_close);
return;
}
@@ -455,58 +387,53 @@ void HttpHandler::handle_get_status(Connection &conn, HttpRequestState &state,
void HttpHandler::handle_put_retention(Connection &conn,
HttpRequestState &state,
const RouteMatch &) {
auto *ctx = state.response_context;
assert(ctx);
// TODO: Parse retention policy from body and store
auto json_response = R"({"policy_id":"example","status":"created"})";
auto http_response =
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
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena),
state.connection_close);
}
void HttpHandler::handle_get_retention(Connection &conn,
HttpRequestState &state,
const RouteMatch &) {
auto *ctx = state.response_context;
assert(ctx);
// TODO: Extract policy_id from URL or return all policies
auto json_response = R"({"policies":[]})";
auto http_response =
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
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena),
state.connection_close);
}
void HttpHandler::handle_delete_retention(Connection &conn,
HttpRequestState &state,
const RouteMatch &) {
auto *ctx = state.response_context;
assert(ctx);
// TODO: Extract policy_id from URL and delete
auto json_response = R"({"policy_id":"example","status":"deleted"})";
auto http_response =
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
auto *conn_state = static_cast<HttpConnectionState *>(conn.user_data);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
std::move(state.arena),
state.connection_close);
}
void HttpHandler::handle_get_metrics(Connection &conn,
HttpRequestState &state) {
auto *ctx = state.response_context;
assert(ctx);
metrics_counter.inc();
auto metrics_span = metric::render(state.arena);
@@ -523,19 +450,19 @@ void HttpHandler::handle_get_metrics(Connection &conn,
// Build HTTP headers
std::string_view headers;
if (ctx->connection_close) {
if (state.connection_close) {
headers = static_format(
state.arena, "HTTP/1.1 200 OK\r\n",
"Content-Type: text/plain; version=0.0.4\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");
} else {
headers = static_format(
state.arena, "HTTP/1.1 200 OK\r\n",
"Content-Type: text/plain; version=0.0.4\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");
}
@@ -545,7 +472,9 @@ void HttpHandler::handle_get_metrics(Connection &conn,
}
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 &) {
@@ -556,17 +485,41 @@ void HttpHandler::handle_get_ok(Connection &, HttpRequestState &) {
}
void HttpHandler::handle_not_found(Connection &conn, HttpRequestState &state) {
auto *ctx = state.response_context;
assert(ctx);
not_found_counter.inc();
auto json_response = R"({"error":"Not found"})";
auto http_response =
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);
conn_state->send_ordered_response(conn, ctx, http_response,
std::move(state.arena));
conn_state->send_ordered_response(conn, state.sequence_id, http_response,
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>
+13 -26
View File
@@ -1,7 +1,6 @@
#pragma once
#include <map>
#include <memory>
#include <string_view>
#include <llhttp.h>
@@ -20,18 +19,22 @@ struct RouteMatch;
/**
* HTTP-specific response context stored in pipeline entries.
* Arena-allocated and identified by a ProtocolHandle (sequence_id).
* Also owns the response data once it becomes ready.
* Arena-allocated and passed through pipeline for response correlation.
*/
struct HttpResponseContext {
int64_t sequence_id; // For response ordering in pipelining
int64_t http_request_id; // For X-Response-ID header
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;
Arena arena;
bool connection_close;
};
/**
@@ -66,10 +69,6 @@ struct HttpRequestState {
0; // X-Request-Id header value (for tracing/logging)
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
Arena::Ptr<JsonCommitRequestParser> commit_parser;
Arena::Ptr<CommitRequest> commit_request;
@@ -90,27 +89,15 @@ struct HttpConnectionState {
int64_t get_next_sequence_id() { return next_sequence_id++; }
HttpConnectionState();
~HttpConnectionState();
// Register an HttpResponseContext so the pipeline can resolve it by handle.
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,
void send_ordered_response(Connection &conn, int64_t sequence_id,
std::span<std::string_view> http_response,
Arena arena);
Arena arena, bool close_connection);
private:
// All registered response contexts keyed by sequence_id.
// A context stays here until its response has been dispatched.
std::map<int64_t, std::unique_ptr<HttpResponseContext>> contexts_;
// Response ordering for HTTP pipelining
std::map<int64_t, ResponseData>
ready_responses; // sequence_id -> response data
int64_t next_sequence_to_send = 0;
int64_t next_sequence_id = 0;
};
+1 -1
View File
@@ -3,7 +3,7 @@
#include <memory>
#include <simdutf.h>
#include <weaseljson.h>
#include <weaseljson/weaseljson.h>
#include "commit_request_parser.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 persist_success = false; // Set by persist stage
// Protocol-agnostic handle for correlating the response
ProtocolHandle handle = -1;
// Protocol-agnostic context (arena-allocated, protocol-specific)
void *protocol_context = nullptr;
const CommitRequest *commit_request = nullptr; // Points to request_arena data
// Request arena contains parsed request data and response data
@@ -28,9 +28,9 @@ struct CommitEntry {
std::string_view response_json;
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)
: connection(std::move(conn)), handle(handle), commit_request(req),
: connection(std::move(conn)), protocol_context(ctx), commit_request(req),
request_arena(std::move(arena)) {}
};
@@ -42,8 +42,8 @@ struct StatusEntry {
WeakRef<MessageSender> connection;
int64_t version_upper_bound = 0; // Set by sequence stage
// Protocol-agnostic handle for correlating the response
ProtocolHandle handle = -1;
// Protocol-agnostic context (arena-allocated, protocol-specific)
void *protocol_context = nullptr;
std::string_view status_request_id; // Points to request_arena data
// Request arena for request data
@@ -53,9 +53,9 @@ struct StatusEntry {
std::string_view response_json;
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)
: connection(std::move(conn)), handle(handle),
: connection(std::move(conn)), protocol_context(ctx),
status_request_id(request_id), request_arena(std::move(arena)) {}
};
@@ -67,8 +67,8 @@ struct StatusEntry {
struct HealthCheckEntry {
WeakRef<MessageSender> connection;
// Protocol-agnostic handle for correlating the response
ProtocolHandle handle = -1;
// Protocol-agnostic context (arena-allocated, protocol-specific)
void *protocol_context = nullptr;
// Request arena for response data
Arena request_arena;
@@ -77,9 +77,8 @@ struct HealthCheckEntry {
std::string_view response_json;
HealthCheckEntry() = default; // Default constructor for variant
explicit HealthCheckEntry(WeakRef<MessageSender> conn, ProtocolHandle handle,
Arena arena)
: connection(std::move(conn)), handle(handle),
explicit HealthCheckEntry(WeakRef<MessageSender> conn, void *ctx, Arena arena)
: connection(std::move(conn)), protocol_context(ctx),
request_arena(std::move(arena)) {}
};
@@ -90,8 +89,8 @@ struct HealthCheckEntry {
struct GetVersionEntry {
WeakRef<MessageSender> connection;
// Protocol-agnostic handle for correlating the response
ProtocolHandle handle = -1;
// Protocol-agnostic context (arena-allocated, protocol-specific)
void *protocol_context = nullptr;
// Request arena for response data
Arena request_arena;
@@ -103,9 +102,9 @@ struct GetVersionEntry {
int64_t version;
GetVersionEntry() = default; // Default constructor for variant
explicit GetVersionEntry(WeakRef<MessageSender> conn, ProtocolHandle handle,
Arena arena, int64_t version)
: connection(std::move(conn)), handle(handle),
explicit GetVersionEntry(WeakRef<MessageSender> conn, void *ctx, Arena arena,
int64_t version)
: connection(std::move(conn)), protocol_context(ctx),
request_arena(std::move(arena)), version(version) {}
};
+25 -33
View File
@@ -1,5 +1,6 @@
#include "server.hpp"
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -74,7 +75,7 @@ Server::~Server() {
}
// Close all epoll instances
for (auto [epollfd] : event_loops_) {
for (int epollfd : epoll_fds_) {
if (epollfd != -1) {
int e = close(epollfd);
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)
for (int fd : listen_fds_) {
@@ -166,7 +167,7 @@ int Server::create_local_connection() {
// Use round-robin distribution for local connections across epoll instances
size_t epoll_index =
connection_distribution_counter_.fetch_add(1, std::memory_order_relaxed) %
event_loops_.size();
epoll_fds_.size();
// Create Connection object
auto connection = make_ref<Connection>(
@@ -183,7 +184,7 @@ int Server::create_local_connection() {
event.events = EPOLLIN;
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) {
perror("epoll_ctl ADD local connection");
connection_registry_.remove(server_fd);
@@ -220,11 +221,11 @@ void Server::setup_shutdown_pipe() {
void Server::create_epoll_instances() {
// Create one epoll instance per I/O thread (1:1 mapping) to eliminate
// 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) {
event_loops_[i].epoll_fd_ = epoll_create1(EPOLL_CLOEXEC);
if (event_loops_[i].epoll_fd_ == -1) {
epoll_fds_[i] = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fds_[i] == -1) {
perror("epoll_create1");
std::abort();
}
@@ -234,7 +235,7 @@ void Server::create_epoll_instances() {
shutdown_event.events = EPOLLIN;
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) {
perror("epoll_ctl shutdown pipe");
std::abort();
@@ -246,8 +247,8 @@ void Server::create_epoll_instances() {
struct epoll_event listen_event;
listen_event.events = EPOLLIN | EPOLLEXCLUSIVE;
listen_event.data.fd = listen_fd;
if (epoll_ctl(event_loops_[i].epoll_fd_, EPOLL_CTL_ADD, listen_fd,
&listen_event) == -1) {
if (epoll_ctl(epoll_fds_[i], EPOLL_CTL_ADD, listen_fd, &listen_event) ==
-1) {
perror("epoll_ctl listen socket");
std::abort();
}
@@ -264,7 +265,7 @@ void Server::start_io_threads(std::vector<std::thread> &threads) {
("io-" + std::to_string(thread_id)).c_str());
// 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<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;
g_read_buffer.resize(buf_size);
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.
for (;;) {
int r = conn->read_bytes(buf, buf_size);
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) {
// Error or EOF - connection should be closed
close_connection(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
int shutdown_pipe_[2] = {-1, -1};
struct EventLoopState {
int epoll_fd_;
};
// 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>
listen_fds_; // FDs to accept connections on (Server owns these)
// Private helper methods
void setup_shutdown_pipe();
void setup_signal_handling();
void create_epoll_instances();
void start_io_threads(std::vector<std::thread> &threads);
+6 -20
View File
@@ -3,36 +3,22 @@
#include "connection_handler.hpp"
#include "server.hpp"
#include <atomic>
#include <doctest/doctest.h>
#include <latch>
#include <string_view>
#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 {
Arena arena;
std::span<std::string_view> reply;
WeakRef<MessageSender> wconn;
Event done;
std::latch done{1};
void on_data_arrived(std::string_view data, Connection &conn) override {
reply = arena.allocate_span<std::string_view>(1);
reply[0] = arena.copy_string(data);
wconn = conn.get_weak_ref();
CHECK(wconn.lock());
done.set();
done.count_down();
}
};
@@ -74,8 +60,8 @@ struct ShutdownTestHandler : ConnectionHandler {
Arena arena;
std::span<std::string_view> reply;
WeakRef<MessageSender> wconn;
Event received_data;
Event connection_closed_latch;
std::latch received_data{1};
std::latch connection_closed_latch{1};
ConnectionShutdown shutdown_mode = ConnectionShutdown::None;
std::atomic<bool> connection_closed{false};
@@ -83,12 +69,12 @@ struct ShutdownTestHandler : ConnectionHandler {
reply = arena.allocate_span<std::string_view>(1);
reply[0] = arena.copy_string(data);
wconn = conn.get_weak_ref();
received_data.set();
received_data.count_down();
}
void on_connection_closed(Connection &) override {
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)
r"\b(onUrl|onHeaderField|onHeaderFieldComplete|onHeaderValue|onHeaderValueComplete|onHeadersComplete|onBody|onMessageComplete)\b",
# 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: