diff --git a/examples/http2_client.cpp b/examples/http2_client.cpp index ec7d55d..2e31027 100644 --- a/examples/http2_client.cpp +++ b/examples/http2_client.cpp @@ -27,13 +27,13 @@ std::mutex g_mutex; std::condition_variable g_cv; /// Perform HTTP/2 requests demonstrating various features -coro::task run_client(io::io_context& io_ctx, const std::string& base_url) { +coro::task run_client(const std::string& base_url) { // Create HTTP/2 client with custom config h2_client_config config; config.user_agent = "elio-http2-client-example/1.0"; config.max_concurrent_streams = 100; - - h2_client client(io_ctx, config); + + h2_client client(config); ELIO_LOG_INFO("=== HTTP/2 Client Example ==="); ELIO_LOG_INFO("Base URL: {}", base_url); @@ -89,11 +89,11 @@ coro::task run_client(io::io_context& io_ctx, const std::string& base_url) } /// Simple one-off HTTP/2 request demonstration -coro::task simple_request(io::io_context& io_ctx, const std::string& url) { +coro::task simple_request(const std::string& url) { ELIO_LOG_INFO("Fetching via HTTP/2: {}", url); - + // Use convenience function for one-off requests - auto result = co_await h2_get(io_ctx, url); + auto result = co_await h2_get(url); if (result) { auto& resp = *result; @@ -155,10 +155,10 @@ int main(int argc, char* argv[]) { // Run appropriate mode if (full_demo) { - auto task = run_client(io::default_io_context(), url); + auto task = run_client(url); sched.spawn(task.release()); } else { - auto task = simple_request(io::default_io_context(), url); + auto task = simple_request(url); sched.spawn(task.release()); } diff --git a/examples/http_client.cpp b/examples/http_client.cpp index 7b20316..ef5475b 100644 --- a/examples/http_client.cpp +++ b/examples/http_client.cpp @@ -25,14 +25,14 @@ std::mutex g_mutex; std::condition_variable g_cv; /// Perform multiple HTTP requests demonstrating various features -coro::task run_client(io::io_context& io_ctx, const std::string& base_url) { +coro::task run_client(const std::string& base_url) { // Create client with custom config client_config config; config.user_agent = "elio-http-client-example/1.0"; config.follow_redirects = true; config.max_redirects = 5; - - client c(io_ctx, config); + + client c(config); ELIO_LOG_INFO("=== HTTP Client Example ==="); ELIO_LOG_INFO("Base URL: {}", base_url); @@ -184,11 +184,11 @@ coro::task run_client(io::io_context& io_ctx, const std::string& base_url) } /// Simple one-off request demonstration -coro::task simple_request(io::io_context& io_ctx, const std::string& url) { +coro::task simple_request(const std::string& url) { ELIO_LOG_INFO("Fetching: {}", url); - + // Use convenience function for one-off requests - auto result = co_await http::get(io_ctx, url); + auto result = co_await http::get(url); if (result) { auto& resp = *result; @@ -240,10 +240,10 @@ int main(int argc, char* argv[]) { // Run appropriate mode if (full_demo) { - auto task = run_client(io::default_io_context(), url); + auto task = run_client(url); sched.spawn(task.release()); } else { - auto task = simple_request(io::default_io_context(), url); + auto task = simple_request(url); sched.spawn(task.release()); } diff --git a/examples/http_server.cpp b/examples/http_server.cpp index 756b37d..a51320e 100644 --- a/examples/http_server.cpp +++ b/examples/http_server.cpp @@ -268,16 +268,14 @@ int main(int argc, char* argv[]) { if (use_https) { try { auto tls_ctx = tls::tls_context::make_server(cert_file, key_file); - + auto server_task = srv.listen_tls( - bind_addr, - io::default_io_context(), - sched, + bind_addr, tls_ctx, opts ); sched.spawn(server_task.release()); - + ELIO_LOG_INFO("HTTPS server started on {}", bind_addr.to_string()); } catch (const std::exception& e) { ELIO_LOG_ERROR("Failed to start HTTPS server: {}", e.what()); @@ -285,13 +283,11 @@ int main(int argc, char* argv[]) { } } else { auto server_task = srv.listen( - bind_addr, - io::default_io_context(), - sched, + bind_addr, opts ); sched.spawn(server_task.release()); - + ELIO_LOG_INFO("HTTP server started on {}", bind_addr.to_string()); } diff --git a/examples/rpc_server_example.cpp b/examples/rpc_server_example.cpp index 25bee20..dddedef 100644 --- a/examples/rpc_server_example.cpp +++ b/examples/rpc_server_example.cpp @@ -188,10 +188,8 @@ task signal_handler_task() { } task server_main(uint16_t port, [[maybe_unused]] scheduler& sched) { - auto& ctx = io::default_io_context(); - // Bind TCP listener - auto listener_result = tcp_listener::bind(ipv4_address(port), ctx); + auto listener_result = tcp_listener::bind(ipv4_address(port)); if (!listener_result) { ELIO_LOG_ERROR("Failed to bind to port {}: {}", port, strerror(errno)); co_return; diff --git a/examples/sse_client.cpp b/examples/sse_client.cpp index a3d26d2..8163b59 100644 --- a/examples/sse_client.cpp +++ b/examples/sse_client.cpp @@ -31,18 +31,18 @@ std::mutex g_mutex; std::condition_variable g_cv; /// Listen to SSE events -coro::task listen_events(io::io_context& io_ctx, const std::string& url) { +coro::task listen_events(const std::string& url) { ELIO_LOG_INFO("=== SSE Client Demo ==="); ELIO_LOG_INFO("Connecting to: {}", url); - + // Configure client client_config config; config.user_agent = "elio-sse-demo/1.0"; config.auto_reconnect = true; config.default_retry_ms = 3000; config.verify_certificate = false; // Allow self-signed certs for testing - - sse_client client(io_ctx, config); + + sse_client client(config); // Connect bool connected = co_await client.connect(url); @@ -105,10 +105,10 @@ coro::task listen_events(io::io_context& io_ctx, const std::string& url) { } /// Simple connection test -coro::task simple_test(io::io_context& io_ctx, const std::string& url) { +coro::task simple_test(const std::string& url) { ELIO_LOG_INFO("Simple SSE test: connecting to {}", url); - - auto client_opt = co_await sse_connect(io_ctx, url); + + auto client_opt = co_await sse_connect(url); if (!client_opt) { ELIO_LOG_ERROR("Failed to connect"); { @@ -145,15 +145,15 @@ coro::task simple_test(io::io_context& io_ctx, const std::string& url) { } /// Test reconnection behavior -coro::task reconnect_test(io::io_context& io_ctx, const std::string& url) { +coro::task reconnect_test(const std::string& url) { ELIO_LOG_INFO("Reconnection test: connecting to {}", url); - + client_config config; config.auto_reconnect = true; config.default_retry_ms = 2000; config.max_reconnect_attempts = 3; - - sse_client client(io_ctx, config); + + sse_client client(config); if (!co_await client.connect(url)) { ELIO_LOG_ERROR("Initial connection failed"); @@ -240,17 +240,17 @@ int main(int argc, char* argv[]) { // Run client based on mode switch (mode) { case Mode::demo: { - auto task = listen_events(io::default_io_context(), url); + auto task = listen_events(url); sched.spawn(task.release()); break; } case Mode::simple: { - auto task = simple_test(io::default_io_context(), url); + auto task = simple_test(url); sched.spawn(task.release()); break; } case Mode::reconnect: { - auto task = reconnect_test(io::default_io_context(), url); + auto task = reconnect_test(url); sched.spawn(task.release()); break; } diff --git a/examples/sse_server.cpp b/examples/sse_server.cpp index 7ad7fe6..21b1e26 100644 --- a/examples/sse_server.cpp +++ b/examples/sse_server.cpp @@ -119,9 +119,14 @@ class sse_http_server { explicit sse_http_server(router r, server_config config = {}) : router_(std::move(r)), config_(config) {} - coro::task listen(const net::ipv4_address& addr, io::io_context& io_ctx, - runtime::scheduler& sched) { - auto listener_result = net::tcp_listener::bind(addr, io_ctx); + coro::task listen(const net::ipv4_address& addr) { + auto* sched = runtime::scheduler::current(); + if (!sched) { + ELIO_LOG_ERROR("SSE server must be started from within a scheduler context"); + co_return; + } + + auto listener_result = net::tcp_listener::bind(addr); if (!listener_result) { ELIO_LOG_ERROR("Failed to bind SSE server: {}", strerror(errno)); co_return; @@ -142,7 +147,7 @@ class sse_http_server { } auto handler = handle_connection(std::move(*stream_result)); - sched.spawn(handler.release()); + sched->spawn(handler.release()); } } @@ -373,11 +378,7 @@ int main(int argc, char* argv[]) { sched.spawn(sig_handler.release()); // Start server - auto server_task = srv.listen( - net::ipv4_address(port), - io::default_io_context(), - sched - ); + auto server_task = srv.listen(net::ipv4_address(port)); sched.spawn(server_task.release()); ELIO_LOG_INFO("SSE server started on port {}", port); diff --git a/examples/tcp_echo_server.cpp b/examples/tcp_echo_server.cpp index fe5eaa2..0cb25db 100644 --- a/examples/tcp_echo_server.cpp +++ b/examples/tcp_echo_server.cpp @@ -98,11 +98,8 @@ task handle_client(tcp_stream stream, int client_id) { /// Main server loop - accepts connections and spawns handlers task server_main(const socket_address& bind_addr, const tcp_options& opts, scheduler& sched) { - // Use the default io_context which is polled by scheduler workers - auto& ctx = io::default_io_context(); - // Bind TCP listener - auto listener_result = tcp_listener::bind(bind_addr, ctx, opts); + auto listener_result = tcp_listener::bind(bind_addr, opts); if (!listener_result) { ELIO_LOG_ERROR("Failed to bind to {}: {}", bind_addr.to_string(), strerror(errno)); diff --git a/examples/uds_echo_server.cpp b/examples/uds_echo_server.cpp index f71b3a1..994fff8 100644 --- a/examples/uds_echo_server.cpp +++ b/examples/uds_echo_server.cpp @@ -88,14 +88,11 @@ task handle_client(uds_stream stream, int client_id) { /// Main server loop - accepts connections and spawns handlers task server_main(const unix_address& addr, scheduler& sched) { - // Use the default io_context which is polled by scheduler workers - auto& ctx = io::default_io_context(); - // Bind UDS listener uds_options opts; opts.unlink_on_bind = true; // Remove existing socket file if any - - auto listener_result = uds_listener::bind(addr, ctx, opts); + + auto listener_result = uds_listener::bind(addr, opts); if (!listener_result) { ELIO_LOG_ERROR("Failed to bind to {}: {}", addr.to_string(), strerror(errno)); diff --git a/examples/websocket_client.cpp b/examples/websocket_client.cpp index ac6c0da..b260fd0 100644 --- a/examples/websocket_client.cpp +++ b/examples/websocket_client.cpp @@ -73,16 +73,16 @@ coro::task interactive_session(ws_client& client) { } /// Demonstrate various WebSocket features -coro::task demo_features(io::io_context& io_ctx, const std::string& url) { +coro::task demo_features(const std::string& url) { ELIO_LOG_INFO("=== WebSocket Client Demo ==="); ELIO_LOG_INFO("Connecting to: {}", url); - + // Configure client client_config config; config.user_agent = "elio-websocket-demo/1.0"; config.verify_certificate = false; // Allow self-signed certs for testing - - ws_client client(io_ctx, config); + + ws_client client(config); // Connect bool connected = co_await client.connect(url); @@ -176,10 +176,10 @@ coro::task demo_features(io::io_context& io_ctx, const std::string& url) { } /// Simple echo test -coro::task echo_test(io::io_context& io_ctx, const std::string& url) { +coro::task echo_test(const std::string& url) { ELIO_LOG_INFO("Echo test: connecting to {}", url); - - auto client_opt = co_await ws_connect(io_ctx, url); + + auto client_opt = co_await ws_connect(url); if (!client_opt) { ELIO_LOG_ERROR("Failed to connect"); { @@ -262,10 +262,10 @@ int main(int argc, char* argv[]) { // Run client if (demo_mode) { - auto task = demo_features(io::default_io_context(), url); + auto task = demo_features(url); sched.spawn(task.release()); } else { - auto task = echo_test(io::default_io_context(), url); + auto task = echo_test(url); sched.spawn(task.release()); } diff --git a/examples/websocket_server.cpp b/examples/websocket_server.cpp index e004755..c02c3d2 100644 --- a/examples/websocket_server.cpp +++ b/examples/websocket_server.cpp @@ -316,11 +316,7 @@ int main(int argc, char* argv[]) { sched.spawn(sig_handler.release()); // Start server - auto server_task = srv.listen( - net::ipv4_address(port), - io::default_io_context(), - sched - ); + auto server_task = srv.listen(net::ipv4_address(port)); sched.spawn(server_task.release()); ELIO_LOG_INFO("WebSocket server started on port {}", port); diff --git a/include/elio/coro/frame_allocator.hpp b/include/elio/coro/frame_allocator.hpp index a5ffabb..204ba8d 100644 --- a/include/elio/coro/frame_allocator.hpp +++ b/include/elio/coro/frame_allocator.hpp @@ -5,20 +5,26 @@ #include #include #include +#include namespace elio::coro { /// Thread-local free-list based frame allocator for small coroutine frames /// Dramatically reduces allocation overhead for frequently created/destroyed coroutines -/// +/// +/// Design: Each allocated frame has a hidden header storing the source pool ID. +/// When deallocated on a different thread, the frame is returned via an MPSC queue +/// to its source pool. This handles work-stealing scenarios where coroutines +/// are allocated on thread A but deallocated on thread B. +/// /// Note: Under sanitizers, pooling is disabled to allow proper leak/error detection. -/// This is because coroutines may be allocated on one thread and deallocated on another -/// due to work stealing, which can confuse thread-local pooling. class frame_allocator { public: // Support frames up to 256 bytes (covers most simple tasks) + // Actual allocation includes header, so user-visible size is MAX_FRAME_SIZE static constexpr size_t MAX_FRAME_SIZE = 256; static constexpr size_t POOL_SIZE = 1024; + static constexpr size_t REMOTE_QUEUE_BATCH = 64; // Process remote returns in batches // Detect sanitizers: GCC uses __SANITIZE_*, Clang uses __has_feature #if defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__) @@ -42,45 +48,208 @@ class frame_allocator { static void* allocate(size_t size) { if (size <= MAX_FRAME_SIZE) { auto& alloc = instance(); + + // First try to reclaim remote returns periodically + alloc.reclaim_remote_returns(); + if (alloc.free_count_ > 0) { - return alloc.pool_[--alloc.free_count_]; + void* block = alloc.pool_[--alloc.free_count_]; + // Update header to reflect current pool ownership + // This is important because blocks may have been returned from remote threads + auto* header = static_cast(block); + header->source_pool_id = alloc.pool_id_; + return block_to_user(block); } - // Allocate MAX_FRAME_SIZE so pooled blocks are always large enough - return ::operator new(MAX_FRAME_SIZE); + + // Allocate new block with header + void* block = ::operator new(ALLOC_BLOCK_SIZE); + auto* header = static_cast(block); + header->source_pool_id = alloc.pool_id_; + header->next.store(nullptr, std::memory_order_relaxed); + return block_to_user(block); } - // Fall back to standard allocation for large frames + // Fall back to standard allocation for large frames (no header) return ::operator new(size); } static void deallocate(void* ptr, size_t size) noexcept { if (size <= MAX_FRAME_SIZE) { + void* block = user_to_block(ptr); + auto* header = static_cast(block); auto& alloc = instance(); - if (alloc.free_count_ < POOL_SIZE) { - alloc.pool_[alloc.free_count_++] = ptr; + + // Fast path: same thread - return directly to local pool + if (header->source_pool_id == alloc.pool_id_) { + if (alloc.free_count_ < POOL_SIZE) { + alloc.pool_[alloc.free_count_++] = block; + return; + } + // Pool full, delete the block (not the user pointer!) + ::operator delete(block); + return; + } else { + // Cross-thread deallocation: push to source pool's remote queue + frame_allocator* source = get_pool_by_id(header->source_pool_id); + if (source) { + source->push_remote_return(block); + return; + } + // Source pool no longer exists (thread exited), delete the block + ::operator delete(block); return; } } + // Large allocation - was allocated without header ::operator delete(ptr); } #endif private: - frame_allocator() : free_count_(0) {} - + // Block header stored before user data + struct block_header { + uint32_t source_pool_id; // ID of the pool that allocated this block + std::atomic next; // For MPSC queue linkage + }; + + // Total block size including header, aligned for user data + static constexpr size_t HEADER_SIZE = sizeof(block_header); + static constexpr size_t ALLOC_BLOCK_SIZE = HEADER_SIZE + MAX_FRAME_SIZE; + + // Convert between block (with header) and user pointer + static void* block_to_user(void* block) noexcept { + return static_cast(block) + HEADER_SIZE; + } + + static void* user_to_block(void* user) noexcept { + return static_cast(user) - HEADER_SIZE; + } + + frame_allocator() + : free_count_(0) + , pool_id_(next_pool_id_.fetch_add(1, std::memory_order_relaxed)) + , remote_head_{0, {nullptr}} // Initialize dummy head: pool_id=0, next=nullptr + , remote_tail_(&remote_head_) { + // Register this pool for cross-thread access + register_pool(this); + } + ~frame_allocator() { + // Unregister before cleanup + unregister_pool(this); + + // Reclaim any remaining remote returns + reclaim_all_remote_returns(); + // Free all cached frames when thread exits for (size_t i = 0; i < free_count_; ++i) { ::operator delete(pool_[i]); } } - + + // MPSC queue: push from any thread (producers), pop from owner only (consumer) + void push_remote_return(void* block) noexcept { + auto* header = static_cast(block); + header->next.store(nullptr, std::memory_order_relaxed); + + // Atomic push to MPSC queue (lock-free) + block_header* prev = remote_tail_.exchange(header, std::memory_order_acq_rel); + prev->next.store(header, std::memory_order_release); + } + + // Called by owner thread to reclaim remote returns + void reclaim_remote_returns() noexcept { + // Quick check without full synchronization + block_header* head = remote_head_.next.load(std::memory_order_acquire); + if (!head) return; + + size_t count = 0; + while (head && count < REMOTE_QUEUE_BATCH && free_count_ < POOL_SIZE) { + block_header* next = head->next.load(std::memory_order_acquire); + + // If next is null but tail points elsewhere, spin briefly + // (producer is in the middle of push) + if (!next && remote_tail_.load(std::memory_order_acquire) != head) { + // Spin wait for producer to complete + for (int i = 0; i < 100 && !head->next.load(std::memory_order_acquire); ++i) { + // Brief spin + } + next = head->next.load(std::memory_order_acquire); + } + + pool_[free_count_++] = head; + remote_head_.next.store(next, std::memory_order_release); + head = next; + ++count; + } + } + + // Called during destruction to reclaim all + void reclaim_all_remote_returns() noexcept { + block_header* head = remote_head_.next.load(std::memory_order_acquire); + while (head) { + block_header* next = head->next.load(std::memory_order_acquire); + + // If next is null but tail points elsewhere, spin briefly + if (!next && remote_tail_.load(std::memory_order_acquire) != head) { + for (int i = 0; i < 1000 && !head->next.load(std::memory_order_acquire); ++i) { + // Brief spin + } + next = head->next.load(std::memory_order_acquire); + } + + if (free_count_ < POOL_SIZE) { + pool_[free_count_++] = head; + } else { + ::operator delete(head); + } + head = next; + } + remote_head_.next.store(nullptr, std::memory_order_release); + remote_tail_.store(&remote_head_, std::memory_order_release); + } + static frame_allocator& instance() { static thread_local frame_allocator alloc; return alloc; } + // Pool registry for cross-thread access + static constexpr size_t MAX_POOLS = 256; + + static void register_pool(frame_allocator* pool) noexcept { + uint32_t id = pool->pool_id_; + if (id < MAX_POOLS) { + pool_registry_[id].store(pool, std::memory_order_release); + } + } + + static void unregister_pool(frame_allocator* pool) noexcept { + uint32_t id = pool->pool_id_; + if (id < MAX_POOLS) { + pool_registry_[id].store(nullptr, std::memory_order_release); + } + } + + static frame_allocator* get_pool_by_id(uint32_t id) noexcept { + if (id < MAX_POOLS) { + return pool_registry_[id].load(std::memory_order_acquire); + } + return nullptr; + } + std::array pool_; size_t free_count_; + uint32_t pool_id_; + + // MPSC queue for remote returns (dummy head node pattern) + block_header remote_head_; // Dummy node - next points to actual head + std::atomic remote_tail_; + + // Global pool ID counter + static inline std::atomic next_pool_id_{0}; + + // Global pool registry for cross-thread lookups + static inline std::array, MAX_POOLS> pool_registry_{}; }; } // namespace elio::coro diff --git a/include/elio/coro/promise_base.hpp b/include/elio/coro/promise_base.hpp index cffb513..2b1bc12 100644 --- a/include/elio/coro/promise_base.hpp +++ b/include/elio/coro/promise_base.hpp @@ -38,28 +38,59 @@ struct debug_location { uint32_t line = 0; }; +/// Thread-local ID allocator for coroutine debug IDs +/// Allocates IDs in batches to avoid global atomic contention +class id_allocator { +public: + static constexpr uint64_t BATCH_SIZE = 1024; + + static uint64_t allocate() noexcept { + auto& alloc = instance(); + if (alloc.next_id_ >= alloc.end_id_) { + // Batch exhausted - get a new batch + uint64_t batch_start = global_counter_.fetch_add(BATCH_SIZE, std::memory_order_relaxed); + alloc.next_id_ = batch_start; + alloc.end_id_ = batch_start + BATCH_SIZE; + } + return alloc.next_id_++; + } + +private: + id_allocator() noexcept : next_id_(0), end_id_(0) {} + + static id_allocator& instance() noexcept { + static thread_local id_allocator alloc; + return alloc; + } + + uint64_t next_id_; + uint64_t end_id_; + + static inline std::atomic global_counter_{1}; +}; + /// Base class for all coroutine promise types /// Implements lightweight virtual stack tracking via thread-local intrusive list -/// +/// /// Debug support: /// - Each frame has a unique ID for identification /// - Source location can be set for debugging /// - State tracking (created/running/suspended/completed/failed) /// - Virtual stack via parent_ pointer chain -/// +/// /// Note: No global frame registry to avoid synchronization overhead. /// Debuggers should find coroutine frames through scheduler's worker queues. class promise_base { public: - /// Magic number for debugger validation: "ELIOFRME" + /// Magic number for debugger validation: "ELIOFRME" static constexpr uint64_t FRAME_MAGIC = 0x454C494F46524D45ULL; - promise_base() noexcept + promise_base() noexcept : frame_magic_(FRAME_MAGIC) , parent_(current_frame_) , debug_state_(coroutine_state::created) , debug_worker_id_(static_cast(-1)) - , debug_id_(next_id_.fetch_add(1, std::memory_order_relaxed)) + , debug_id_(id_allocator::allocate()) , affinity_(NO_AFFINITY) { current_frame_ = this; @@ -146,7 +177,6 @@ class promise_base { size_t affinity_; static inline thread_local promise_base* current_frame_ = nullptr; - static inline std::atomic next_id_{1}; }; } // namespace elio::coro diff --git a/include/elio/coro/task.hpp b/include/elio/coro/task.hpp index f4f387d..332d4f7 100644 --- a/include/elio/coro/task.hpp +++ b/include/elio/coro/task.hpp @@ -176,11 +176,20 @@ class join_handle { [[nodiscard]] bool await_ready() const noexcept { return state_->is_completed(); } - + bool await_suspend(std::coroutine_handle<> awaiter) noexcept { - return state_->set_waiter(awaiter); + // Keep a local copy of the shared_ptr to prevent use-after-free. + // Without this, a race can occur: + // 1. set_waiter() stores the awaiter and checks completed_ + // 2. Meanwhile, complete() is called, which schedules the awaiter + // 3. The awaiter runs on another thread, finishes, and destroys this join_handle + // 4. The last shared_ptr ref is gone, join_state is destroyed + // 5. set_waiter() tries to access destroyed memory + // Holding a local shared_ptr ensures join_state outlives set_waiter(). + auto state = state_; + return state->set_waiter(awaiter); } - + T await_resume() { return state_->get_value(); } @@ -210,15 +219,18 @@ class join_handle { [[nodiscard]] bool await_ready() const noexcept { return state_->is_completed(); } - + bool await_suspend(std::coroutine_handle<> awaiter) noexcept { - return state_->set_waiter(awaiter); + // Keep a local copy of the shared_ptr to prevent use-after-free. + // See join_handle::await_suspend for detailed explanation. + auto state = state_; + return state->set_waiter(awaiter); } - + void await_resume() { state_->get_value(); } - + [[nodiscard]] bool is_ready() const noexcept { return state_->is_completed(); } diff --git a/include/elio/http/client_base.hpp b/include/elio/http/client_base.hpp new file mode 100644 index 0000000..c422c2f --- /dev/null +++ b/include/elio/http/client_base.hpp @@ -0,0 +1,77 @@ +#pragma once + +/// @file client_base.hpp +/// @brief Common configuration and utilities for HTTP-based clients +/// +/// This file provides shared infrastructure for HTTP, WebSocket, and SSE clients: +/// - Base client configuration with common settings +/// - TLS context initialization utilities +/// - Connection utility functions + +#include +#include +#include +#include +#include + +#include +#include + +namespace elio::http { + +/// Base configuration shared by all HTTP-based clients +/// Can be embedded in more specific configuration structures +struct base_client_config { + std::chrono::seconds connect_timeout{10}; ///< Connection timeout + std::chrono::seconds read_timeout{30}; ///< Read timeout + size_t read_buffer_size = 8192; ///< Read buffer size + std::string user_agent; ///< User-Agent header (empty = no header) + bool verify_certificate = true; ///< Verify TLS certificates +}; + +/// Initialize a TLS context for client use with default settings +/// @param ctx TLS context to initialize +/// @param verify_certificate Whether to verify server certificates +inline void init_client_tls_context(tls::tls_context& ctx, bool verify_certificate = true) { + ctx.use_default_verify_paths(); + if (verify_certificate) { + ctx.set_verify_mode(tls::verify_mode::peer); + } else { + ctx.set_verify_mode(tls::verify_mode::none); + } +} + +/// Connect to a host with TLS context setup +/// @param host Hostname +/// @param port Port number +/// @param secure If true, use TLS +/// @param tls_ctx TLS context (required if secure) +/// @return Connected stream or std::nullopt on error +inline coro::task> +client_connect(std::string_view host, uint16_t port, bool secure, + tls::tls_context* tls_ctx) { + if (secure) { + if (!tls_ctx) { + ELIO_LOG_ERROR("TLS context required for secure connection to {}:{}", host, port); + co_return std::nullopt; + } + + auto result = co_await tls::tls_connect(*tls_ctx, host, port); + if (!result) { + ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", host, port, strerror(errno)); + co_return std::nullopt; + } + + co_return net::stream(std::move(*result)); + } else { + auto result = co_await net::tcp_connect(host, port); + if (!result) { + ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", host, port, strerror(errno)); + co_return std::nullopt; + } + + co_return net::stream(std::move(*result)); + } +} + +} // namespace elio::http diff --git a/include/elio/http/http2_client.hpp b/include/elio/http/http2_client.hpp index 90fa637..ddcf0e0 100644 --- a/include/elio/http/http2_client.hpp +++ b/include/elio/http/http2_client.hpp @@ -85,10 +85,12 @@ class h2_connection { /// HTTP/2 client with connection management class h2_client { public: - /// Create HTTP/2 client with I/O context - explicit h2_client(io::io_context& io_ctx, h2_client_config config = {}) - : io_ctx_(&io_ctx) - , config_(config) + /// Create HTTP/2 client with default configuration + h2_client() : h2_client(h2_client_config{}) {} + + /// Create HTTP/2 client with configuration + explicit h2_client(h2_client_config config) + : config_(config) , tls_ctx_(tls::tls_mode::client) { // Setup TLS context for HTTP/2 tls_ctx_.use_default_verify_paths(); @@ -245,24 +247,22 @@ class h2_client { connections_[key] = std::move(conn); } - io::io_context* io_ctx_; h2_client_config config_; tls::tls_context tls_ctx_; std::unordered_map connections_; }; /// Convenience function for one-off HTTP/2 GET request -inline coro::task> h2_get(io::io_context& io_ctx, std::string_view url) { - h2_client client(io_ctx); +inline coro::task> h2_get(std::string_view url) { + h2_client client; co_return co_await client.get(url); } /// Convenience function for one-off HTTP/2 POST request -inline coro::task> h2_post(io::io_context& io_ctx, - std::string_view url, +inline coro::task> h2_post(std::string_view url, std::string_view body, std::string_view content_type = mime::application_form_urlencoded) { - h2_client client(io_ctx); + h2_client client; co_return co_await client.post(url, body, content_type); } diff --git a/include/elio/http/http_client.hpp b/include/elio/http/http_client.hpp index d58c8d7..c4b11a6 100644 --- a/include/elio/http/http_client.hpp +++ b/include/elio/http/http_client.hpp @@ -3,9 +3,8 @@ #include #include #include -#include -#include -#include +#include +#include #include #include #include @@ -14,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -24,95 +22,20 @@ namespace elio::http { /// HTTP client configuration -struct client_config { - std::chrono::seconds connect_timeout{10}; ///< Connection timeout - std::chrono::seconds read_timeout{30}; ///< Read timeout +struct client_config : base_client_config { size_t max_redirects = 5; ///< Max redirects to follow bool follow_redirects = true; ///< Auto-follow redirects - size_t read_buffer_size = 8192; ///< Read buffer size size_t max_connections_per_host = 6; ///< Max connections per host std::chrono::seconds pool_idle_timeout{60}; ///< Idle connection timeout - std::string user_agent = "elio-http/1.0"; ///< User-Agent header -}; -/// Connection wrapper (TCP or TLS) -class connection { -public: - using stream_type = std::variant; - - connection() = default; - - /// Create plain TCP connection - explicit connection(net::tcp_stream tcp) - : stream_(std::move(tcp)), secure_(false) {} - - /// Create TLS connection - explicit connection(tls::tls_stream tls) - : stream_(std::move(tls)), secure_(true) {} - - // Move only - connection(connection&&) = default; - connection& operator=(connection&&) = default; - connection(const connection&) = delete; - connection& operator=(const connection&) = delete; - - /// Check if connected - bool is_connected() const noexcept { - return !std::holds_alternative(stream_); - } - - /// Check if secure (TLS) - bool is_secure() const noexcept { return secure_; } - - /// Read data - coro::task read(void* buffer, size_t length) { - if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).read(buffer, length); - } else if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).read(buffer, length); - } - co_return io::io_result{-ENOTCONN, 0}; - } - - /// Write data - coro::task write(const void* buffer, size_t length) { - if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).write(buffer, length); - } else if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).write(buffer, length); - } - co_return io::io_result{-ENOTCONN, 0}; - } - - /// Write string - coro::task write(std::string_view data) { - return write(data.data(), data.size()); - } - - /// Close connection - coro::task close() { - if (std::holds_alternative(stream_)) { - co_await std::get(stream_).shutdown(); - } - stream_ = std::monostate{}; - } - - /// Get last use time - std::chrono::steady_clock::time_point last_use() const noexcept { - return last_use_; - } - - /// Update last use time - void touch() { - last_use_ = std::chrono::steady_clock::now(); + client_config() { + user_agent = "elio-http/1.0"; } - -private: - stream_type stream_; - bool secure_ = false; - std::chrono::steady_clock::time_point last_use_ = std::chrono::steady_clock::now(); }; +/// Connection wrapper using unified net::stream +using connection = net::stream; + /// Connection pool for HTTP keep-alive class connection_pool { public: @@ -120,12 +43,12 @@ class connection_pool { : config_(config) {} /// Get or create a connection to host - coro::task> acquire(const std::string& host, + coro::task> acquire(const std::string& host, uint16_t port, bool secure, tls::tls_context* tls_ctx = nullptr) { std::string key = make_key(host, port, secure); - + // Try to get an existing connection { std::lock_guard lock(mutex_); @@ -133,7 +56,7 @@ class connection_pool { if (it != pools_.end() && !it->second.empty()) { auto conn = std::move(it->second.front()); it->second.pop_front(); - + // Check if connection is still valid (not too old) auto age = std::chrono::steady_clock::now() - conn.last_use(); if (age < config_.pool_idle_timeout) { @@ -143,30 +66,14 @@ class connection_pool { // Connection too old, let it close } } - - // Create new connection - if (secure) { - if (!tls_ctx) { - ELIO_LOG_ERROR("TLS context required for HTTPS connection"); - co_return std::nullopt; - } - - auto result = co_await tls::tls_connect(*tls_ctx, host, port); - if (!result) { - ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", host, port, strerror(errno)); - co_return std::nullopt; - } - - co_return connection(std::move(*result)); - } else { - auto result = co_await net::tcp_connect(host, port); - if (!result) { - ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", host, port, strerror(errno)); - co_return std::nullopt; - } - - co_return connection(std::move(*result)); + + // Create new connection using client_connect utility + auto result = co_await client_connect(host, port, secure, tls_ctx); + if (!result) { + co_return std::nullopt; } + + co_return std::move(*result); } /// Return a connection to the pool @@ -202,15 +109,16 @@ class connection_pool { /// HTTP client class client { public: - /// Create client with I/O context - explicit client(io::io_context& io_ctx, client_config config = {}) - : io_ctx_(&io_ctx) - , config_(config) + /// Create client with default configuration + client() : client(client_config{}) {} + + /// Create client with configuration + explicit client(client_config config) + : config_(config) , pool_(config) , tls_ctx_(tls::tls_mode::client) { - // Setup default TLS context - tls_ctx_.use_default_verify_paths(); - tls_ctx_.set_verify_mode(tls::verify_mode::peer); + // Setup TLS context using shared utility + init_client_tls_context(tls_ctx_, config_.verify_certificate); } /// Perform HTTP GET request @@ -507,7 +415,6 @@ class client { co_return resp; } - io::io_context* io_ctx_; client_config config_; connection_pool pool_; tls::tls_context tls_ctx_; @@ -517,35 +424,32 @@ class client { /// Perform HTTP GET request /// @return Response on success, std::nullopt on error (check errno) -inline coro::task> get(io::io_context& io_ctx, std::string_view url) { - client c(io_ctx); +inline coro::task> get(std::string_view url) { + client c; co_return co_await c.get(url); } /// Perform HTTP GET request with cancellation support -inline coro::task> get(io::io_context& io_ctx, std::string_view url, - coro::cancel_token token) { - client c(io_ctx); +inline coro::task> get(std::string_view url, coro::cancel_token token) { + client c; co_return co_await c.get(url, std::move(token)); } /// Perform HTTP POST request /// @return Response on success, std::nullopt on error (check errno) -inline coro::task> post(io::io_context& io_ctx, - std::string_view url, - std::string_view body, - std::string_view content_type = mime::application_form_urlencoded) { - client c(io_ctx); +inline coro::task> post(std::string_view url, + std::string_view body, + std::string_view content_type = mime::application_form_urlencoded) { + client c; co_return co_await c.post(url, body, content_type); } /// Perform HTTP POST request with cancellation support -inline coro::task> post(io::io_context& io_ctx, - std::string_view url, - std::string_view body, - coro::cancel_token token, - std::string_view content_type = mime::application_form_urlencoded) { - client c(io_ctx); +inline coro::task> post(std::string_view url, + std::string_view body, + coro::cancel_token token, + std::string_view content_type = mime::application_form_urlencoded) { + client c; co_return co_await c.post(url, body, std::move(token), content_type); } diff --git a/include/elio/http/http_server.hpp b/include/elio/http/http_server.hpp index 1f28fdb..552a303 100644 --- a/include/elio/http/http_server.hpp +++ b/include/elio/http/http_server.hpp @@ -241,20 +241,25 @@ class server { } /// Start listening on address (plain HTTP) - coro::task listen(const net::socket_address& addr, io::io_context& io_ctx, - runtime::scheduler& sched, + coro::task listen(const net::socket_address& addr, const net::tcp_options& opts = {}) { - auto listener_result = net::tcp_listener::bind(addr, io_ctx, opts); + auto* sched = runtime::scheduler::current(); + if (!sched) { + ELIO_LOG_ERROR("HTTP server must be started from within a scheduler context"); + co_return; + } + + auto listener_result = net::tcp_listener::bind(addr, opts); if (!listener_result) { ELIO_LOG_ERROR("Failed to bind HTTP server: {}", strerror(errno)); co_return; } - + ELIO_LOG_INFO("HTTP server listening on {}", addr.to_string()); - + auto& listener = *listener_result; running_ = true; - + while (running_) { auto stream_result = co_await listener.accept(); if (!stream_result) { @@ -263,28 +268,34 @@ class server { } continue; } - + // Spawn connection handler auto handler = handle_connection(std::move(*stream_result)); - sched.spawn(handler.release()); + sched->spawn(handler.release()); } } - + /// Start listening with TLS (HTTPS) - coro::task listen_tls(const net::socket_address& addr, io::io_context& io_ctx, - runtime::scheduler& sched, tls::tls_context& tls_ctx, + coro::task listen_tls(const net::socket_address& addr, + tls::tls_context& tls_ctx, const net::tcp_options& opts = {}) { - auto listener_result = net::tcp_listener::bind(addr, io_ctx, opts); + auto* sched = runtime::scheduler::current(); + if (!sched) { + ELIO_LOG_ERROR("HTTPS server must be started from within a scheduler context"); + co_return; + } + + auto listener_result = net::tcp_listener::bind(addr, opts); if (!listener_result) { ELIO_LOG_ERROR("Failed to bind HTTPS server: {}", strerror(errno)); co_return; } - + ELIO_LOG_INFO("HTTPS server listening on {}", addr.to_string()); - + auto& listener = *listener_result; running_ = true; - + while (running_) { auto stream_result = co_await listener.accept(); if (!stream_result) { @@ -293,10 +304,10 @@ class server { } continue; } - + // Create TLS stream and spawn handler auto handler = handle_tls_connection(std::move(*stream_result), tls_ctx); - sched.spawn(handler.release()); + sched->spawn(handler.release()); } } diff --git a/include/elio/http/sse_client.hpp b/include/elio/http/sse_client.hpp index 2a37299..a42537a 100644 --- a/include/elio/http/sse_client.hpp +++ b/include/elio/http/sse_client.hpp @@ -12,9 +12,8 @@ #include #include #include -#include -#include -#include +#include +#include #include #include #include @@ -24,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -32,15 +30,16 @@ namespace elio::http::sse { /// SSE client configuration -struct client_config { - std::chrono::seconds connect_timeout{10}; ///< Connection timeout - size_t read_buffer_size = 4096; ///< Read buffer size +struct client_config : http::base_client_config { int default_retry_ms = 3000; ///< Default reconnect interval bool auto_reconnect = true; ///< Enable auto-reconnection size_t max_reconnect_attempts = 0; ///< Max reconnect attempts (0 = unlimited) std::string last_event_id; ///< Initial Last-Event-ID - std::string user_agent = "elio-sse-client/1.0"; ///< User-Agent header - bool verify_certificate = true; ///< Verify TLS certificates + + client_config() { + user_agent = "elio-sse-client/1.0"; + read_buffer_size = 4096; // SSE uses smaller buffer + } }; /// SSE connection state @@ -195,23 +194,18 @@ class event_parser { /// SSE client class sse_client { public: - using stream_type = std::variant; - - /// Create SSE client with I/O context - explicit sse_client(io::io_context& io_ctx, client_config config = {}) - : io_ctx_(&io_ctx) - , config_(config) + /// Create SSE client with default configuration + sse_client() : sse_client(client_config{}) {} + + /// Create SSE client with configuration + explicit sse_client(client_config config) + : config_(config) , tls_ctx_(tls::tls_mode::client) { - // Setup TLS context - tls_ctx_.use_default_verify_paths(); - if (config_.verify_certificate) { - tls_ctx_.set_verify_mode(tls::verify_mode::peer); - } else { - tls_ctx_.set_verify_mode(tls::verify_mode::none); - } - + // Setup TLS context using shared utility + http::init_client_tls_context(tls_ctx_, config_.verify_certificate); + buffer_.resize(config_.read_buffer_size); - + if (!config_.last_event_id.empty()) { last_event_id_ = config_.last_event_id; } @@ -265,11 +259,7 @@ class sse_client { /// Close the connection coro::task close() { state_ = client_state::closed; - - if (std::holds_alternative(stream_)) { - co_await std::get(stream_).shutdown(); - } - stream_ = std::monostate{}; + co_await stream_.close(); } /// Get TLS context for configuration @@ -358,31 +348,17 @@ class sse_client { } coro::task do_connect() { - ELIO_LOG_DEBUG("Connecting to SSE endpoint {}:{}{}", + ELIO_LOG_DEBUG("Connecting to SSE endpoint {}:{}{}", url_.host, url_.effective_port(), url_.path); - - // Establish TCP connection - if (url_.is_secure()) { - auto result = co_await tls::tls_connect(tls_ctx_, - url_.host, url_.effective_port()); - if (!result) { - ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", - url_.host, url_.effective_port(), strerror(errno)); - state_ = client_state::disconnected; - co_return false; - } - stream_ = std::move(*result); - } else { - auto result = co_await net::tcp_connect(url_.host, - url_.effective_port()); - if (!result) { - ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", - url_.host, url_.effective_port(), strerror(errno)); - state_ = client_state::disconnected; - co_return false; - } - stream_ = std::move(*result); + + // Establish connection using shared utility + auto conn_result = co_await http::client_connect( + url_.host, url_.effective_port(), url_.is_secure(), &tls_ctx_); + if (!conn_result) { + state_ = client_state::disconnected; + co_return false; } + stream_ = std::move(*conn_result); // Send HTTP request std::string request; @@ -479,12 +455,9 @@ class sse_client { coro::task try_reconnect() { // Reset parser but keep last_event_id parser_.reset(); - + // Close current connection - if (std::holds_alternative(stream_)) { - co_await std::get(stream_).shutdown(); - } - stream_ = std::monostate{}; + co_await stream_.close(); // Get retry interval int retry_ms = parser_.retry_ms(); @@ -536,27 +509,16 @@ class sse_client { } coro::task read(void* buf, size_t len) { - if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).read(buf, len); - } else if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).read(buf, len); - } - co_return io::io_result{-ENOTCONN, 0}; + co_return co_await stream_.read(buf, len); } - + coro::task write(const void* buf, size_t len) { - if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).write(buf, len); - } else if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).write(buf, len); - } - co_return io::io_result{-ENOTCONN, 0}; + co_return co_await stream_.write(buf, len); } - io::io_context* io_ctx_; client_config config_; tls::tls_context tls_ctx_; - stream_type stream_; + net::stream stream_; coro::cancel_token token_; ///< Cancellation token for connection url url_; @@ -567,9 +529,9 @@ class sse_client { }; /// Convenience function for one-off SSE connection -inline coro::task> -sse_connect(io::io_context& io_ctx, std::string_view url, client_config config = {}) { - auto client = std::make_optional(io_ctx, config); +inline coro::task> +sse_connect(std::string_view url, client_config config = {}) { + auto client = std::make_optional(config); bool success = co_await client->connect(url); if (!success) { co_return std::nullopt; @@ -578,10 +540,9 @@ sse_connect(io::io_context& io_ctx, std::string_view url, client_config config = } /// Convenience function for one-off SSE connection with cancellation support -inline coro::task> -sse_connect(io::io_context& io_ctx, std::string_view url, coro::cancel_token token, - client_config config = {}) { - auto client = std::make_optional(io_ctx, config); +inline coro::task> +sse_connect(std::string_view url, coro::cancel_token token, client_config config = {}) { + auto client = std::make_optional(config); bool success = co_await client->connect(url, std::move(token)); if (!success) { co_return std::nullopt; diff --git a/include/elio/http/websocket_client.hpp b/include/elio/http/websocket_client.hpp index 55e127e..77d68b0 100644 --- a/include/elio/http/websocket_client.hpp +++ b/include/elio/http/websocket_client.hpp @@ -14,9 +14,8 @@ #include #include #include -#include -#include -#include +#include +#include #include #include #include @@ -25,42 +24,35 @@ #include #include #include -#include #include #include namespace elio::http::websocket { /// WebSocket client configuration -struct client_config { - std::chrono::seconds connect_timeout{10}; ///< Connection timeout - std::chrono::seconds read_timeout{30}; ///< Read timeout +struct client_config : http::base_client_config { size_t max_message_size = 16 * 1024 * 1024; ///< Max message size (16MB) - size_t read_buffer_size = 8192; ///< Read buffer size std::vector subprotocols; ///< Requested subprotocols std::string origin; ///< Origin header (for browser compatibility) - std::string user_agent = "elio-websocket/1.0"; ///< User-Agent header - bool verify_certificate = true; ///< Verify TLS certificates + + client_config() { + user_agent = "elio-websocket/1.0"; + } }; /// WebSocket client connection class ws_client { public: - using stream_type = std::variant; - - /// Create WebSocket client with I/O context - explicit ws_client(io::io_context& io_ctx, client_config config = {}) - : io_ctx_(&io_ctx) - , config_(config) + /// Create WebSocket client with default configuration + ws_client() : ws_client(client_config{}) {} + + /// Create WebSocket client with configuration + explicit ws_client(client_config config) + : config_(config) , tls_ctx_(tls::tls_mode::client) { - // Setup TLS context - tls_ctx_.use_default_verify_paths(); - if (config_.verify_certificate) { - tls_ctx_.set_verify_mode(tls::verify_mode::peer); - } else { - tls_ctx_.set_verify_mode(tls::verify_mode::none); - } - + // Setup TLS context using shared utility + http::init_client_tls_context(tls_ctx_, config_.verify_certificate); + parser_.set_max_message_size(config_.max_message_size); buffer_.resize(config_.read_buffer_size); } @@ -141,21 +133,18 @@ class ws_client { if (state_ != connection_state::open) { co_return; } - + state_ = connection_state::closing; - + auto frame = encode_close_frame(code, reason, true); co_await send_raw(frame); - + // Wait for close response (with timeout) // Simplified: just mark as closed state_ = connection_state::closed; - - // Cleanup stream - if (std::holds_alternative(stream_)) { - co_await std::get(stream_).shutdown(); - } - stream_ = std::monostate{}; + + // Cleanup stream using unified close + co_await stream_.close(); } /// Receive next message (blocks until message available or connection closed) @@ -255,26 +244,16 @@ class ws_client { co_return false; } - // Establish TCP connection - if (secure_) { - auto result = co_await tls::tls_connect(tls_ctx_, host_, port); - if (!result) { - ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", host_, port, strerror(errno)); - co_return false; - } - stream_ = std::move(*result); - } else { - auto result = co_await net::tcp_connect(host_, port); - if (!result) { - ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", host_, port, strerror(errno)); - co_return false; - } - stream_ = std::move(*result); + // Establish connection using shared utility + auto conn_result = co_await http::client_connect(host_, port, secure_, &tls_ctx_); + if (!conn_result) { + co_return false; } + stream_ = std::move(*conn_result); // Check cancellation before handshake if (token.is_cancelled()) { - stream_ = std::monostate{}; + stream_.disconnect(); co_return false; } @@ -449,21 +428,11 @@ class ws_client { } coro::task read(void* buf, size_t len) { - if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).read(buf, len); - } else if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).read(buf, len); - } - co_return io::io_result{-ENOTCONN, 0}; + co_return co_await stream_.read(buf, len); } - + coro::task write(const void* buf, size_t len) { - if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).write(buf, len); - } else if (std::holds_alternative(stream_)) { - co_return co_await std::get(stream_).write(buf, len); - } - co_return io::io_result{-ENOTCONN, 0}; + co_return co_await stream_.write(buf, len); } coro::task send_raw(const std::vector& data) { @@ -509,10 +478,9 @@ class ws_client { } } - io::io_context* io_ctx_; client_config config_; tls::tls_context tls_ctx_; - stream_type stream_; + net::stream stream_; std::string host_; std::string path_; @@ -527,9 +495,9 @@ class ws_client { }; /// Convenience function for one-off WebSocket connection -inline coro::task> -ws_connect(io::io_context& io_ctx, std::string_view url, client_config config = {}) { - auto client = std::make_optional(io_ctx, config); +inline coro::task> +ws_connect(std::string_view url, client_config config = {}) { + auto client = std::make_optional(config); bool success = co_await client->connect(url); if (!success) { co_return std::nullopt; diff --git a/include/elio/http/websocket_server.hpp b/include/elio/http/websocket_server.hpp index f73bcd3..808c29d 100644 --- a/include/elio/http/websocket_server.hpp +++ b/include/elio/http/websocket_server.hpp @@ -421,19 +421,24 @@ class ws_server { : router_(std::move(r)), http_config_(http_config) {} /// Start listening on address (plain HTTP/WS) - coro::task listen(net::ipv4_address addr, io::io_context& io_ctx, - runtime::scheduler& sched) { - auto listener_result = net::tcp_listener::bind(addr, io_ctx); + coro::task listen(net::ipv4_address addr) { + auto* sched = runtime::scheduler::current(); + if (!sched) { + ELIO_LOG_ERROR("WebSocket server must be started from within a scheduler context"); + co_return; + } + + auto listener_result = net::tcp_listener::bind(addr); if (!listener_result) { ELIO_LOG_ERROR("Failed to bind WebSocket server: {}", strerror(errno)); co_return; } - + ELIO_LOG_INFO("WebSocket server listening on {}", addr.to_string()); - + auto& listener = *listener_result; running_ = true; - + while (running_) { auto stream_result = co_await listener.accept(); if (!stream_result) { @@ -442,27 +447,32 @@ class ws_server { } continue; } - + // Spawn connection handler auto handler = handle_connection(std::move(*stream_result)); - sched.spawn(handler.release()); + sched->spawn(handler.release()); } } - + /// Start listening with TLS (HTTPS/WSS) - coro::task listen_tls(net::ipv4_address addr, io::io_context& io_ctx, - runtime::scheduler& sched, tls::tls_context& tls_ctx) { - auto listener_result = net::tcp_listener::bind(addr, io_ctx); + coro::task listen_tls(net::ipv4_address addr, tls::tls_context& tls_ctx) { + auto* sched = runtime::scheduler::current(); + if (!sched) { + ELIO_LOG_ERROR("Secure WebSocket server must be started from within a scheduler context"); + co_return; + } + + auto listener_result = net::tcp_listener::bind(addr); if (!listener_result) { ELIO_LOG_ERROR("Failed to bind secure WebSocket server: {}", strerror(errno)); co_return; } - + ELIO_LOG_INFO("Secure WebSocket server listening on {}", addr.to_string()); - + auto& listener = *listener_result; running_ = true; - + while (running_) { auto stream_result = co_await listener.accept(); if (!stream_result) { @@ -471,10 +481,10 @@ class ws_server { } continue; } - + // Spawn TLS connection handler auto handler = handle_tls_connection(std::move(*stream_result), tls_ctx); - sched.spawn(handler.release()); + sched->spawn(handler.release()); } } diff --git a/include/elio/io/io_backend.hpp b/include/elio/io/io_backend.hpp index 8afc683..5fbd4f6 100644 --- a/include/elio/io/io_backend.hpp +++ b/include/elio/io/io_backend.hpp @@ -55,15 +55,19 @@ struct io_request { int64_t offset; ///< File offset (-1 for current position) std::coroutine_handle<> awaiter; ///< Coroutine to resume on completion void* user_data; ///< User data for tracking - + // For vectored I/O ::iovec* iovecs; size_t iovec_count; - + // For socket operations ::sockaddr* addr; ::socklen_t* addrlen; int socket_flags; + + // For timeout operations - pointer to awaiter's local timespec to avoid data races + // This is a void* to avoid including linux/time_types.h here + void* timeout_ts; }; /// Abstract I/O backend interface @@ -71,31 +75,35 @@ struct io_request { class io_backend { public: virtual ~io_backend() = default; - + /// Prepare an I/O operation (does not submit yet) /// @param req The I/O request to prepare /// @return true if prepared successfully, false if queue is full virtual bool prepare(const io_request& req) = 0; - + /// Submit all prepared I/O operations /// @return Number of operations submitted virtual int submit() = 0; - + /// Poll for completed I/O operations /// @param timeout Maximum time to wait (-1 for infinite, 0 for non-blocking) /// @return Number of completions processed virtual int poll(std::chrono::milliseconds timeout) = 0; - + /// Check if there are pending operations virtual bool has_pending() const noexcept = 0; - + /// Get the number of pending operations virtual size_t pending_count() const noexcept = 0; - + /// Cancel a pending operation /// @param user_data The user_data of the operation to cancel /// @return true if cancellation was submitted virtual bool cancel(void* user_data) = 0; + + /// Check if this backend supports io_uring features (like timeout_ts) + /// @return true if this is an io_uring backend + virtual bool is_io_uring() const noexcept { return false; } }; } // namespace elio::io diff --git a/include/elio/io/io_context.hpp b/include/elio/io/io_context.hpp index 9093d88..e5ebb72 100644 --- a/include/elio/io/io_context.hpp +++ b/include/elio/io/io_context.hpp @@ -176,6 +176,12 @@ class io_context { return backend_.get(); } + /// Check if the current backend is io_uring + /// @return true if using io_uring backend + bool is_io_uring() const noexcept { + return backend_ && backend_->is_io_uring(); + } + private: std::unique_ptr backend_; ///< Active I/O backend backend_type backend_type_; ///< Current backend type diff --git a/include/elio/io/io_uring_backend.hpp b/include/elio/io/io_uring_backend.hpp index b6803df..f3e9602 100644 --- a/include/elio/io/io_uring_backend.hpp +++ b/include/elio/io/io_uring_backend.hpp @@ -155,10 +155,15 @@ class io_uring_backend : public io_backend { case io_op::timeout: { // Timeout in nanoseconds + // Store timespec in user_data area of the awaiter to avoid shared state + // The awaiter must have a ts_ member for this to work auto ns = static_cast(req.length); - ts_.tv_sec = ns / 1000000000LL; - ts_.tv_nsec = ns % 1000000000LL; - io_uring_prep_timeout(sqe, &ts_, 0, 0); + auto* ts = static_cast<__kernel_timespec*>(req.timeout_ts); + if (ts) { + ts->tv_sec = ns / 1000000000LL; + ts->tv_nsec = ns % 1000000000LL; + io_uring_prep_timeout(sqe, ts, 0, 0); + } break; } @@ -299,6 +304,9 @@ class io_uring_backend : public io_backend { } return false; } + + /// Override to indicate this is an io_uring backend + bool is_io_uring() const noexcept override { return true; } private: /// Deferred resume entry - stores handle with its result @@ -354,8 +362,7 @@ class io_uring_backend : public io_backend { private: struct io_uring ring_; ///< io_uring instance std::atomic pending_ops_; ///< Number of pending operations - struct __kernel_timespec ts_ = {}; ///< Reusable timespec for timeout ops - + static inline thread_local io_result last_result_{}; }; diff --git a/include/elio/net/stream.hpp b/include/elio/net/stream.hpp new file mode 100644 index 0000000..8e15627 --- /dev/null +++ b/include/elio/net/stream.hpp @@ -0,0 +1,200 @@ +#pragma once + +/// @file stream.hpp +/// @brief Unified stream abstraction for TCP and TLS connections +/// +/// This file provides a polymorphic stream wrapper that abstracts over +/// plain TCP streams and TLS-encrypted streams, eliminating code +/// duplication in HTTP, WebSocket, and SSE clients. + +#include +#include +#include +#include +#include + +#include +#include + +namespace elio::net { + +/// Unified stream type that can be either plain TCP or TLS encrypted +/// Provides a common interface for read/write operations +class stream { +public: + using variant_type = std::variant; + + /// Default constructor - creates a disconnected stream + stream() = default; + + /// Create from a plain TCP stream + explicit stream(tcp_stream tcp) + : stream_(std::move(tcp)), secure_(false) {} + + /// Create from a TLS stream + explicit stream(tls::tls_stream tls) + : stream_(std::move(tls)), secure_(true) {} + + // Move only + stream(stream&&) = default; + stream& operator=(stream&&) = default; + stream(const stream&) = delete; + stream& operator=(const stream&) = delete; + + /// Check if connected (not in monostate) + bool is_connected() const noexcept { + return !std::holds_alternative(stream_); + } + + /// Check if this is a secure (TLS) connection + bool is_secure() const noexcept { return secure_; } + + /// Read data from the stream + /// @param buffer Buffer to read into + /// @param length Maximum bytes to read + /// @return io_result with bytes read or error + coro::task read(void* buffer, size_t length) { + if (std::holds_alternative(stream_)) { + co_return co_await std::get(stream_).read(buffer, length); + } else if (std::holds_alternative(stream_)) { + co_return co_await std::get(stream_).read(buffer, length); + } + co_return io::io_result{-ENOTCONN, 0}; + } + + /// Write data to the stream + /// @param buffer Data to write + /// @param length Number of bytes to write + /// @return io_result with bytes written or error + coro::task write(const void* buffer, size_t length) { + if (std::holds_alternative(stream_)) { + co_return co_await std::get(stream_).write(buffer, length); + } else if (std::holds_alternative(stream_)) { + co_return co_await std::get(stream_).write(buffer, length); + } + co_return io::io_result{-ENOTCONN, 0}; + } + + /// Write string_view data + coro::task write(std::string_view data) { + return write(data.data(), data.size()); + } + + /// Write all data, retrying short writes + /// @return true if all data was written, false on error + coro::task write_all(const void* buffer, size_t length) { + const auto* ptr = static_cast(buffer); + size_t remaining = length; + + while (remaining > 0) { + auto result = co_await write(ptr, remaining); + if (result.result <= 0) { + co_return false; + } + ptr += result.result; + remaining -= static_cast(result.result); + } + co_return true; + } + + /// Write all data from string_view + coro::task write_all(std::string_view data) { + return write_all(data.data(), data.size()); + } + + /// Close/shutdown the stream + coro::task close() { + if (std::holds_alternative(stream_)) { + co_await std::get(stream_).shutdown(); + } + stream_ = std::monostate{}; + } + + /// Disconnect without TLS shutdown (synchronous) + /// Use this when you need to reset the stream without an async operation + void disconnect() noexcept { + stream_ = std::monostate{}; + } + + /// Get last use time (for connection pooling) + std::chrono::steady_clock::time_point last_use() const noexcept { + return last_use_; + } + + /// Update last use time + void touch() { + last_use_ = std::chrono::steady_clock::now(); + } + + /// Get underlying file descriptor (-1 if not connected) + int fd() const noexcept { + if (std::holds_alternative(stream_)) { + return std::get(stream_).fd(); + } else if (std::holds_alternative(stream_)) { + return std::get(stream_).fd(); + } + return -1; + } + + /// Access underlying TCP stream (throws if not TCP or disconnected) + tcp_stream& as_tcp() { + return std::get(stream_); + } + + const tcp_stream& as_tcp() const { + return std::get(stream_); + } + + /// Access underlying TLS stream (throws if not TLS or disconnected) + tls::tls_stream& as_tls() { + return std::get(stream_); + } + + const tls::tls_stream& as_tls() const { + return std::get(stream_); + } + + /// Check if holds TCP stream + bool is_tcp() const noexcept { + return std::holds_alternative(stream_); + } + + /// Check if holds TLS stream + bool is_tls() const noexcept { + return std::holds_alternative(stream_); + } + +private: + variant_type stream_; + bool secure_ = false; + std::chrono::steady_clock::time_point last_use_ = std::chrono::steady_clock::now(); +}; + +/// Connect to a host, automatically selecting TCP or TLS based on secure flag +/// @param host Hostname to connect to +/// @param port Port number +/// @param secure If true, establish TLS connection +/// @param tls_ctx TLS context (required if secure=true) +/// @return Connected stream on success, std::nullopt on error +inline coro::task> +connect(std::string_view host, uint16_t port, bool secure = false, + tls::tls_context* tls_ctx = nullptr) { + if (secure) { + if (!tls_ctx) { + co_return std::nullopt; + } + auto result = co_await tls::tls_connect(*tls_ctx, host, port); + if (!result) { + co_return std::nullopt; + } + co_return stream(std::move(*result)); + } else { + auto result = co_await tcp_connect(host, port); + if (!result) { + co_return std::nullopt; + } + co_return stream(std::move(*result)); + } +} + +} // namespace elio::net diff --git a/include/elio/net/tcp.hpp b/include/elio/net/tcp.hpp index b4e1fec..2eec265 100644 --- a/include/elio/net/tcp.hpp +++ b/include/elio/net/tcp.hpp @@ -272,27 +272,25 @@ class socket_address { class tcp_stream { public: /// Construct from file descriptor - explicit tcp_stream(int fd, io::io_context& ctx) - : fd_(fd), ctx_(&ctx) { + explicit tcp_stream(int fd) + : fd_(fd) { // Make non-blocking int flags = fcntl(fd_, F_GETFL, 0); fcntl(fd_, F_SETFL, flags | O_NONBLOCK); } - + /// Move constructor tcp_stream(tcp_stream&& other) noexcept : fd_(other.fd_) - , ctx_(other.ctx_) , peer_addr_(std::move(other.peer_addr_)) { other.fd_ = -1; } - + /// Move assignment tcp_stream& operator=(tcp_stream&& other) noexcept { if (this != &other) { close_sync(); fd_ = other.fd_; - ctx_ = other.ctx_; peer_addr_ = std::move(other.peer_addr_); other.fd_ = -1; } @@ -314,10 +312,6 @@ class tcp_stream { /// Get the file descriptor int fd() const noexcept { return fd_; } - /// Get the I/O context - io::io_context& context() noexcept { return *ctx_; } - const io::io_context& context() const noexcept { return *ctx_; } - /// Get peer address std::optional peer_address() const { if (peer_addr_) { @@ -417,7 +411,6 @@ class tcp_stream { } int fd_ = -1; - io::io_context* ctx_; std::optional peer_addr_; }; @@ -426,50 +419,44 @@ class tcp_listener { public: /// Create and bind a TCP listener (IPv4) /// @param addr Address to bind to - /// @param ctx I/O context /// @param opts Socket options /// @return TCP listener on success, std::nullopt on error (check errno) static std::optional bind( const ipv4_address& addr, - io::io_context& ctx, - const tcp_options& opts = {}) + const tcp_options& opts = {}) { - return bind_impl(socket_address(addr), ctx, opts); + return bind_impl(socket_address(addr), opts); } - + /// Create and bind a TCP listener (IPv6) static std::optional bind( const ipv6_address& addr, - io::io_context& ctx, const tcp_options& opts = {}) { - return bind_impl(socket_address(addr), ctx, opts); + return bind_impl(socket_address(addr), opts); } - + /// Create and bind a TCP listener (generic address) static std::optional bind( const socket_address& addr, - io::io_context& ctx, const tcp_options& opts = {}) { - return bind_impl(addr, ctx, opts); + return bind_impl(addr, opts); } /// Move constructor tcp_listener(tcp_listener&& other) noexcept : fd_(other.fd_) - , ctx_(other.ctx_) , local_addr_(std::move(other.local_addr_)) , opts_(other.opts_) { other.fd_ = -1; } - + /// Move assignment tcp_listener& operator=(tcp_listener&& other) noexcept { if (this != &other) { close_sync(); fd_ = other.fd_; - ctx_ = other.ctx_; local_addr_ = std::move(other.local_addr_); opts_ = other.opts_; other.fd_ = -1; @@ -524,14 +511,14 @@ class tcp_listener { std::optional await_resume() { result_ = io::io_context::get_last_result(); - + if (!result_.success()) { errno = result_.error_code(); return std::nullopt; } - + int client_fd = result_.result; - tcp_stream stream(client_fd, *listener_.ctx_); + tcp_stream stream(client_fd); // Apply TCP options if (listener_.opts_.no_delay) { @@ -570,7 +557,6 @@ class tcp_listener { private: static std::optional bind_impl( const socket_address& addr, - io::io_context& ctx, const tcp_options& opts) { int family = addr.family(); @@ -631,12 +617,12 @@ class tcp_listener { } ELIO_LOG_INFO("TCP listener bound to {}", bound_addr.to_string()); - - return tcp_listener(fd, ctx, bound_addr, opts); + + return tcp_listener(fd, bound_addr, opts); } - - tcp_listener(int fd, io::io_context& ctx, const socket_address& addr, const tcp_options& opts) - : fd_(fd), ctx_(&ctx), local_addr_(addr), opts_(opts) {} + + tcp_listener(int fd, const socket_address& addr, const tcp_options& opts) + : fd_(fd), local_addr_(addr), opts_(opts) {} void close_sync() { if (fd_ >= 0) { @@ -647,7 +633,6 @@ class tcp_listener { } int fd_ = -1; - io::io_context* ctx_; socket_address local_addr_; tcp_options opts_; }; @@ -730,7 +715,7 @@ class tcp_connect_awaitable { return std::nullopt; } - tcp_stream stream(fd_, io::current_io_context()); + tcp_stream stream(fd_); fd_ = -1; // Transfer ownership stream.set_peer_address(addr_); diff --git a/include/elio/net/uds.hpp b/include/elio/net/uds.hpp index d930020..1a4ba3e 100644 --- a/include/elio/net/uds.hpp +++ b/include/elio/net/uds.hpp @@ -110,27 +110,25 @@ struct unix_address { class uds_stream { public: /// Construct from file descriptor - explicit uds_stream(int fd, io::io_context& ctx) - : fd_(fd), ctx_(&ctx) { + explicit uds_stream(int fd) + : fd_(fd) { // Make non-blocking int flags = fcntl(fd_, F_GETFL, 0); fcntl(fd_, F_SETFL, flags | O_NONBLOCK); } - + /// Move constructor uds_stream(uds_stream&& other) noexcept : fd_(other.fd_) - , ctx_(other.ctx_) , peer_addr_(std::move(other.peer_addr_)) { other.fd_ = -1; } - + /// Move assignment uds_stream& operator=(uds_stream&& other) noexcept { if (this != &other) { close_sync(); fd_ = other.fd_; - ctx_ = other.ctx_; peer_addr_ = std::move(other.peer_addr_); other.fd_ = -1; } @@ -152,10 +150,6 @@ class uds_stream { /// Get the file descriptor int fd() const noexcept { return fd_; } - /// Get the I/O context - io::io_context& context() noexcept { return *ctx_; } - const io::io_context& context() const noexcept { return *ctx_; } - /// Get peer address std::optional peer_address() const { if (!peer_addr_.path.empty()) { @@ -249,7 +243,6 @@ class uds_stream { } int fd_ = -1; - io::io_context* ctx_; unix_address peer_addr_; }; @@ -258,12 +251,10 @@ class uds_listener { public: /// Create and bind a Unix Domain Socket listener /// @param addr Address (path) to bind to - /// @param ctx I/O context /// @param opts Socket options /// @return UDS listener on success, std::nullopt on error (check errno) static std::optional bind( const unix_address& addr, - io::io_context& ctx, const uds_options& opts = {}) { int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); @@ -309,25 +300,23 @@ class uds_listener { } ELIO_LOG_INFO("UDS listener bound to {}", addr.to_string()); - - return uds_listener(fd, ctx, addr, opts); + + return uds_listener(fd, addr, opts); } - + /// Move constructor uds_listener(uds_listener&& other) noexcept : fd_(other.fd_) - , ctx_(other.ctx_) , local_addr_(std::move(other.local_addr_)) , opts_(other.opts_) { other.fd_ = -1; } - + /// Move assignment uds_listener& operator=(uds_listener&& other) noexcept { if (this != &other) { close_sync(); fd_ = other.fd_; - ctx_ = other.ctx_; local_addr_ = std::move(other.local_addr_); opts_ = other.opts_; other.fd_ = -1; @@ -389,7 +378,7 @@ class uds_listener { } int client_fd = result_.result; - uds_stream stream(client_fd, *listener_.ctx_); + uds_stream stream(client_fd); // Set peer address if available if (peer_addr_len_ > offsetof(struct sockaddr_un, sun_path)) { @@ -419,8 +408,8 @@ class uds_listener { } private: - uds_listener(int fd, io::io_context& ctx, const unix_address& addr, const uds_options& opts) - : fd_(fd), ctx_(&ctx), local_addr_(addr), opts_(opts) {} + uds_listener(int fd, const unix_address& addr, const uds_options& opts) + : fd_(fd), local_addr_(addr), opts_(opts) {} void close_sync() { if (fd_ >= 0) { @@ -437,7 +426,6 @@ class uds_listener { } int fd_ = -1; - io::io_context* ctx_; unix_address local_addr_; uds_options opts_; }; @@ -527,7 +515,7 @@ class uds_connect_awaitable { return std::nullopt; } - uds_stream stream(fd_, io::current_io_context()); + uds_stream stream(fd_); fd_ = -1; // Transfer ownership stream.set_peer_address(addr_); diff --git a/include/elio/runtime/chase_lev_deque.hpp b/include/elio/runtime/chase_lev_deque.hpp index 6ffcf02..a1d67c5 100644 --- a/include/elio/runtime/chase_lev_deque.hpp +++ b/include/elio/runtime/chase_lev_deque.hpp @@ -90,12 +90,14 @@ class chase_lev_deque { size_t b = bottom_.load(std::memory_order_relaxed); size_t t = top_.load(std::memory_order_acquire); circular_buffer* buf = buffer_.load(std::memory_order_relaxed); - + if (b - t >= buf->capacity() - 1) { buf = resize(buf, t, b); } - + buf->store(b, item); + // Release fence ensures the store to buffer is visible before bottom update + // This is sufficient - no need for seq_cst here since push doesn't race with pop std::atomic_thread_fence(std::memory_order_release); bottom_.store(b + 1, std::memory_order_relaxed); } @@ -104,22 +106,27 @@ class chase_lev_deque { [[nodiscard]] T* pop() noexcept { size_t b = bottom_.load(std::memory_order_relaxed); circular_buffer* buf = buffer_.load(std::memory_order_relaxed); - + if (b == 0) return nullptr; - + b = b - 1; - // Use relaxed store - the seq_cst fence provides synchronization + // Use relaxed store - the seq_cst fence provides synchronization with steal() bottom_.store(b, std::memory_order_relaxed); + // seq_cst fence is REQUIRED here for correctness with steal() + // It ensures the bottom store is visible to thieves before we read top, + // and that we see any concurrent top updates from thieves std::atomic_thread_fence(std::memory_order_seq_cst); - + size_t t = top_.load(std::memory_order_relaxed); - + if (t <= b) { T* item = buf->load(b); if (t == b) { // Last element - race with thieves + // acq_rel is sufficient here: acquire ensures we see the item, + // release ensures our update to top is visible if (!top_.compare_exchange_strong(t, t + 1, - std::memory_order_seq_cst, + std::memory_order_acq_rel, std::memory_order_relaxed)) { // Lost race to thief bottom_.store(b + 1, std::memory_order_relaxed); @@ -129,7 +136,7 @@ class chase_lev_deque { } return item; } - + // Queue was empty bottom_.store(b + 1, std::memory_order_relaxed); return nullptr; @@ -158,14 +165,18 @@ class chase_lev_deque { /// Steal an element (thieves only) - lock-free [[nodiscard]] T* steal() noexcept { size_t t = top_.load(std::memory_order_acquire); + // seq_cst fence is REQUIRED here for correctness with pop() + // It ensures we see the latest bottom value after any concurrent pop std::atomic_thread_fence(std::memory_order_seq_cst); size_t b = bottom_.load(std::memory_order_acquire); - + if (t < b) { circular_buffer* buf = buffer_.load(std::memory_order_acquire); T* item = buf->load(t); + // acq_rel CAS: acquire ensures we see the item data, + // release ensures our top update is visible to owner's pop if (top_.compare_exchange_strong(t, t + 1, - std::memory_order_seq_cst, + std::memory_order_acq_rel, std::memory_order_relaxed)) { return item; } diff --git a/include/elio/sync/primitives.hpp b/include/elio/sync/primitives.hpp index bcadd07..7c0337b 100644 --- a/include/elio/sync/primitives.hpp +++ b/include/elio/sync/primitives.hpp @@ -21,95 +21,97 @@ namespace elio::sync { /// Coroutine-aware mutex /// Unlike std::mutex, this suspends the coroutine instead of blocking the thread +/// +/// Optimized with atomic fast path for try_lock - avoids mutex acquisition +/// in the uncontended case for ~10x performance improvement. class mutex { public: mutex() = default; ~mutex() = default; - + // Non-copyable, non-movable mutex(const mutex&) = delete; mutex& operator=(const mutex&) = delete; mutex(mutex&&) = delete; mutex& operator=(mutex&&) = delete; - + /// Lock awaitable class lock_awaitable { public: explicit lock_awaitable(mutex& m) : mutex_(m) {} - + bool await_ready() const noexcept { - // Try to acquire without waiting + // Try to acquire without waiting using atomic fast path return mutex_.try_lock(); } - + bool await_suspend(std::coroutine_handle<> awaiter) noexcept { - // Try to acquire again under lock std::lock_guard guard(mutex_.internal_mutex_); - - if (!mutex_.locked_) { - mutex_.locked_ = true; + + // Double-check after acquiring internal lock + // Use relaxed here since we hold the mutex + if (!mutex_.locked_.load(std::memory_order_relaxed)) { + mutex_.locked_.store(true, std::memory_order_relaxed); return false; // Don't suspend, we got the lock } - + // Add to wait queue mutex_.waiters_.push(awaiter); return true; // Suspend } - + void await_resume() const noexcept {} - + private: mutex& mutex_; }; - + /// Acquire the mutex auto lock() { return lock_awaitable(*this); } - + /// Try to acquire the mutex without waiting + /// Lock-free fast path using atomic CAS - no mutex acquisition needed bool try_lock() noexcept { - std::lock_guard guard(internal_mutex_); - if (!locked_) { - locked_ = true; - return true; - } - return false; + bool expected = false; + return locked_.compare_exchange_strong(expected, true, + std::memory_order_acquire, std::memory_order_relaxed); } - + /// Release the mutex void unlock() { std::coroutine_handle<> to_resume; - + { std::lock_guard guard(internal_mutex_); - + if (!waiters_.empty()) { // Wake up next waiter to_resume = waiters_.front(); waiters_.pop(); - // Lock remains held by the woken coroutine + // Lock remains held by the woken coroutine (locked_ stays true) } else { - locked_ = false; + // No waiters - release the lock + locked_.store(false, std::memory_order_release); } } - + // Re-schedule the waiter through the scheduler instead of resuming directly // This avoids deep recursion and ownership confusion if (to_resume) { runtime::schedule_handle(to_resume); } } - + /// Check if mutex is currently locked bool is_locked() const noexcept { - std::lock_guard guard(internal_mutex_); - return locked_; + return locked_.load(std::memory_order_acquire); } - + private: mutable std::mutex internal_mutex_; - bool locked_ = false; + std::atomic locked_{false}; std::queue> waiters_; }; @@ -159,178 +161,219 @@ class lock_guard { /// Coroutine-aware shared mutex (read-write lock) /// Allows multiple readers or a single writer +/// +/// Optimized with atomic fast paths for readers: +/// - try_lock_shared uses atomic fetch_add without mutex +/// - Reader-heavy workloads see ~100x improvement +/// +/// State encoding (64-bit): +/// - Bit 63: writer_waiting flag +/// - Bit 62: writer_active flag +/// - Bits 0-61: reader_count (max ~4.6 quintillion readers) class shared_mutex { public: shared_mutex() = default; ~shared_mutex() = default; - + // Non-copyable, non-movable shared_mutex(const shared_mutex&) = delete; shared_mutex& operator=(const shared_mutex&) = delete; shared_mutex(shared_mutex&&) = delete; shared_mutex& operator=(shared_mutex&&) = delete; - + +private: + // State bit masks + static constexpr uint64_t WRITER_ACTIVE = 1ULL << 62; + static constexpr uint64_t WRITER_WAITING = 1ULL << 63; + static constexpr uint64_t READER_MASK = (1ULL << 62) - 1; + static constexpr uint64_t WRITER_FLAGS = WRITER_ACTIVE | WRITER_WAITING; + +public: /// Shared lock awaitable (for readers) class lock_shared_awaitable { public: explicit lock_shared_awaitable(shared_mutex& m) : mutex_(m) {} - + bool await_ready() const noexcept { return mutex_.try_lock_shared(); } - + bool await_suspend(std::coroutine_handle<> awaiter) noexcept { std::lock_guard guard(mutex_.internal_mutex_); - - // Can acquire if no writer holds the lock and no writers are waiting - // (or we choose to allow readers even when writers wait - configurable policy) - if (!mutex_.writer_active_ && mutex_.pending_writers_ == 0) { - ++mutex_.reader_count_; + + // Check state under lock + uint64_t state = mutex_.state_.load(std::memory_order_relaxed); + if (!(state & WRITER_FLAGS)) { + // No writer active or waiting - acquire read lock + mutex_.state_.fetch_add(1, std::memory_order_acquire); return false; // Don't suspend, we got the lock } - + // Add to reader wait queue mutex_.reader_waiters_.push(awaiter); return true; // Suspend } - + void await_resume() const noexcept {} - + private: shared_mutex& mutex_; }; - + /// Exclusive lock awaitable (for writers) class lock_awaitable { public: explicit lock_awaitable(shared_mutex& m) : mutex_(m) {} - + bool await_ready() const noexcept { return mutex_.try_lock(); } - + bool await_suspend(std::coroutine_handle<> awaiter) noexcept { std::lock_guard guard(mutex_.internal_mutex_); - - if (!mutex_.writer_active_ && mutex_.reader_count_ == 0) { - mutex_.writer_active_ = true; + + uint64_t state = mutex_.state_.load(std::memory_order_relaxed); + if (state == 0) { + // No readers or writers - acquire write lock + mutex_.state_.store(WRITER_ACTIVE, std::memory_order_release); return false; // Don't suspend, we got the lock } - - // Track pending writer and add to wait queue + + // Mark writer waiting and add to wait queue + mutex_.state_.fetch_or(WRITER_WAITING, std::memory_order_relaxed); ++mutex_.pending_writers_; mutex_.writer_waiters_.push(awaiter); return true; // Suspend } - + void await_resume() const noexcept {} - + private: shared_mutex& mutex_; }; - + /// Acquire shared (read) lock auto lock_shared() { return lock_shared_awaitable(*this); } - + /// Acquire exclusive (write) lock auto lock() { return lock_awaitable(*this); } - + /// Try to acquire shared lock without waiting + /// Lock-free fast path using atomic CAS - no mutex needed in common case bool try_lock_shared() noexcept { - std::lock_guard guard(internal_mutex_); - if (!writer_active_ && pending_writers_ == 0) { - ++reader_count_; - return true; + uint64_t state = state_.load(std::memory_order_relaxed); + + // Fast path: if no writer active/waiting, try to increment reader count + while (!(state & WRITER_FLAGS)) { + if (state_.compare_exchange_weak(state, state + 1, + std::memory_order_acquire, std::memory_order_relaxed)) { + return true; + } + // CAS failed, state was updated - loop will re-check } return false; } - + /// Try to acquire exclusive lock without waiting bool try_lock() noexcept { - std::lock_guard guard(internal_mutex_); - if (!writer_active_ && reader_count_ == 0) { - writer_active_ = true; - return true; - } - return false; + uint64_t expected = 0; + return state_.compare_exchange_strong(expected, WRITER_ACTIVE, + std::memory_order_acquire, std::memory_order_relaxed); } - + /// Release shared (read) lock void unlock_shared() { - std::vector> to_resume; - + // Decrement reader count atomically + uint64_t prev_state = state_.fetch_sub(1, std::memory_order_release); + uint64_t new_readers = (prev_state & READER_MASK) - 1; + + // Fast path: if there are still readers or no writer waiting, done + if (new_readers > 0 || !(prev_state & WRITER_WAITING)) { + return; + } + + // Slow path: might need to wake a writer + std::coroutine_handle<> to_resume; { std::lock_guard guard(internal_mutex_); - - --reader_count_; - - // If no more readers and a writer is waiting, wake the writer - if (reader_count_ == 0 && !writer_waiters_.empty()) { + + // Double-check under lock + uint64_t state = state_.load(std::memory_order_relaxed); + if ((state & READER_MASK) == 0 && !writer_waiters_.empty()) { auto writer = writer_waiters_.front(); writer_waiters_.pop(); --pending_writers_; - writer_active_ = true; - to_resume.push_back(writer); + + // Clear WRITER_WAITING if no more pending writers, set WRITER_ACTIVE + uint64_t new_state = WRITER_ACTIVE; + if (pending_writers_ > 0) { + new_state |= WRITER_WAITING; + } + state_.store(new_state, std::memory_order_release); + to_resume = writer; } } - - for (auto& h : to_resume) { - runtime::schedule_handle(h); + + if (to_resume) { + runtime::schedule_handle(to_resume); } } - + /// Release exclusive (write) lock void unlock() { std::vector> to_resume; - + { std::lock_guard guard(internal_mutex_); - - writer_active_ = false; - + // Prefer writers over readers to prevent writer starvation if (!writer_waiters_.empty()) { auto writer = writer_waiters_.front(); writer_waiters_.pop(); --pending_writers_; - writer_active_ = true; + + // Keep WRITER_ACTIVE, update WRITER_WAITING based on remaining writers + uint64_t new_state = WRITER_ACTIVE; + if (pending_writers_ > 0) { + new_state |= WRITER_WAITING; + } + state_.store(new_state, std::memory_order_release); to_resume.push_back(writer); } else { // Wake all waiting readers + size_t reader_count = reader_waiters_.size(); while (!reader_waiters_.empty()) { to_resume.push_back(reader_waiters_.front()); reader_waiters_.pop(); - ++reader_count_; } + // Clear writer flags and set reader count + state_.store(reader_count, std::memory_order_release); } } - + for (auto& h : to_resume) { runtime::schedule_handle(h); } } - + /// Get current reader count size_t reader_count() const noexcept { - std::lock_guard guard(internal_mutex_); - return reader_count_; + return state_.load(std::memory_order_acquire) & READER_MASK; } - + /// Check if a writer holds the lock bool is_writer_active() const noexcept { - std::lock_guard guard(internal_mutex_); - return writer_active_; + return (state_.load(std::memory_order_acquire) & WRITER_ACTIVE) != 0; } - + private: mutable std::mutex internal_mutex_; - size_t reader_count_ = 0; - size_t pending_writers_ = 0; - bool writer_active_ = false; + std::atomic state_{0}; // Packed: [writer_waiting:1][writer_active:1][readers:62] + size_t pending_writers_ = 0; // Count of pending writers (for WRITER_WAITING flag management) std::queue> reader_waiters_; std::queue> writer_waiters_; }; diff --git a/include/elio/time/timer.hpp b/include/elio/time/timer.hpp index 3aeb03e..280d88b 100644 --- a/include/elio/time/timer.hpp +++ b/include/elio/time/timer.hpp @@ -8,6 +8,10 @@ #include #include +#ifdef __linux__ +#include // For __kernel_timespec (available on all Linux) +#endif + namespace elio::time { /// Awaitable for sleeping/delaying execution @@ -19,31 +23,36 @@ class sleep_awaitable { template explicit sleep_awaitable(std::chrono::duration duration) : duration_ns_(std::chrono::duration_cast(duration).count()) {} - + /// Construct with explicit io_context template sleep_awaitable(io::io_context& ctx, std::chrono::duration duration) : ctx_(&ctx) , duration_ns_(std::chrono::duration_cast(duration).count()) {} - + bool await_ready() const noexcept { // If duration is zero or negative, complete immediately return duration_ns_ <= 0; } - + void await_suspend(std::coroutine_handle<> awaiter) { // Get io_context from current worker or use provided one io::io_context* ctx = ctx_; if (!ctx) { ctx = &io::current_io_context(); } - + // Use io_context timeout mechanism io::io_request req{}; req.op = io::io_op::timeout; req.length = static_cast(duration_ns_); req.awaiter = awaiter; - +#ifdef __linux__ + // Provide our local timespec for io_uring backend (runtime check) + // epoll backend ignores this field + req.timeout_ts = &ts_; +#endif + if (!ctx->prepare(req)) { // Failed to prepare, fall back to thread sleep ELIO_LOG_WARNING("sleep_awaitable: failed to prepare timeout, using thread sleep"); @@ -51,17 +60,20 @@ class sleep_awaitable { awaiter.resume(); return; } - + ctx->submit(); } - + void await_resume() const noexcept { // Nothing to return } - + private: io::io_context* ctx_ = nullptr; int64_t duration_ns_; +#ifdef __linux__ + mutable __kernel_timespec ts_{}; // Storage for io_uring timeout (always available on Linux) +#endif }; /// Awaitable for cancellable sleep operations @@ -148,7 +160,12 @@ class cancellable_sleep_awaitable { req.op = io::io_op::timeout; req.length = static_cast(duration_ns_); req.awaiter = awaiter; - +#ifdef __linux__ + // Provide our local timespec for io_uring backend (runtime check) + // epoll backend ignores this field + req.timeout_ts = &ts_; +#endif + if (!ctx->prepare(req)) { cancel_registration_.unregister(); // Destroy unused cancel executor @@ -158,7 +175,7 @@ class cancellable_sleep_awaitable { } // Failed to prepare, fall back to polling sleep ELIO_LOG_WARNING("cancellable_sleep: failed to prepare timeout, using polling sleep"); - auto end_time = std::chrono::steady_clock::now() + + auto end_time = std::chrono::steady_clock::now() + std::chrono::nanoseconds(duration_ns_); while (std::chrono::steady_clock::now() < end_time) { if (token_.is_cancelled()) { @@ -170,7 +187,7 @@ class cancellable_sleep_awaitable { awaiter.resume(); return; } - + ctx->submit(); } @@ -229,6 +246,9 @@ class cancellable_sleep_awaitable { std::coroutine_handle<> cancel_executor_handle_; std::atomic cancel_executor_handed_off_{false}; bool cancelled_ = false; +#ifdef __linux__ + mutable __kernel_timespec ts_{}; // Storage for io_uring timeout (always available on Linux) +#endif }; /// Sleep for a duration @@ -239,12 +259,6 @@ inline auto sleep_for(std::chrono::duration duration) { return sleep_awaitable(duration); } -/// Sleep for a duration using a specific io_context -template -inline auto sleep_for(io::io_context& ctx, std::chrono::duration duration) { - return sleep_awaitable(ctx, duration); -} - /// Sleep for a duration with cancellation support /// @param duration Duration to sleep /// @param token Cancellation token - sleep returns early if cancelled @@ -254,13 +268,6 @@ inline auto sleep_for(std::chrono::duration duration, coro::cancel_ return cancellable_sleep_awaitable(duration, std::move(token)); } -/// Sleep for a duration with cancellation support using a specific io_context -template -inline auto sleep_for(io::io_context& ctx, std::chrono::duration duration, - coro::cancel_token token) { - return cancellable_sleep_awaitable(ctx, duration, std::move(token)); -} - /// Sleep until a time point /// @param time_point Time point to sleep until /// @return Awaitable that completes at the time point diff --git a/include/elio/tls/tls_stream.hpp b/include/elio/tls/tls_stream.hpp index 7dc78ba..9bf427f 100644 --- a/include/elio/tls/tls_stream.hpp +++ b/include/elio/tls/tls_stream.hpp @@ -388,29 +388,29 @@ class tls_listener { /// Bind and create a TLS listener (IPv4) /// @return TLS listener on success, std::nullopt on error (check errno) - static std::optional - bind(const net::ipv4_address& addr, io::io_context& io_ctx, tls_context& ctx) { - auto tcp_result = net::tcp_listener::bind(addr, io_ctx); + static std::optional + bind(const net::ipv4_address& addr, tls_context& ctx) { + auto tcp_result = net::tcp_listener::bind(addr); if (!tcp_result) { return std::nullopt; } return tls_listener(std::move(*tcp_result), ctx); } - + /// Bind and create a TLS listener (IPv6) static std::optional - bind(const net::ipv6_address& addr, io::io_context& io_ctx, tls_context& ctx) { - auto tcp_result = net::tcp_listener::bind(addr, io_ctx); + bind(const net::ipv6_address& addr, tls_context& ctx) { + auto tcp_result = net::tcp_listener::bind(addr); if (!tcp_result) { return std::nullopt; } return tls_listener(std::move(*tcp_result), ctx); } - + /// Bind and create a TLS listener (generic address) static std::optional - bind(const net::socket_address& addr, io::io_context& io_ctx, tls_context& ctx) { - auto tcp_result = net::tcp_listener::bind(addr, io_ctx); + bind(const net::socket_address& addr, tls_context& ctx) { + auto tcp_result = net::tcp_listener::bind(addr); if (!tcp_result) { return std::nullopt; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 70356fb..f94800f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -20,6 +20,18 @@ set(TEST_SOURCES integration/test_dynamic_threads.cpp ) +# Add HTTP tests (no TLS dependency for parser tests) +list(APPEND TEST_SOURCES + unit/test_http_parser.cpp +) + +# Add HTTP/2 tests if nghttp2 is available +if(TARGET nghttp2_static) + list(APPEND TEST_SOURCES + unit/test_http2.cpp + ) +endif() + # Add WebSocket and SSE tests if TLS is available if(TARGET elio_tls) list(APPEND TEST_SOURCES @@ -34,6 +46,9 @@ target_link_libraries(elio_tests PRIVATE elio Catch2::Catch2WithMain) if(TARGET elio_tls) target_link_libraries(elio_tests PRIVATE elio_tls) endif() +if(TARGET nghttp2_static) + target_link_libraries(elio_tests PRIVATE nghttp2_static) +endif() target_compile_definitions(elio_tests PRIVATE ELIO_DEBUG) # ASAN-enabled test executable @@ -42,6 +57,9 @@ target_link_libraries(elio_tests_asan PRIVATE elio Catch2::Catch2WithMain) if(TARGET elio_tls) target_link_libraries(elio_tests_asan PRIVATE elio_tls) endif() +if(TARGET nghttp2_static) + target_link_libraries(elio_tests_asan PRIVATE nghttp2_static) +endif() target_compile_options(elio_tests_asan PRIVATE -fsanitize=address -g -fno-omit-frame-pointer) target_link_options(elio_tests_asan PRIVATE -fsanitize=address) target_compile_definitions(elio_tests_asan PRIVATE ELIO_DEBUG) @@ -52,6 +70,9 @@ target_link_libraries(elio_tests_tsan PRIVATE elio Catch2::Catch2WithMain) if(TARGET elio_tls) target_link_libraries(elio_tests_tsan PRIVATE elio_tls) endif() +if(TARGET nghttp2_static) + target_link_libraries(elio_tests_tsan PRIVATE nghttp2_static) +endif() target_compile_options(elio_tests_tsan PRIVATE -fsanitize=thread -g -fno-omit-frame-pointer -Wno-tsan) target_link_options(elio_tests_tsan PRIVATE -fsanitize=thread) target_compile_definitions(elio_tests_tsan PRIVATE ELIO_DEBUG) diff --git a/tests/unit/test_http2.cpp b/tests/unit/test_http2.cpp new file mode 100644 index 0000000..0889f10 --- /dev/null +++ b/tests/unit/test_http2.cpp @@ -0,0 +1,71 @@ +#include +#include + +using namespace elio::http; + +TEST_CASE("HTTP/2 error codes", "[http2]") { + SECTION("Error code values match nghttp2") { + REQUIRE(static_cast(h2_error::none) == 0); + REQUIRE(static_cast(h2_error::protocol_error) == NGHTTP2_PROTOCOL_ERROR); + REQUIRE(static_cast(h2_error::internal_error) == NGHTTP2_INTERNAL_ERROR); + REQUIRE(static_cast(h2_error::flow_control_error) == NGHTTP2_FLOW_CONTROL_ERROR); + REQUIRE(static_cast(h2_error::stream_closed) == NGHTTP2_STREAM_CLOSED); + REQUIRE(static_cast(h2_error::frame_size_error) == NGHTTP2_FRAME_SIZE_ERROR); + REQUIRE(static_cast(h2_error::refused_stream) == NGHTTP2_REFUSED_STREAM); + REQUIRE(static_cast(h2_error::cancel) == NGHTTP2_CANCEL); + REQUIRE(static_cast(h2_error::compression_error) == NGHTTP2_COMPRESSION_ERROR); + } +} + +TEST_CASE("HTTP/2 stream state", "[http2]") { + h2_stream stream; + + SECTION("Default state") { + REQUIRE(stream.stream_id == -1); + REQUIRE_FALSE(stream.headers_complete); + REQUIRE_FALSE(stream.body_complete); + REQUIRE_FALSE(stream.closed); + REQUIRE(stream.error == h2_error::none); + REQUIRE_FALSE(stream.is_complete()); + } + + SECTION("Headers complete") { + stream.headers_complete = true; + REQUIRE_FALSE(stream.is_complete()); + } + + SECTION("Body complete") { + stream.body_complete = true; + REQUIRE_FALSE(stream.is_complete()); + } + + SECTION("Fully complete") { + stream.headers_complete = true; + stream.body_complete = true; + REQUIRE(stream.is_complete()); + } + + SECTION("Stream with data") { + stream.stream_id = 1; + stream.response_status = status::ok; + stream.response_body = "Hello, World!"; + stream.response_headers.set("Content-Type", "text/plain"); + stream.headers_complete = true; + stream.body_complete = true; + + REQUIRE(stream.stream_id == 1); + REQUIRE(stream.response_status == status::ok); + REQUIRE(stream.response_body == "Hello, World!"); + REQUIRE(stream.response_headers.get("Content-Type") == "text/plain"); + REQUIRE(stream.is_complete()); + } +} + +TEST_CASE("nghttp2 library version", "[http2]") { + // Basic check that nghttp2 is linked and functional + nghttp2_info* info = nghttp2_version(0); + REQUIRE(info != nullptr); + REQUIRE(info->version_num > 0); + + INFO("nghttp2 version: " << info->version_str); +} diff --git a/tests/unit/test_http_parser.cpp b/tests/unit/test_http_parser.cpp new file mode 100644 index 0000000..a8069ea --- /dev/null +++ b/tests/unit/test_http_parser.cpp @@ -0,0 +1,637 @@ +#include +#include +#include +#include + +using namespace elio::http; + +TEST_CASE("HTTP request parser - basic GET request", "[http][parser]") { + request_parser parser; + + std::string request = + "GET /path/to/resource HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: test/1.0\r\n" + "\r\n"; + + auto [result, consumed] = parser.parse(request); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.get_method() == method::GET); + REQUIRE(parser.path() == "/path/to/resource"); + REQUIRE(parser.version() == "HTTP/1.1"); + REQUIRE(parser.get_headers().get("Host") == "example.com"); + REQUIRE(parser.get_headers().get("User-Agent") == "test/1.0"); + REQUIRE(parser.body().empty()); +} + +TEST_CASE("HTTP request parser - GET with query string", "[http][parser]") { + request_parser parser; + + std::string request = + "GET /search?q=hello&page=1 HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"; + + auto [result, consumed] = parser.parse(request); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.path() == "/search"); + REQUIRE(parser.query() == "q=hello&page=1"); +} + +TEST_CASE("HTTP request parser - POST with body", "[http][parser]") { + request_parser parser; + + std::string request = + "POST /api/data HTTP/1.1\r\n" + "Host: example.com\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 13\r\n" + "\r\n" + "{\"key\":\"val\"}"; + + auto [result, consumed] = parser.parse(request); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.get_method() == method::POST); + REQUIRE(parser.body() == "{\"key\":\"val\"}"); + REQUIRE(parser.get_headers().content_type() == "application/json"); +} + +TEST_CASE("HTTP request parser - chunked encoding", "[http][parser]") { + request_parser parser; + + std::string request = + "POST /upload HTTP/1.1\r\n" + "Host: example.com\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\n" + "Hello\r\n" + "6\r\n" + " World\r\n" + "0\r\n" + "\r\n"; + + auto [result, consumed] = parser.parse(request); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.body() == "Hello World"); +} + +TEST_CASE("HTTP request parser - incremental parsing", "[http][parser]") { + request_parser parser; + + // Send request in chunks + std::string part1 = "GET /test HTTP/1.1\r\n"; + std::string part2 = "Host: example.com\r\n"; + std::string part3 = "\r\n"; + + auto [r1, c1] = parser.parse(part1); + REQUIRE(r1 == parse_result::need_more); + REQUIRE_FALSE(parser.is_complete()); + + auto [r2, c2] = parser.parse(part2); + REQUIRE(r2 == parse_result::need_more); + REQUIRE_FALSE(parser.is_complete()); + + auto [r3, c3] = parser.parse(part3); + REQUIRE(r3 == parse_result::complete); + REQUIRE(parser.is_complete()); +} + +TEST_CASE("HTTP request parser - all methods", "[http][parser]") { + auto test_method = [](const char* method_str, method expected) { + request_parser parser; + std::string request = std::string(method_str) + " / HTTP/1.1\r\nHost: test\r\n\r\n"; + auto [result, consumed] = parser.parse(request); + REQUIRE(result == parse_result::complete); + REQUIRE(parser.get_method() == expected); + }; + + test_method("GET", method::GET); + test_method("HEAD", method::HEAD); + test_method("POST", method::POST); + test_method("PUT", method::PUT); + test_method("DELETE", method::DELETE_); + test_method("PATCH", method::PATCH); + test_method("OPTIONS", method::OPTIONS); + test_method("CONNECT", method::CONNECT); + test_method("TRACE", method::TRACE); +} + +TEST_CASE("HTTP request parser - invalid request", "[http][parser]") { + SECTION("Missing method") { + request_parser parser; + auto [result, consumed] = parser.parse("/ HTTP/1.1\r\n\r\n"); + REQUIRE(result == parse_result::error); + REQUIRE(parser.has_error()); + } + + SECTION("Unknown method") { + request_parser parser; + auto [result, consumed] = parser.parse("INVALID / HTTP/1.1\r\n\r\n"); + REQUIRE(result == parse_result::error); + } + + SECTION("Invalid HTTP version") { + request_parser parser; + auto [result, consumed] = parser.parse("GET / FTP/1.0\r\n\r\n"); + REQUIRE(result == parse_result::error); + } + + SECTION("Missing colon in header") { + request_parser parser; + auto [result, consumed] = parser.parse("GET / HTTP/1.1\r\nBadHeader\r\n\r\n"); + REQUIRE(result == parse_result::error); + } +} + +TEST_CASE("HTTP request parser - reset", "[http][parser]") { + request_parser parser; + + std::string request1 = "GET /first HTTP/1.1\r\nHost: test\r\n\r\n"; + auto [r1, c1] = parser.parse(request1); + REQUIRE(r1 == parse_result::complete); + REQUIRE(parser.path() == "/first"); + + parser.reset(); + + std::string request2 = "POST /second HTTP/1.1\r\nHost: test\r\nContent-Length: 4\r\n\r\ntest"; + auto [r2, c2] = parser.parse(request2); + REQUIRE(r2 == parse_result::complete); + REQUIRE(parser.path() == "/second"); + REQUIRE(parser.get_method() == method::POST); + REQUIRE(parser.body() == "test"); +} + +// Response parser tests + +TEST_CASE("HTTP response parser - basic 200 OK", "[http][parser]") { + response_parser parser; + + std::string response = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 13\r\n" + "\r\n" + ""; + + auto [result, consumed] = parser.parse(response); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.get_status() == status::ok); + REQUIRE(parser.status_code() == 200); + REQUIRE(parser.version() == "HTTP/1.1"); + REQUIRE(parser.reason() == "OK"); + REQUIRE(parser.body() == ""); +} + +TEST_CASE("HTTP response parser - various status codes", "[http][parser]") { + auto test_status = [](uint16_t code, const char* reason, status expected) { + response_parser parser; + std::string response = "HTTP/1.1 " + std::to_string(code) + " " + reason + "\r\n\r\n"; + auto [result, consumed] = parser.parse(response); + REQUIRE(result == parse_result::complete); + REQUIRE(parser.get_status() == expected); + REQUIRE(parser.status_code() == code); + }; + + test_status(200, "OK", status::ok); + test_status(201, "Created", status::created); + test_status(204, "No Content", status::no_content); + test_status(301, "Moved Permanently", status::moved_permanently); + test_status(302, "Found", status::found); + test_status(400, "Bad Request", status::bad_request); + test_status(401, "Unauthorized", status::unauthorized); + test_status(403, "Forbidden", status::forbidden); + test_status(404, "Not Found", status::not_found); + test_status(500, "Internal Server Error", status::internal_server_error); + test_status(503, "Service Unavailable", status::service_unavailable); +} + +TEST_CASE("HTTP response parser - chunked response", "[http][parser]") { + response_parser parser; + + std::string response = + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "7\r\n" + "Mozilla\r\n" + "9\r\n" + "Developer\r\n" + "7\r\n" + "Network\r\n" + "0\r\n" + "\r\n"; + + auto [result, consumed] = parser.parse(response); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.body() == "MozillaDeveloperNetwork"); +} + +TEST_CASE("HTTP response parser - incremental parsing", "[http][parser]") { + response_parser parser; + + std::string part1 = "HTTP/1.1 200 OK\r\n"; + std::string part2 = "Content-Length: 5\r\n\r\n"; + std::string part3 = "Hello"; + + auto [r1, c1] = parser.parse(part1); + REQUIRE(r1 == parse_result::need_more); + + auto [r2, c2] = parser.parse(part2); + REQUIRE(r2 == parse_result::need_more); + + auto [r3, c3] = parser.parse(part3); + REQUIRE(r3 == parse_result::complete); + REQUIRE(parser.body() == "Hello"); +} + +TEST_CASE("HTTP response parser - no body", "[http][parser]") { + response_parser parser; + + std::string response = + "HTTP/1.1 204 No Content\r\n" + "Server: test\r\n" + "\r\n"; + + auto [result, consumed] = parser.parse(response); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.get_status() == status::no_content); + REQUIRE(parser.body().empty()); +} + +TEST_CASE("HTTP response parser - case insensitive headers", "[http][parser]") { + response_parser parser; + + std::string response = + "HTTP/1.1 200 OK\r\n" + "content-type: application/json\r\n" + "CONTENT-LENGTH: 2\r\n" + "\r\n" + "{}"; + + auto [result, consumed] = parser.parse(response); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.get_headers().get("Content-Type") == "application/json"); + REQUIRE(parser.get_headers().content_length().value() == 2); +} + +TEST_CASE("HTTP response parser - header whitespace trimming", "[http][parser]") { + response_parser parser; + + std::string response = + "HTTP/1.1 200 OK\r\n" + "X-Custom: value with spaces \r\n" + "Content-Length: 0\r\n" + "\r\n"; + + auto [result, consumed] = parser.parse(response); + + REQUIRE(result == parse_result::complete); + REQUIRE(parser.get_headers().get("X-Custom") == "value with spaces"); +} + +TEST_CASE("HTTP response parser - reset", "[http][parser]") { + response_parser parser; + + std::string response1 = "HTTP/1.1 404 Not Found\r\n\r\n"; + auto [r1, c1] = parser.parse(response1); + REQUIRE(r1 == parse_result::complete); + REQUIRE(parser.status_code() == 404); + + parser.reset(); + + std::string response2 = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nfoo"; + auto [r2, c2] = parser.parse(response2); + REQUIRE(r2 == parse_result::complete); + REQUIRE(parser.status_code() == 200); + REQUIRE(parser.body() == "foo"); +} + +// URL parsing tests + +TEST_CASE("URL parsing - basic URLs", "[http][url]") { + SECTION("Simple HTTP URL") { + auto u = url::parse("http://example.com/path"); + REQUIRE(u.has_value()); + REQUIRE(u->scheme == "http"); + REQUIRE(u->host == "example.com"); + REQUIRE(u->port == 0); + REQUIRE(u->path == "/path"); + REQUIRE(u->effective_port() == 80); + REQUIRE_FALSE(u->is_secure()); + } + + SECTION("HTTPS URL with port") { + auto u = url::parse("https://example.com:8443/api"); + REQUIRE(u.has_value()); + REQUIRE(u->scheme == "https"); + REQUIRE(u->host == "example.com"); + REQUIRE(u->port == 8443); + REQUIRE(u->path == "/api"); + REQUIRE(u->effective_port() == 8443); + REQUIRE(u->is_secure()); + } + + SECTION("URL with query string") { + auto u = url::parse("http://example.com/search?q=test&page=1"); + REQUIRE(u.has_value()); + REQUIRE(u->path == "/search"); + REQUIRE(u->query == "q=test&page=1"); + REQUIRE(u->path_with_query() == "/search?q=test&page=1"); + } + + SECTION("URL with fragment") { + auto u = url::parse("http://example.com/page#section"); + REQUIRE(u.has_value()); + REQUIRE(u->path == "/page"); + REQUIRE(u->fragment == "section"); + } + + SECTION("URL with userinfo") { + auto u = url::parse("http://user:pass@example.com/"); + REQUIRE(u.has_value()); + REQUIRE(u->userinfo == "user:pass"); + REQUIRE(u->host == "example.com"); + REQUIRE(u->authority() == "user:pass@example.com"); + } +} + +TEST_CASE("URL parsing - edge cases", "[http][url]") { + SECTION("No scheme implies http") { + auto u = url::parse("example.com/path"); + REQUIRE(u.has_value()); + REQUIRE(u->scheme == "http"); + REQUIRE(u->host == "example.com"); + } + + SECTION("No path implies /") { + auto u = url::parse("http://example.com"); + REQUIRE(u.has_value()); + REQUIRE(u->path == "/"); + } + + SECTION("Empty host is invalid") { + auto u = url::parse("http:///path"); + REQUIRE_FALSE(u.has_value()); + } + + SECTION("Mixed case scheme") { + auto u = url::parse("HTTPS://Example.COM/Path"); + REQUIRE(u.has_value()); + REQUIRE(u->scheme == "https"); + REQUIRE(u->host == "Example.COM"); // Host preserved + } +} + +TEST_CASE("URL parsing - IPv6", "[http][url]") { + auto u = url::parse("http://[::1]:8080/test"); + REQUIRE(u.has_value()); + REQUIRE(u->host == "::1"); + REQUIRE(u->port == 8080); + REQUIRE(u->path == "/test"); +} + +TEST_CASE("URL encoding/decoding", "[http][url]") { + SECTION("Encode special characters") { + REQUIRE(url_encode("hello world") == "hello%20world"); + REQUIRE(url_encode("foo=bar") == "foo%3Dbar"); + REQUIRE(url_encode("test&value") == "test%26value"); + } + + SECTION("Decode percent-encoded") { + REQUIRE(url_decode("hello%20world") == "hello world"); + REQUIRE(url_decode("foo%3Dbar") == "foo=bar"); + REQUIRE(url_decode("test%26value") == "test&value"); + } + + SECTION("Plus as space") { + REQUIRE(url_decode("hello+world") == "hello world"); + } + + SECTION("Unreserved characters unchanged") { + std::string unreserved = "abcABC123-_.~"; + REQUIRE(url_encode(unreserved) == unreserved); + } +} + +TEST_CASE("Query string parsing", "[http][url]") { + SECTION("Simple pairs") { + auto params = parse_query_string("foo=bar&baz=qux"); + REQUIRE(params["foo"] == "bar"); + REQUIRE(params["baz"] == "qux"); + } + + SECTION("URL-encoded values") { + auto params = parse_query_string("name=John%20Doe&city=New%20York"); + REQUIRE(params["name"] == "John Doe"); + REQUIRE(params["city"] == "New York"); + } + + SECTION("Empty value") { + auto params = parse_query_string("flag&key=value"); + REQUIRE(params["flag"] == ""); + REQUIRE(params["key"] == "value"); + } +} + +// Headers tests + +TEST_CASE("Headers collection", "[http][headers]") { + headers h; + + SECTION("Set and get") { + h.set("Content-Type", "text/html"); + REQUIRE(h.get("Content-Type") == "text/html"); + REQUIRE(h.get("content-type") == "text/html"); // Case insensitive + REQUIRE(h.get("CONTENT-TYPE") == "text/html"); + } + + SECTION("Contains") { + h.set("X-Custom", "value"); + REQUIRE(h.contains("X-Custom")); + REQUIRE(h.contains("x-custom")); + REQUIRE_FALSE(h.contains("X-Other")); + } + + SECTION("Remove") { + h.set("X-Remove", "value"); + REQUIRE(h.contains("X-Remove")); + h.remove("x-remove"); + REQUIRE_FALSE(h.contains("X-Remove")); + } + + SECTION("Content-Length") { + h.set_content_length(1234); + REQUIRE(h.content_length().value() == 1234); + } + + SECTION("Keep-alive detection") { + headers h1; + h1.set("Connection", "keep-alive"); + REQUIRE(h1.keep_alive("1.1")); + + headers h2; + h2.set("Connection", "close"); + REQUIRE_FALSE(h2.keep_alive("1.1")); + + headers h3; + REQUIRE(h3.keep_alive("1.1")); // Default for HTTP/1.1 + REQUIRE_FALSE(h3.keep_alive("1.0")); // Default for HTTP/1.0 + } + + SECTION("Chunked detection") { + headers h1; + h1.set("Transfer-Encoding", "chunked"); + REQUIRE(h1.is_chunked()); + + headers h2; + REQUIRE_FALSE(h2.is_chunked()); + } +} + +TEST_CASE("Method to string conversion", "[http][method]") { + REQUIRE(method_to_string(method::GET) == "GET"); + REQUIRE(method_to_string(method::POST) == "POST"); + REQUIRE(method_to_string(method::PUT) == "PUT"); + REQUIRE(method_to_string(method::DELETE_) == "DELETE"); + REQUIRE(method_to_string(method::PATCH) == "PATCH"); + REQUIRE(method_to_string(method::HEAD) == "HEAD"); + REQUIRE(method_to_string(method::OPTIONS) == "OPTIONS"); +} + +TEST_CASE("Status reason phrase", "[http][status]") { + REQUIRE(status_reason(status::ok) == "OK"); + REQUIRE(status_reason(status::not_found) == "Not Found"); + REQUIRE(status_reason(status::internal_server_error) == "Internal Server Error"); + REQUIRE(status_reason(status::moved_permanently) == "Moved Permanently"); + REQUIRE(status_reason(status::unauthorized) == "Unauthorized"); +} + +// HTTP Message tests + +TEST_CASE("HTTP request serialization", "[http][message]") { + SECTION("Simple GET request") { + request req(method::GET, "/api/users"); + req.set_host("example.com"); + + std::string serialized = req.serialize(); + + REQUIRE(serialized.find("GET /api/users HTTP/1.1\r\n") != std::string::npos); + REQUIRE(serialized.find("Host: example.com\r\n") != std::string::npos); + REQUIRE(serialized.ends_with("\r\n\r\n")); + } + + SECTION("POST request with body") { + request req(method::POST, "/api/data"); + req.set_host("example.com"); + req.set_body(std::string_view("{\"key\":\"value\"}")); + req.set_content_type("application/json"); + + std::string serialized = req.serialize(); + + REQUIRE(serialized.find("POST /api/data HTTP/1.1\r\n") != std::string::npos); + REQUIRE(serialized.find("Content-Type: application/json\r\n") != std::string::npos); + REQUIRE(serialized.find("{\"key\":\"value\"}") != std::string::npos); + } + + SECTION("Request with query string") { + request req(method::GET, "/search"); + req.set_query("q=test&page=1"); + req.set_host("example.com"); + + std::string serialized = req.serialize(); + + REQUIRE(serialized.find("GET /search?q=test&page=1 HTTP/1.1\r\n") != std::string::npos); + } +} + +TEST_CASE("HTTP request from parser roundtrip", "[http][message]") { + std::string original = + "POST /api/test HTTP/1.1\r\n" + "Host: example.com\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 13\r\n" + "\r\n" + "{\"key\":\"val\"}"; + + request_parser parser; + auto [result, consumed] = parser.parse(original); + REQUIRE(result == parse_result::complete); + + request req = request::from_parser(parser); + REQUIRE(req.get_method() == method::POST); + REQUIRE(req.path() == "/api/test"); + REQUIRE(req.host() == "example.com"); + REQUIRE(req.body() == "{\"key\":\"val\"}"); +} + +TEST_CASE("HTTP response creation", "[http][message]") { + SECTION("Simple text response") { + response resp(status::ok, "Hello, World!", mime::text_plain); + + REQUIRE(resp.get_status() == status::ok); + REQUIRE(resp.status_code() == 200); + REQUIRE(resp.body() == "Hello, World!"); + REQUIRE(resp.get_headers().content_type() == "text/plain"); + REQUIRE(resp.get_headers().content_length().value() == 13); + } + + SECTION("JSON response") { + response resp(status::ok, "{\"success\":true}", mime::application_json); + + REQUIRE(resp.get_headers().content_type() == "application/json"); + } + + SECTION("Error response") { + response resp(status::not_found); + + REQUIRE(resp.get_status() == status::not_found); + REQUIRE(resp.status_code() == 404); + } + + SECTION("Redirect response") { + response resp(status::moved_permanently); + resp.set_header("Location", "https://new-url.example.com/"); + + REQUIRE(resp.is_redirect()); + REQUIRE(resp.header("Location") == "https://new-url.example.com/"); + } +} + +TEST_CASE("HTTP response serialization", "[http][message]") { + response resp(status::ok, "Hello", mime::text_plain); + resp.set_header("X-Custom", "value"); + + std::string serialized = resp.serialize(); + + REQUIRE(serialized.find("HTTP/1.1 200 OK\r\n") != std::string::npos); + REQUIRE(serialized.find("Content-Type: text/plain\r\n") != std::string::npos); + REQUIRE(serialized.find("X-Custom: value\r\n") != std::string::npos); + REQUIRE(serialized.find("\r\n\r\nHello") != std::string::npos); +} + +TEST_CASE("HTTP response from parser roundtrip", "[http][message]") { + std::string body = "

Hello

"; + std::string original = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Content-Length: " + std::to_string(body.size()) + "\r\n" + "\r\n" + body; + + response_parser parser; + auto [result, consumed] = parser.parse(original); + REQUIRE(result == parse_result::complete); + + response resp = response::from_parser(parser); + REQUIRE(resp.get_status() == status::ok); + REQUIRE(resp.body() == body); + REQUIRE(resp.header("Content-Type") == "text/html"); +} diff --git a/tests/unit/test_io.cpp b/tests/unit/test_io.cpp index f973b9e..7cabd9a 100644 --- a/tests/unit/test_io.cpp +++ b/tests/unit/test_io.cpp @@ -532,15 +532,15 @@ TEST_CASE("UDS listener bind and accept", "[uds][listener]") { auto addr = unix_address::abstract("elio_test_listener_" + std::to_string(getpid())); SECTION("bind creates listener") { - auto listener = uds_listener::bind(addr, default_io_context()); + auto listener = uds_listener::bind(addr); REQUIRE(listener.has_value()); REQUIRE(listener->is_valid()); REQUIRE(listener->fd() >= 0); REQUIRE(listener->local_address().to_string() == addr.to_string()); } - + SECTION("accept returns connection") { - auto listener = uds_listener::bind(addr, default_io_context()); + auto listener = uds_listener::bind(addr); REQUIRE(listener.has_value()); // Create a client connection in a separate thread @@ -573,6 +573,7 @@ TEST_CASE("UDS listener bind and accept", "[uds][listener]") { auto stream = co_await listener->accept(); accepted_stream = std::move(stream); accepted = true; + co_return; }; auto t = accept_coro(); @@ -595,9 +596,9 @@ TEST_CASE("UDS listener bind and accept", "[uds][listener]") { TEST_CASE("UDS connect", "[uds][connect]") { auto addr = unix_address::abstract("elio_test_connect_" + std::to_string(getpid())); - + // Create server listener - auto listener = uds_listener::bind(addr, default_io_context()); + auto listener = uds_listener::bind(addr); REQUIRE(listener.has_value()); // Start accept in background @@ -614,27 +615,29 @@ TEST_CASE("UDS connect", "[uds][connect]") { auto stream = co_await listener->accept(); server_stream = std::move(stream); server_accepted = true; + co_return; }; - + auto connect_coro = [&]() -> task { auto stream = co_await uds_connect(addr); client_stream = std::move(stream); client_connected = true; + co_return; }; - + auto accept_task = accept_coro(); auto connect_task = connect_coro(); - + sched.spawn(accept_task.release()); sched.spawn(connect_task.release()); - + // Wait until both complete for (int i = 0; i < 200 && (!server_accepted || !client_connected); ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); } - + sched.shutdown(); - + REQUIRE(server_accepted); REQUIRE(client_connected); REQUIRE(server_stream.has_value()); @@ -645,9 +648,9 @@ TEST_CASE("UDS connect", "[uds][connect]") { TEST_CASE("UDS stream read/write", "[uds][stream]") { auto addr = unix_address::abstract("elio_test_rw_" + std::to_string(getpid())); - + // Create server and client - auto listener = uds_listener::bind(addr, default_io_context()); + auto listener = uds_listener::bind(addr); REQUIRE(listener.has_value()); std::optional server_stream; @@ -661,12 +664,14 @@ TEST_CASE("UDS stream read/write", "[uds][stream]") { auto stream = co_await listener->accept(); server_stream = std::move(stream); setup_complete++; + co_return; }; - + auto connect_coro = [&]() -> task { auto stream = co_await uds_connect(addr); client_stream = std::move(stream); setup_complete++; + co_return; }; auto accept_task = accept_coro(); @@ -759,8 +764,8 @@ TEST_CASE("UDS stream read/write", "[uds][stream]") { TEST_CASE("UDS multiple concurrent connections", "[uds][concurrent]") { auto addr = unix_address::abstract("elio_test_concurrent_" + std::to_string(getpid())); - - auto listener = uds_listener::bind(addr, default_io_context()); + + auto listener = uds_listener::bind(addr); REQUIRE(listener.has_value()); constexpr int NUM_CLIENTS = 3; @@ -777,33 +782,39 @@ TEST_CASE("UDS multiple concurrent connections", "[uds][concurrent]") { auto stream = co_await listener->accept(); server_streams[0] = std::move(stream); accepts_done++; + co_return; }; auto accept1 = [&]() -> task { auto stream = co_await listener->accept(); server_streams[1] = std::move(stream); accepts_done++; + co_return; }; auto accept2 = [&]() -> task { auto stream = co_await listener->accept(); server_streams[2] = std::move(stream); accepts_done++; + co_return; }; - + // Connect coroutines auto connect0 = [&]() -> task { auto stream = co_await uds_connect(addr); client_streams[0] = std::move(stream); connects_done++; + co_return; }; auto connect1 = [&]() -> task { auto stream = co_await uds_connect(addr); client_streams[1] = std::move(stream); connects_done++; + co_return; }; auto connect2 = [&]() -> task { auto stream = co_await uds_connect(addr); client_streams[2] = std::move(stream); connects_done++; + co_return; }; auto a0 = accept0(); auto a1 = accept1(); auto a2 = accept2(); @@ -843,34 +854,36 @@ TEST_CASE("UDS filesystem socket", "[uds][filesystem]") { // Ensure socket file doesn't exist ::unlink(path.c_str()); - auto listener = uds_listener::bind(addr, default_io_context()); + auto listener = uds_listener::bind(addr); REQUIRE(listener.has_value()); - + // Socket file should exist struct stat st; REQUIRE(stat(path.c_str(), &st) == 0); REQUIRE(S_ISSOCK(st.st_mode)); - + // Create client connection std::atomic connected{false}; std::optional client_stream; - + std::atomic accepted{false}; std::optional server_stream; - + scheduler sched(2); sched.start(); - + auto connect_coro = [&]() -> task { auto stream = co_await uds_connect(addr); client_stream = std::move(stream); connected = true; + co_return; }; - + auto accept_coro = [&]() -> task { auto stream = co_await listener->accept(); server_stream = std::move(stream); accepted = true; + co_return; }; auto accept_task = accept_coro(); @@ -894,8 +907,8 @@ TEST_CASE("UDS filesystem socket", "[uds][filesystem]") { TEST_CASE("UDS echo test", "[uds][echo]") { auto addr = unix_address::abstract("elio_test_echo_" + std::to_string(getpid())); - - auto listener = uds_listener::bind(addr, default_io_context()); + + auto listener = uds_listener::bind(addr); REQUIRE(listener.has_value()); // Use a simpler pattern: thread for client, coroutine for server @@ -1130,16 +1143,16 @@ TEST_CASE("socket_address variant operations", "[tcp][address][socket_address]") TEST_CASE("TCP IPv6 listener and connect", "[tcp][ipv6][integration]") { SECTION("IPv6 listener binds successfully") { // Use IPv6 loopback to avoid network issues - auto listener = tcp_listener::bind(ipv6_address("::1", 0), default_io_context()); + auto listener = tcp_listener::bind(ipv6_address("::1", 0)); REQUIRE(listener.has_value()); REQUIRE(listener->is_valid()); REQUIRE(listener->local_address().family() == AF_INET6); REQUIRE(listener->local_address().port() > 0); } - + SECTION("IPv6 accept and connect") { // Create listener on IPv6 loopback - auto listener = tcp_listener::bind(ipv6_address("::1", 0), default_io_context()); + auto listener = tcp_listener::bind(ipv6_address("::1", 0)); REQUIRE(listener.has_value()); // Get the assigned port @@ -1158,12 +1171,14 @@ TEST_CASE("TCP IPv6 listener and connect", "[tcp][ipv6][integration]") { auto stream = co_await listener->accept(); server_stream = std::move(stream); accepted = true; + co_return; }; - + auto connect_coro = [&]() -> task { auto stream = co_await tcp_connect(ipv6_address("::1", port)); client_stream = std::move(stream); connected = true; + co_return; }; auto accept_task = accept_coro(); diff --git a/wiki/API-Reference.md b/wiki/API-Reference.md index e2cac8b..aa4f819 100644 --- a/wiki/API-Reference.md +++ b/wiki/API-Reference.md @@ -542,9 +542,7 @@ Async-friendly signalfd wrapper. class signal_fd { public: // Create signalfd (auto_block=true blocks signals automatically) - explicit signal_fd(const signal_set& signals, - io::io_context& ctx = io::default_io_context(), - bool auto_block = true); + explicit signal_fd(const signal_set& signals, bool auto_block = true); signal_fd(signal_fd&& other) noexcept; signal_fd& operator=(signal_fd&& other) noexcept; @@ -605,12 +603,9 @@ public: ```cpp // Wait for signals (convenience, creates temporary signal_fd) -/* awaitable */ wait_signal(const signal_set& signals, - io::io_context& ctx = io::default_io_context(), - bool auto_block = true); +/* awaitable */ wait_signal(const signal_set& signals, bool auto_block = true); -/* awaitable */ wait_signal(int signo, - io::io_context& ctx = io::default_io_context()); +/* awaitable */ wait_signal(int signo); // Signal name/number conversion const char* signal_name(int signo); // SIGINT -> "INT" @@ -694,7 +689,7 @@ public: // Bind to address (returns std::nullopt on error, check errno) static std::optional bind( const ipv4_address& addr, - io_context& ctx + const tcp_options& opts = {} ); // Accept a connection (awaitable, returns std::optional) @@ -757,26 +752,26 @@ HTTP client with connection pooling. ```cpp class client { public: - explicit client(io_context& ctx); - client(io_context& ctx, const client_config& config); - + client(); + explicit client(const client_config& config); + // GET request (awaitable) /* awaitable */ get(const std::string& url); - + // POST request (awaitable) - /* awaitable */ post(const std::string& url, + /* awaitable */ post(const std::string& url, const std::string& body, const std::string& content_type); - + // HEAD request (awaitable) /* awaitable */ head(const std::string& url); - + // Send custom request (awaitable) /* awaitable */ send(const request& req, const url& target); }; // Convenience function for one-off GET -/* awaitable */ get(io_context& ctx, const std::string& url); +/* awaitable */ get(const std::string& url); ``` ### `client_config` @@ -868,44 +863,44 @@ HTTP/2 client with connection multiplexing. ```cpp class h2_client { public: - explicit h2_client(io_context& ctx); - h2_client(io_context& ctx, const h2_client_config& config); - + h2_client(); + explicit h2_client(const h2_client_config& config); + // GET request (awaitable) /* awaitable */ get(const std::string& url); - + // POST request (awaitable) - /* awaitable */ post(const std::string& url, + /* awaitable */ post(const std::string& url, const std::string& body, const std::string& content_type); - + // PUT request (awaitable) /* awaitable */ put(const std::string& url, const std::string& body, const std::string& content_type); - + // DELETE request (awaitable) /* awaitable */ del(const std::string& url); - + // PATCH request (awaitable) /* awaitable */ patch(const std::string& url, const std::string& body, const std::string& content_type); - + // Send custom request (awaitable) /* awaitable */ send(method m, const url& target, std::string_view body = {}, std::string_view content_type = {}); - + // Access TLS context for configuration tls_context& tls_context(); }; // Convenience function for one-off HTTP/2 GET -/* awaitable */ h2_get(io_context& ctx, const std::string& url); +/* awaitable */ h2_get(const std::string& url); // Convenience function for one-off HTTP/2 POST -/* awaitable */ h2_post(io_context& ctx, const std::string& url, +/* awaitable */ h2_post(const std::string& url, const std::string& body, const std::string& content_type); ``` @@ -988,7 +983,7 @@ TLS-wrapped TCP stream. ```cpp class tls_stream { public: - tls_stream(tcp_stream tcp, tls_context& ctx, io_context& io_ctx); + tls_stream(tcp_stream tcp, tls_context& ctx); // Set SNI hostname void set_hostname(const std::string& hostname); diff --git a/wiki/Core-Concepts.md b/wiki/Core-Concepts.md index ecfdfb6..614c299 100644 --- a/wiki/Core-Concepts.md +++ b/wiki/Core-Concepts.md @@ -57,10 +57,7 @@ coro::task async_main(int argc, char* argv[]) { if (argc > 1) { std::cout << "Argument: " << argv[1] << std::endl; } - - // Access the I/O context for async operations - auto& ctx = io::default_io_context(); - + // Your async code here co_return 0; } @@ -215,15 +212,15 @@ This design ensures: ## I/O Context -The I/O context manages async I/O operations. Each worker thread has its own I/O context for lock-free operation. +The I/O context manages async I/O operations. Each worker thread has its own I/O context for lock-free operation. In most cases, you don't need to interact with io_context directly - the library automatically uses the per-thread io_context. -### Using the Default Context +### Accessing the Current Context ```cpp #include -// Get the global I/O context (for TCP listeners, etc.) -auto& ctx = io::default_io_context(); +// Get the current thread's I/O context (from within a coroutine) +auto& ctx = io::current_io_context(); ``` ### I/O Backends diff --git a/wiki/HTTP2-Guide.md b/wiki/HTTP2-Guide.md new file mode 100644 index 0000000..e7c6e3d --- /dev/null +++ b/wiki/HTTP2-Guide.md @@ -0,0 +1,285 @@ +# HTTP/2 Guide + +Elio provides full HTTP/2 client support via the nghttp2 library. This guide covers HTTP/2 usage, configuration, and best practices. + +## Overview + +HTTP/2 offers several advantages over HTTP/1.1: +- **Multiplexing**: Multiple requests/responses over a single connection +- **Header compression**: HPACK compression reduces overhead +- **Binary framing**: More efficient parsing +- **Stream prioritization**: Clients can hint at request importance + +## Requirements + +HTTP/2 support requires: +- OpenSSL with ALPN support +- nghttp2 library (fetched automatically by CMake) +- CMake option: `-DELIO_ENABLE_HTTP2=ON` + +Note: HTTP/2 requires HTTPS (h2 over TLS). For plaintext HTTP, use the HTTP/1.1 client. + +## Basic Usage + +### Simple GET Request + +```cpp +#include +#include + +using namespace elio; +using namespace elio::http; + +coro::task fetch_example() { + h2_client client; + + // Simple GET request + auto resp = co_await client.get("https://example.com/"); + if (resp) { + std::cout << "Status: " << resp->status_code() << std::endl; + std::cout << "Body: " << resp->body() << std::endl; + } +} +``` + +### POST Request with JSON + +```cpp +coro::task post_example() { + h2_client client; + + auto resp = co_await client.post( + "https://api.example.com/users", + R"({"name": "John", "email": "john@example.com"})", + mime::application_json + ); + + if (resp) { + std::cout << "Created: " << resp->body() << std::endl; + } +} +``` + +## Client Configuration + +### h2_client_config + +```cpp +struct h2_client_config { + std::chrono::seconds connect_timeout{10}; + std::chrono::seconds read_timeout{30}; + size_t max_concurrent_streams = 100; + size_t initial_window_size = 65535; + bool verify_certificate = true; + std::string user_agent = "elio-http2/1.0"; +}; + +// Usage +h2_client_config config; +config.verify_certificate = false; // For testing only! +config.max_concurrent_streams = 50; + +h2_client client(config); +``` + +### TLS Configuration + +```cpp +h2_client client; + +// Access the underlying TLS context +auto& tls_ctx = client.tls_context(); + +// Use custom CA certificate +tls_ctx.load_verify_file("/path/to/ca-bundle.crt"); + +// Use client certificate (for mutual TLS) +tls_ctx.use_certificate_file("/path/to/client.crt"); +tls_ctx.use_private_key_file("/path/to/client.key"); +``` + +## Concurrent Requests + +HTTP/2 excels at handling multiple concurrent requests: + +```cpp +coro::task parallel_requests() { + h2_client client; + + // Spawn multiple requests concurrently + auto h1 = client.get("https://api.example.com/users/1").spawn(); + auto h2 = client.get("https://api.example.com/users/2").spawn(); + auto h3 = client.get("https://api.example.com/users/3").spawn(); + + // Wait for all responses + auto r1 = co_await h1; + auto r2 = co_await h2; + auto r3 = co_await h3; + + // All requests used the same TCP connection! +} +``` + +## Response Handling + +```cpp +coro::task handle_response() { + h2_client client(ctx); + + auto resp = co_await client.get("https://example.com/api/data"); + + if (!resp) { + std::cerr << "Request failed: " << strerror(errno) << std::endl; + co_return; + } + + // Check status + if (!resp->is_success()) { + std::cerr << "HTTP error: " << resp->status_code() << std::endl; + co_return; + } + + // Access headers + auto content_type = resp->header("Content-Type"); + auto cache_control = resp->header("Cache-Control"); + + // Get body + std::string body = resp->body(); + + // Or take ownership (avoids copy) + std::string body_moved = resp->take_body(); +} +``` + +## Error Handling + +```cpp +coro::task error_handling() { + h2_client client(ctx); + + auto resp = co_await client.get("https://example.com/"); + + if (!resp) { + switch (errno) { + case ECONNREFUSED: + std::cerr << "Connection refused" << std::endl; + break; + case ETIMEDOUT: + std::cerr << "Connection timeout" << std::endl; + break; + case ECONNRESET: + std::cerr << "Connection reset" << std::endl; + break; + default: + std::cerr << "Error: " << strerror(errno) << std::endl; + } + co_return; + } + + // Handle HTTP-level errors + if (resp->status_code() >= 400) { + std::cerr << "HTTP " << resp->status_code() << ": " + << resp->body() << std::endl; + } +} +``` + +## Cancellation + +```cpp +coro::task cancellable_request() { + h2_client client(ctx); + coro::cancel_source cancel_source; + + // Start cancellable request + auto request_task = client.get("https://slow-api.example.com/", + cancel_source.token()); + + // In another coroutine or after timeout + cancel_source.cancel(); + + auto resp = co_await request_task; + if (!resp && errno == ECANCELED) { + std::cout << "Request was cancelled" << std::endl; + } +} +``` + +## Connection Management + +The h2_client maintains a connection pool internally: + +```cpp +coro::task connection_lifecycle() { + h2_client client(ctx); + + // First request establishes connection + co_await client.get("https://api.example.com/ping"); + + // Subsequent requests reuse the same connection + for (int i = 0; i < 100; i++) { + co_await client.get("https://api.example.com/data/" + + std::to_string(i)); + } + + // Connection is automatically closed when client is destroyed +} +``` + +## HTTP/2 vs HTTP/1.1 + +| Feature | HTTP/2 | HTTP/1.1 | +|---------|--------|----------| +| Multiplexing | Yes | No (pipelining limited) | +| Header compression | HPACK | None | +| Binary protocol | Yes | Text-based | +| Server push | Supported | No | +| TLS required | Yes (in practice) | No | +| Connection reuse | Single connection | Connection pool | + +### When to Use HTTP/2 + +**Use HTTP/2 when:** +- Making many concurrent requests to the same host +- Bandwidth is limited (header compression helps) +- Low latency is critical +- Server supports HTTP/2 + +**Use HTTP/1.1 when:** +- Server doesn't support HTTP/2 +- Making single requests +- HTTP (non-TLS) is required +- Compatibility with older infrastructure + +## Debugging + +Enable debug logging to see HTTP/2 frame details: + +```cpp +// Set log level before creating client +elio::log::set_level(elio::log::level::debug); + +h2_client client(ctx); +auto resp = co_await client.get("https://example.com/"); + +// Output shows frame exchanges: +// [DEBUG] Sending SETTINGS frame +// [DEBUG] Received SETTINGS frame +// [DEBUG] Sending HEADERS frame (stream 1) +// [DEBUG] Received HEADERS frame (stream 1) +// [DEBUG] Received DATA frame (stream 1, 1234 bytes) +``` + +## Best Practices + +1. **Reuse clients**: Create one h2_client per host and reuse it +2. **Use concurrency**: Take advantage of multiplexing with parallel requests +3. **Handle errors**: Always check response validity +4. **Set timeouts**: Configure appropriate timeouts for your use case +5. **Verify certificates**: Only disable certificate verification for testing + +## See Also + +- [HTTP/1.1 Client](Networking.md#http-client) +- [TLS Configuration](TLS-Configuration.md) +- [WebSocket Guide](WebSocket-SSE.md) diff --git a/wiki/Home.md b/wiki/Home.md index 90ecde3..b89ac8b 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -57,9 +57,13 @@ int main() { - [[Getting Started]] - Installation and first steps - [[Core Concepts]] - Coroutines, tasks, and scheduler - [[Signal Handling]] - Safe signal handling with signalfd -- [[Networking]] - TCP, HTTP, and TLS +- [[Networking]] - TCP, HTTP/1.1, and connections +- [[HTTP2 Guide]] - HTTP/2 client usage and multiplexing +- [[TLS Configuration]] - TLS/SSL setup and certificate management +- [[WebSocket SSE]] - WebSocket and Server-Sent Events - [[RPC Framework]] - High-performance RPC with zero-copy serialization - [[Hash Functions]] - CRC32, SHA-1, and SHA-256 +- [[Performance Tuning]] - Optimization and benchmarking - [[Debugging]] - GDB/LLDB extensions and elio-pstack - [[Examples]] - Code examples and use cases - [[API Reference]] - Detailed API documentation diff --git a/wiki/Networking.md b/wiki/Networking.md index fc3b647..8f7ed44 100644 --- a/wiki/Networking.md +++ b/wiki/Networking.md @@ -24,21 +24,21 @@ coro::task handle_client(tcp_stream stream) { co_return; } -coro::task server(uint16_t port, runtime::scheduler& sched) { - auto& ctx = io::default_io_context(); - - auto listener = tcp_listener::bind(ipv4_address(port), ctx); +coro::task server(uint16_t port) { + auto* sched = runtime::scheduler::current(); + + auto listener = tcp_listener::bind(ipv4_address(port)); if (!listener) { ELIO_LOG_ERROR("Bind failed: {}", strerror(errno)); co_return; } - + while (true) { auto stream = co_await listener->accept(); if (!stream) continue; - + auto handler = handle_client(std::move(*stream)); - sched.spawn(handler.release()); + sched->spawn(handler.release()); } } ``` @@ -137,24 +137,24 @@ coro::task handle_client(uds_stream stream) { co_return; } -coro::task server(const unix_address& addr, runtime::scheduler& sched) { - auto& ctx = io::default_io_context(); - +coro::task server(const unix_address& addr) { + auto* sched = runtime::scheduler::current(); + uds_options opts; opts.unlink_on_bind = true; // Remove existing socket file - - auto listener = uds_listener::bind(addr, ctx, opts); + + auto listener = uds_listener::bind(addr, opts); if (!listener) { ELIO_LOG_ERROR("Bind failed: {}", strerror(errno)); co_return; } - + while (true) { auto stream = co_await listener->accept(); if (!stream) continue; - + auto handler = handle_client(std::move(*stream)); - sched.spawn(handler.release()); + sched->spawn(handler.release()); } } ``` @@ -234,10 +234,10 @@ HTTP support requires linking with `elio_http` and OpenSSL. using namespace elio::http; -coro::task fetch_url(io::io_context& ctx) { +coro::task fetch_url() { // Simple one-off request - auto result = co_await http::get(ctx, "https://httpbin.org/get"); - + auto result = co_await http::get("https://httpbin.org/get"); + if (result) { ELIO_LOG_INFO("Status: {}", result->status_code()); ELIO_LOG_INFO("Body: {}", result->body()); @@ -249,13 +249,13 @@ coro::task fetch_url(io::io_context& ctx) { ### HTTP Client with Configuration ```cpp -coro::task advanced_client(io::io_context& ctx) { +coro::task advanced_client() { client_config config; config.user_agent = "MyApp/1.0"; config.follow_redirects = true; config.max_redirects = 5; - - client c(ctx, config); + + client c(config); // GET request auto resp = co_await c.get("https://api.example.com/data"); @@ -308,14 +308,16 @@ coro::task handle_request(request& req, response& resp) { } coro::task run_server(uint16_t port) { - auto& ctx = io::default_io_context(); - - server_config config; - config.port = port; - config.handler = handle_request; - - server srv(ctx, config); - co_await srv.run(); + router r; + r.get("/", [](context& ctx) { + return response::ok("

Hello from Elio!

", "text/html"); + }); + r.get("/api/data", [](context& ctx) { + return response::ok(R"({"message": "Hello, World!"})", "application/json"); + }); + + server srv(r); + co_await srv.listen(net::ipv4_address(port)); } ``` @@ -336,13 +338,13 @@ HTTP/2 support requires linking with `elio_http2`, OpenSSL, and nghttp2 (fetched using namespace elio::http; -coro::task fetch_url(io::io_context& ctx) { +coro::task fetch_url_h2() { // Create HTTP/2 client - h2_client client(ctx); - + h2_client client; + // Simple GET request (must use HTTPS) auto result = co_await client.get("https://nghttp2.org/"); - + if (result) { ELIO_LOG_INFO("Status: {}", static_cast(result->get_status())); ELIO_LOG_INFO("Body: {}", result->body()); @@ -354,12 +356,12 @@ coro::task fetch_url(io::io_context& ctx) { ### HTTP/2 Client with Configuration ```cpp -coro::task advanced_h2_client(io::io_context& ctx) { +coro::task advanced_h2_client() { h2_client_config config; config.user_agent = "MyApp/1.0"; config.max_concurrent_streams = 100; - - h2_client client(ctx, config); + + h2_client client(config); // GET request auto resp = co_await client.get("https://api.example.com/data"); @@ -407,19 +409,17 @@ coro::task advanced_h2_client(io::io_context& ctx) { using namespace elio::tls; coro::task secure_connection() { - auto& ctx = io::default_io_context(); - // Create TLS context tls_context tls_ctx(tls_method::client); tls_ctx.use_default_verify_paths(); tls_ctx.set_verify_mode(true); - + // Connect TCP auto tcp = co_await tcp_connect(ipv4_address("example.com", 443)); if (!tcp) co_return; - + // Wrap with TLS - tls_stream stream(std::move(*tcp), tls_ctx, ctx); + tls_stream stream(std::move(*tcp), tls_ctx); stream.set_hostname("example.com"); // SNI // Perform handshake @@ -447,23 +447,21 @@ coro::task secure_connection() { ```cpp coro::task tls_server(uint16_t port) { - auto& ctx = io::default_io_context(); - // Create TLS context with certificate tls_context tls_ctx(tls_method::server); tls_ctx.use_certificate_file("server.crt"); tls_ctx.use_private_key_file("server.key"); - - auto listener = tcp_listener::bind(ipv4_address(port), ctx); + + auto listener = tcp_listener::bind(ipv4_address(port)); if (!listener) co_return; - + while (true) { auto tcp = co_await listener->accept(); if (!tcp) continue; - + // Wrap accepted connection with TLS - tls_stream stream(std::move(*tcp), tls_ctx, ctx); - + tls_stream stream(std::move(*tcp), tls_ctx); + auto hs = co_await stream.handshake(); if (hs) { // Handle secure connection @@ -477,8 +475,8 @@ coro::task tls_server(uint16_t port) { The HTTP client automatically manages connection pooling for keep-alive connections: ```cpp -coro::task pooled_requests(io::io_context& ctx) { - client c(ctx); +coro::task pooled_requests() { + client c; // These requests reuse connections when possible for (int i = 0; i < 10; ++i) { diff --git a/wiki/Performance-Tuning.md b/wiki/Performance-Tuning.md new file mode 100644 index 0000000..ca6561f --- /dev/null +++ b/wiki/Performance-Tuning.md @@ -0,0 +1,377 @@ +# Performance Tuning Guide + +This guide covers performance optimization techniques for Elio applications. + +## Overview + +Elio is designed for high performance through: +- Lock-free data structures (Chase-Lev deque) +- Work-stealing scheduler +- Efficient I/O backends (io_uring, epoll) +- Custom coroutine frame allocator +- Minimal synchronization overhead + +## Scheduler Tuning + +### Thread Count + +```cpp +#include + +// Default: matches hardware concurrency +scheduler sched; + +// Custom thread count +scheduler sched(8); // 8 worker threads + +// For I/O-bound workloads, consider more threads than cores +scheduler sched(std::thread::hardware_concurrency() * 2); + +// For CPU-bound workloads, match core count +scheduler sched(std::thread::hardware_concurrency()); +``` + +### Dynamic Thread Adjustment + +```cpp +// Enable dynamic thread pool sizing +sched.set_min_threads(2); +sched.set_max_threads(16); + +// Scheduler will add threads under load and remove idle threads +``` + +### Thread Affinity + +Pin coroutines to specific workers for cache locality: + +```cpp +#include + +coro::task cache_sensitive_work() { + // Pin to current worker + co_await coro::pin_to_current_worker(); + + // All subsequent work stays on this worker + process_data(); +} + +// Or set affinity explicitly +coro::set_affinity(handle, worker_id); +``` + +## I/O Backend Selection + +### io_uring vs epoll + +Elio auto-detects the best available backend: + +```cpp +#include + +// Auto-detect (prefers io_uring) +io::io_context ctx; + +// Force specific backend +io::io_context ctx(io::io_context::backend_type::io_uring); +io::io_context ctx(io::io_context::backend_type::epoll); + +// Check active backend +std::cout << "Backend: " << ctx.get_backend_name() << std::endl; +``` + +**io_uring advantages:** +- Batched syscalls (fewer context switches) +- Kernel-side I/O completion +- Better for high-throughput scenarios + +**epoll fallback:** +- Works on older kernels (pre-5.1) +- Lower memory overhead +- Adequate for moderate workloads + +### io_uring Kernel Requirements + +For best io_uring performance: +- Linux 5.1+: Basic io_uring +- Linux 5.6+: Full features +- Linux 5.11+: Multi-shot accept + +## Memory Management + +### Coroutine Frame Allocator + +Elio uses a thread-local pool allocator for coroutine frames: + +```cpp +// Configured in frame_allocator.hpp +static constexpr size_t MAX_FRAME_SIZE = 256; // Max pooled size +static constexpr size_t POOL_SIZE = 1024; // Pool capacity + +// Statistics (if enabled) +auto stats = coro::frame_allocator::get_stats(); +std::cout << "Allocations: " << stats.allocations << std::endl; +std::cout << "Pool hits: " << stats.pool_hits << std::endl; +``` + +### Avoiding Allocations + +Keep coroutine frames small for pool allocation: + +```cpp +// Bad: Large array in coroutine frame (can't use pool) +coro::task large_frame() { + char buffer[8192]; // Too large for pool + co_await read_data(buffer); +} + +// Good: Allocate separately +coro::task small_frame() { + auto buffer = std::make_unique(8192); + co_await read_data(buffer.get()); +} +``` + +## Synchronization Primitives + +### Mutex Performance + +Elio's mutex uses atomic fast-path for uncontended cases: + +```cpp +#include + +sync::mutex mtx; + +// Fast path: atomic CAS (~10ns) +// Slow path: suspend and queue (~100ns + context switch) + +coro::task critical_section() { + co_await mtx.lock(); + // ... critical section ... + mtx.unlock(); +} + +// Use try_lock to avoid blocking +if (mtx.try_lock()) { + // Got lock immediately + mtx.unlock(); +} else { + // Skip or retry later +} +``` + +### Reader-Writer Lock + +For read-heavy workloads: + +```cpp +sync::shared_mutex rw_mtx; + +// Multiple concurrent readers (atomic counter, no blocking) +coro::task reader() { + co_await rw_mtx.lock_shared(); + auto data = read_data(); + rw_mtx.unlock_shared(); +} + +// Exclusive writers +coro::task writer() { + co_await rw_mtx.lock(); + write_data(); + rw_mtx.unlock(); +} +``` + +### Channel Selection + +Choose appropriate channel type: + +```cpp +// Bounded channel: back-pressure, bounded memory +sync::channel ch(100); + +// Unbounded channel: faster but can grow indefinitely +sync::unbounded_channel uch; + +// SPSC queue: single producer/consumer (fastest) +runtime::spsc_queue spsc(1000); +``` + +## Network Performance + +### Connection Pooling + +HTTP client uses connection pooling by default: + +```cpp +http::client_config config; +config.max_connections_per_host = 10; // Pool size per host +config.pool_idle_timeout = std::chrono::seconds(60); + +http::client client(ctx, config); +``` + +### Buffer Sizes + +Tune read buffer sizes for your workload: + +```cpp +http::client_config config; +config.read_buffer_size = 16384; // 16KB (default: 8KB) + +// For large payloads +config.read_buffer_size = 65536; // 64KB +``` + +### TCP Settings + +Configure TCP options for performance: + +```cpp +// Enable TCP_NODELAY for latency-sensitive applications +net::tcp_stream stream = /* ... */; +stream.set_nodelay(true); + +// Adjust send/receive buffers +stream.set_send_buffer_size(65536); +stream.set_recv_buffer_size(65536); +``` + +## Profiling and Monitoring + +### Scheduler Statistics + +```cpp +// Get scheduler metrics +auto stats = sched.get_stats(); +std::cout << "Tasks spawned: " << stats.tasks_spawned << std::endl; +std::cout << "Tasks completed: " << stats.tasks_completed << std::endl; +std::cout << "Steals: " << stats.work_steals << std::endl; +std::cout << "Average queue depth: " << stats.avg_queue_depth << std::endl; +``` + +### Logging Overhead + +Debug logging has overhead; disable in production: + +```cpp +// Set at compile time +// cmake -DELIO_DEBUG=OFF .. + +// Or at runtime +elio::log::set_level(elio::log::level::warning); +``` + +### Coroutine Stack Tracing + +Use virtual stack for debugging without significant overhead: + +```cpp +// Enable in debug builds only +#ifdef ELIO_DEBUG + auto* frame = coro::current_frame(); + print_stack_trace(frame); +#endif +``` + +## Benchmarking Tips + +### Warm-up + +```cpp +// Warm up allocators and caches +for (int i = 0; i < 1000; i++) { + warmup_task().go(); +} +sched.sync(); + +// Now measure +auto start = std::chrono::steady_clock::now(); +// ... actual benchmark ... +auto end = std::chrono::steady_clock::now(); +``` + +### Avoid Measurement Overhead + +```cpp +// Bad: timing inside hot loop +for (int i = 0; i < 1000000; i++) { + auto start = now(); // Overhead! + do_work(); + auto end = now(); + record(end - start); +} + +// Good: time the whole batch +auto start = now(); +for (int i = 0; i < 1000000; i++) { + do_work(); +} +auto end = now(); +auto avg = (end - start) / 1000000; +``` + +### Use Release Builds + +Always benchmark with optimizations: + +```bash +cmake -DCMAKE_BUILD_TYPE=Release .. +cmake --build . +``` + +## Common Performance Issues + +### Problem: High Latency Spikes + +**Causes:** +- Work stealing delays +- GC pauses in other processes +- Kernel scheduling + +**Solutions:** +- Pin critical tasks to workers +- Use CPU affinity for scheduler threads +- Consider real-time scheduling + +### Problem: Low Throughput + +**Causes:** +- Lock contention +- Inefficient I/O batching +- Small buffer sizes + +**Solutions:** +- Profile lock contention +- Use io_uring for batching +- Increase buffer sizes + +### Problem: High Memory Usage + +**Causes:** +- Unbounded channels +- Large coroutine frames +- Connection pool growth + +**Solutions:** +- Use bounded channels +- Allocate large buffers separately +- Limit connection pool size + +## Quick Reference + +| Scenario | Recommendation | +|----------|----------------| +| I/O-bound | 2x core count threads | +| CPU-bound | 1x core count threads | +| Latency-critical | Pin to workers, io_uring | +| Throughput-critical | Large buffers, batching | +| Memory-constrained | Bounded channels, small pools | +| Read-heavy sync | Use shared_mutex | + +## See Also + +- [Core Concepts](Core-Concepts.md) +- [Debugging Guide](Debugging.md) +- [HTTP/2 Guide](HTTP2-Guide.md) diff --git a/wiki/Signal-Handling.md b/wiki/Signal-Handling.md index 3b0fece..8393d51 100644 --- a/wiki/Signal-Handling.md +++ b/wiki/Signal-Handling.md @@ -97,11 +97,8 @@ Async-friendly signalfd wrapper. // Create with automatic blocking signal_fd sigfd(sigs); -// Create with explicit I/O context -signal_fd sigfd(sigs, my_io_context); - // Don't auto-block (caller manages signal mask) -signal_fd sigfd(sigs, ctx, false); +signal_fd sigfd(sigs, false); // Check validity if (sigfd.valid()) { /* ... */ } @@ -189,7 +186,7 @@ int main() { ### 2. Handle Multiple Signal Types ```cpp -coro::task signal_router(scheduler& sched) { +coro::task signal_router() { signal_set sigs{SIGINT, SIGTERM, SIGUSR1, SIGUSR2, SIGHUP}; signal_fd sigfd(sigs); diff --git a/wiki/TLS-Configuration.md b/wiki/TLS-Configuration.md new file mode 100644 index 0000000..d1f0b47 --- /dev/null +++ b/wiki/TLS-Configuration.md @@ -0,0 +1,334 @@ +# TLS Configuration Guide + +Elio provides TLS/SSL support via OpenSSL. This guide covers TLS configuration for secure connections. + +## Overview + +TLS (Transport Layer Security) provides: +- **Encryption**: Data is encrypted in transit +- **Authentication**: Verify server (and optionally client) identity +- **Integrity**: Detect tampering with data + +## Requirements + +TLS support requires: +- OpenSSL library (1.1.1+ recommended) +- CMake option: `-DELIO_ENABLE_TLS=ON` + +## TLS Context + +The `tls_context` manages TLS configuration: + +### Client Mode + +```cpp +#include +#include + +using namespace elio::tls; + +// Create client TLS context +tls_context ctx(tls_mode::client); + +// Use system CA certificates for verification +ctx.use_default_verify_paths(); + +// Enable peer verification (recommended) +ctx.set_verify_mode(verify_mode::peer); +``` + +### Server Mode + +```cpp +// Create server TLS context +tls_context ctx(tls_mode::server); + +// Load server certificate and private key +ctx.use_certificate_file("/path/to/server.crt"); +ctx.use_private_key_file("/path/to/server.key"); + +// Optionally require client certificates +ctx.set_verify_mode(verify_mode::peer | verify_mode::fail_if_no_peer_cert); +ctx.load_verify_file("/path/to/ca.crt"); +``` + +## Certificate Verification + +### Verification Modes + +```cpp +enum class verify_mode { + none, // No verification (insecure!) + peer, // Verify peer certificate + fail_if_no_peer_cert, // Fail if peer has no certificate + client_once // Only verify client cert once +}; + +// Combine modes with bitwise OR +ctx.set_verify_mode(verify_mode::peer | verify_mode::fail_if_no_peer_cert); +``` + +### Custom Verification + +```cpp +// Load specific CA certificate +ctx.load_verify_file("/path/to/ca-bundle.crt"); + +// Load directory of CA certificates +ctx.load_verify_dir("/etc/ssl/certs/"); + +// Use system default CA store +ctx.use_default_verify_paths(); +``` + +## Client Certificates (Mutual TLS) + +For mutual TLS authentication: + +```cpp +tls_context ctx(tls_mode::client); + +// Load client certificate +ctx.use_certificate_file("/path/to/client.crt"); + +// Load client private key +ctx.use_private_key_file("/path/to/client.key"); + +// Optionally set password callback for encrypted keys +ctx.set_password_callback([](int max_len, int purpose) -> std::string { + return "my_secret_password"; +}); +``` + +## ALPN (Application-Layer Protocol Negotiation) + +ALPN is required for HTTP/2: + +```cpp +// For HTTP/2 client +std::vector protocols = {"h2", "http/1.1"}; +ctx.set_alpn_protocols(protocols); + +// After connection, check negotiated protocol +tls_stream stream = /* ... */; +std::string_view proto = stream.alpn_protocol(); +if (proto == "h2") { + // Use HTTP/2 +} else { + // Fall back to HTTP/1.1 +} +``` + +## Connecting with TLS + +### Using tls_connect + +```cpp +coro::task connect_example() { + tls_context ctx(tls_mode::client); + ctx.use_default_verify_paths(); + ctx.set_verify_mode(verify_mode::peer); + + // Connect with TLS + auto stream = co_await tls_connect(ctx, "example.com", 443); + if (!stream) { + std::cerr << "TLS connection failed: " << strerror(errno) << std::endl; + co_return; + } + + // Use the stream + co_await stream->write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"); + + char buffer[4096]; + auto result = co_await stream->read(buffer, sizeof(buffer)); + if (result.success()) { + std::cout.write(buffer, result.bytes_transferred()); + } + + co_await stream->shutdown(); +} +``` + +### TLS Listener (Server) + +```cpp +coro::task server_example() { + tls_context ctx(tls_mode::server); + ctx.use_certificate_file("/path/to/server.crt"); + ctx.use_private_key_file("/path/to/server.key"); + + auto listener = tls_listener::bind( + net::ipv4_address("0.0.0.0", 8443), + ctx + ); + + if (!listener) { + std::cerr << "Failed to bind" << std::endl; + co_return; + } + + while (true) { + auto client = co_await listener->accept(); + if (client) { + handle_client(std::move(*client)).go(); + } + } +} +``` + +## Protocol Configuration + +### Minimum TLS Version + +```cpp +// Require TLS 1.2 or higher +ctx.set_min_protocol_version(TLS1_2_VERSION); + +// Require TLS 1.3 only +ctx.set_min_protocol_version(TLS1_3_VERSION); +``` + +### Cipher Suites + +```cpp +// Set preferred cipher suites (TLS 1.2) +ctx.set_cipher_list("ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20"); + +// Set cipher suites for TLS 1.3 +ctx.set_ciphersuites("TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"); +``` + +## Error Handling + +```cpp +coro::task handle_tls_errors() { + tls_context ctx(tls_mode::client); + ctx.use_default_verify_paths(); + ctx.set_verify_mode(verify_mode::peer); + + auto stream = co_await tls_connect(ctx, "expired.badssl.com", 443); + + if (!stream) { + // Check errno for specific error + // Common TLS-related errors: + // - Certificate verification failed + // - Connection refused + // - Handshake timeout + std::cerr << "TLS error: " << strerror(errno) << std::endl; + co_return; + } + + // Check certificate verification result + long verify_result = stream->verify_result(); + if (verify_result != X509_V_OK) { + std::cerr << "Certificate error: " + << X509_verify_cert_error_string(verify_result) + << std::endl; + } +} +``` + +## TLS Stream Properties + +```cpp +coro::task inspect_connection() { + auto stream = co_await tls_connect(ctx, "example.com", 443); + if (!stream) co_return; + + // TLS version + std::cout << "TLS Version: " << stream->version() << std::endl; + + // Cipher suite + std::cout << "Cipher: " << stream->cipher() << std::endl; + + // ALPN protocol + std::cout << "ALPN: " << stream->alpn_protocol() << std::endl; + + // Peer certificate + X509* cert = stream->peer_certificate(); + if (cert) { + // Extract certificate info + X509_NAME* subject = X509_get_subject_name(cert); + char buf[256]; + X509_NAME_oneline(subject, buf, sizeof(buf)); + std::cout << "Subject: " << buf << std::endl; + X509_free(cert); + } +} +``` + +## Integration with HTTP Clients + +### HTTP Client TLS Configuration + +```cpp +http::client_config config; +config.verify_certificate = true; // Enable verification + +http::client client(ctx, config); + +// Access and customize TLS context +auto& tls_ctx = client.tls_context(); +tls_ctx.load_verify_file("/path/to/custom-ca.crt"); + +auto resp = co_await client.get("https://example.com/"); +``` + +### HTTP/2 Client TLS Configuration + +```cpp +h2_client client(ctx); + +// Configure TLS for HTTP/2 +auto& tls_ctx = client.tls_context(); + +// HTTP/2 requires ALPN +// (automatically configured by h2_client) + +// Get negotiated protocol after connection +// h2_client uses HTTP/2 if server supports it +``` + +## Best Practices + +1. **Always verify certificates in production** + - Only disable for testing/development + - Use `verify_mode::peer` at minimum + +2. **Use modern TLS versions** + - Require TLS 1.2+ for security + - Consider TLS 1.3 for best security and performance + +3. **Configure strong cipher suites** + - Prefer ECDHE for forward secrecy + - Use AESGCM or ChaCha20 for encryption + +4. **Handle errors gracefully** + - Check return values + - Log certificate verification failures + +5. **Keep certificates updated** + - Monitor certificate expiration + - Use automated renewal (Let's Encrypt, etc.) + +6. **Protect private keys** + - Use file permissions (chmod 600) + - Consider hardware security modules for high-security applications + +## Testing TLS + +Use badssl.com for testing various TLS scenarios: + +```cpp +// Test certificate verification +co_await tls_connect(ctx, "expired.badssl.com", 443); // Should fail +co_await tls_connect(ctx, "wrong.host.badssl.com", 443); // Should fail +co_await tls_connect(ctx, "self-signed.badssl.com", 443); // Should fail +co_await tls_connect(ctx, "sha256.badssl.com", 443); // Should succeed +``` + +## See Also + +- [HTTP/2 Guide](HTTP2-Guide.md) +- [Networking Guide](Networking.md) +- [Performance Tuning](Performance-Tuning.md) diff --git a/wiki/WebSocket-SSE.md b/wiki/WebSocket-SSE.md index e0d59a9..837d7bc 100644 --- a/wiki/WebSocket-SSE.md +++ b/wiki/WebSocket-SSE.md @@ -51,8 +51,7 @@ int main() { runtime::scheduler sched(4); sched.start(); - auto task = srv.listen(net::ipv4_address(8080), - io::default_io_context(), sched); + auto task = srv.listen(net::ipv4_address(8080)); sched.spawn(task.release()); // Run until stopped... @@ -71,13 +70,11 @@ using namespace elio; using namespace elio::http::websocket; coro::task connect_example() { - auto& ctx = io::default_io_context(); - // Create client client_config config; config.subprotocols = {"chat", "json"}; // Optional subprotocols - - ws_client client(ctx, config); + + ws_client client(config); // Connect to server if (!co_await client.connect("ws://localhost:8080/ws")) {