Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ benchmark
# Temporary files
*.tmp
.DS_Store
.qoder/
.clangd
__pycache__
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ install(PROGRAMS
)
install(FILES
tools/elio-gdb.py
tools/elio_lldb.py
tools/elio-lldb.py
DESTINATION share/elio
)
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ elio::
tools/ // Debugging tools
├── elio-pstack // pstack-like CLI tool
├── elio-gdb.py // GDB Python extension
└── elio-lldb.py // LLDB Python extension
├── elio_lldb.py // LLDB import entrypoint
└── elio-lldb.py // LLDB implementation script
```

### Virtual Stack
Expand Down Expand Up @@ -269,7 +270,7 @@ gdb -ex 'source tools/elio-gdb.py' ./myapp
(gdb) elio bt 42 # Show backtrace for vthread #42

# LLDB extension
lldb -o 'command script import tools/elio-lldb.py' ./myapp
lldb -o 'command script import tools/elio_lldb.py' ./myapp
(lldb) elio list
(lldb) elio bt
```
Expand Down
3 changes: 2 additions & 1 deletion examples/debug_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/// coroutines that run concurrently and can be inspected using:
/// - elio-pstack (command line)
/// - GDB with elio-gdb.py
/// - LLDB with elio-lldb.py
/// - LLDB with elio_lldb.py (entrypoint wrapper)
///
/// Usage:
/// ./debug_test # Run normally
Expand Down Expand Up @@ -135,6 +135,7 @@ coro::task<int> async_main(int argc, char* argv[]) {
std::cout << "Paused for debugger. Use one of:" << std::endl;
std::cout << " elio-pstack " << getpid() << std::endl;
std::cout << " gdb -p " << getpid() << " -ex 'source tools/elio-gdb.py' -ex 'elio bt'" << std::endl;
std::cout << " lldb -p " << getpid() << " -o 'command script import tools/elio_lldb.py' -o 'elio bt'" << std::endl;
std::cout << std::endl;
std::cout << "Press Ctrl+C to exit." << std::endl;
std::cout << std::endl;
Expand Down
26 changes: 14 additions & 12 deletions examples/rpc_server_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ using Echo = ELIO_RPC_METHOD(5, EchoRequest, EchoResponse);
// In-memory user store
class UserStore {
public:
struct UserListSnapshot {
std::vector<User> users;
int32_t total_count;
};

std::optional<User> get_user(int32_t id) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = users_.find(id);
Expand All @@ -135,22 +140,18 @@ class UserStore {
return id;
}

std::vector<User> list_users(int32_t offset, int32_t limit) {
UserListSnapshot list_users_snapshot(int32_t offset, int32_t limit) {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<User> result;
UserListSnapshot snapshot;
snapshot.total_count = static_cast<int32_t>(users_.size());
int32_t count = 0;
for (const auto& [id, user] : users_) {
if (count >= offset && result.size() < static_cast<size_t>(limit)) {
result.push_back(user);
if (count >= offset && snapshot.users.size() < static_cast<size_t>(limit)) {
snapshot.users.push_back(user);
}
++count;
}
return result;
}

int32_t total_count() {
std::lock_guard<std::mutex> lock(mutex_);
return static_cast<int32_t>(users_.size());
return snapshot;
}

private:
Expand Down Expand Up @@ -235,8 +236,9 @@ task<void> server_main(uint16_t port, [[maybe_unused]] scheduler& sched) {
server.register_method<ListUsers>([](const ListUsersRequest& req)
-> task<ListUsersResponse> {
ListUsersResponse resp;
resp.users = g_user_store.list_users(req.offset, req.limit);
resp.total_count = g_user_store.total_count();
auto snapshot = g_user_store.list_users_snapshot(req.offset, req.limit);
resp.users = std::move(snapshot.users);
resp.total_count = snapshot.total_count;
co_return resp;
});

Expand Down
21 changes: 16 additions & 5 deletions examples/tcp_echo_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ using namespace elio::net;
/// Client coroutine - connects, sends messages, receives responses
task<int> client_main(std::string_view host, uint16_t port) {
ELIO_LOG_INFO("Connecting to {}:{}...", host, port);

// Connect to server
auto stream_result = co_await tcp_connect(host, port);

auto resolved = co_await resolve_hostname(host, port);
if (!resolved) {
ELIO_LOG_ERROR("Resolve failed: {}", strerror(errno));
co_return 1;
}

auto stream_result = co_await tcp_connect(*resolved);

if (!stream_result) {
ELIO_LOG_ERROR("Connection failed: {}", strerror(errno));
Expand Down Expand Up @@ -79,8 +84,14 @@ task<int> client_main(std::string_view host, uint16_t port) {
/// Non-interactive benchmark mode
task<int> benchmark_main(std::string_view host, uint16_t port, int iterations) {
ELIO_LOG_INFO("Connecting to {}:{} for benchmark...", host, port);

auto stream_result = co_await tcp_connect(host, port);

auto resolved = co_await resolve_hostname(host, port);
if (!resolved) {
ELIO_LOG_ERROR("Resolve failed: {}", strerror(errno));
co_return 1;
}

auto stream_result = co_await tcp_connect(*resolved);
if (!stream_result) {
ELIO_LOG_ERROR("Connection failed: {}", strerror(errno));
co_return 1;
Expand Down
2 changes: 1 addition & 1 deletion include/elio/debug.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/// elio list # List all vthreads
/// elio workers # Show worker information
///
/// In LLDB: command script import /path/to/elio-lldb.py
/// In LLDB: command script import /path/to/elio_lldb.py
/// elio bt # Show all vthread backtraces
///
/// Command line:
Expand Down
1 change: 1 addition & 0 deletions include/elio/elio.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

// Networking
#include "net/tcp.hpp"
#include "net/resolve.hpp"
#include "net/uds.hpp"

// Timers
Expand Down
75 changes: 64 additions & 11 deletions include/elio/http/client_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,39 @@
/// - Connection utility functions

#include <elio/net/stream.hpp>
#include <elio/net/resolve.hpp>
#include <elio/tls/tls_context.hpp>
#include <elio/io/io_context.hpp>
#include <elio/coro/task.hpp>
#include <elio/log/macros.hpp>

#include <string>
#include <chrono>
#include <mutex>
#include <unordered_map>

namespace elio::http {

namespace detail {

inline size_t next_rotation_offset(const std::string& host, uint16_t port, size_t count) {
if (count == 0) {
return 0;
}

static std::mutex mutex;
static std::unordered_map<std::string, size_t> state;

std::lock_guard<std::mutex> lock(mutex);
std::string key = host + ":" + std::to_string(port);
size_t& cursor = state[key];
size_t offset = cursor % count;
cursor = (cursor + 1) % count;
return offset;
}

} // namespace detail

/// Base configuration shared by all HTTP-based clients
/// Can be embedded in more specific configuration structures
struct base_client_config {
Expand All @@ -27,6 +50,8 @@ struct base_client_config {
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
net::resolve_options resolve_options = net::default_cached_resolve_options(); ///< DNS resolve/cache behavior
bool rotate_resolved_addresses = true; ///< Rotate start index across resolved addresses
};

/// Initialize a TLS context for client use with default settings
Expand All @@ -49,28 +74,56 @@ inline void init_client_tls_context(tls::tls_context& ctx, bool verify_certifica
/// @return Connected stream or std::nullopt on error
inline coro::task<std::optional<net::stream>>
client_connect(std::string_view host, uint16_t port, bool secure,
tls::tls_context* tls_ctx) {
tls::tls_context* tls_ctx,
net::resolve_options resolve_opts = net::default_cached_resolve_options(),
bool rotate_resolved_addresses = true) {

auto addresses = co_await net::resolve_all(host, port, resolve_opts);
if (addresses.empty()) {
ELIO_LOG_ERROR("Failed to resolve {}:{}: {}", host, port, strerror(errno));
co_return std::nullopt;
}

size_t offset = rotate_resolved_addresses
? detail::next_rotation_offset(std::string(host), port, addresses.size())
: 0;

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;
for (size_t i = 0; i < addresses.size(); ++i) {
const auto& addr = addresses[(offset + i) % addresses.size()];
auto tcp = co_await net::tcp_connect(addr);
if (!tcp) {
continue;
}

tls::tls_stream tls_stream(std::move(*tcp), *tls_ctx);
tls_stream.set_hostname(host);
auto hs = co_await tls_stream.handshake();
if (!hs) {
continue;
}

co_return net::stream(std::move(tls_stream));
}

co_return net::stream(std::move(*result));
ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", host, port, strerror(errno));
co_return std::nullopt;
} 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;
for (size_t i = 0; i < addresses.size(); ++i) {
const auto& addr = addresses[(offset + i) % addresses.size()];
auto result = co_await net::tcp_connect(addr);
if (result) {
co_return net::stream(std::move(*result));
}
}

co_return net::stream(std::move(*result));
ELIO_LOG_ERROR("Failed to connect to {}:{}: {}", host, port, strerror(errno));
co_return std::nullopt;
}
}

Expand Down
86 changes: 52 additions & 34 deletions include/elio/http/http2_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <elio/http/http_message.hpp>
#include <elio/http/http2_session.hpp>
#include <elio/net/tcp.hpp>
#include <elio/net/resolve.hpp>
#include <elio/tls/tls_context.hpp>
#include <elio/tls/tls_stream.hpp>
#include <elio/io/io_context.hpp>
Expand All @@ -16,6 +17,7 @@
#include <optional>
#include <unordered_map>
#include <chrono>
#include <mutex>

namespace elio::http {

Expand All @@ -27,6 +29,8 @@ struct h2_client_config {
uint32_t initial_window_size = 65535; ///< Initial flow control window size
std::string user_agent = "elio-http2/1.0"; ///< User-Agent header
bool enable_push = false; ///< Enable server push (rarely needed)
net::resolve_options resolve_options = net::default_cached_resolve_options(); ///< DNS resolve/cache behavior
bool rotate_resolved_addresses = true; ///< Rotate start index across resolved addresses
};

/// HTTP/2 connection wrapper
Expand Down Expand Up @@ -197,44 +201,58 @@ class h2_client {
co_return std::move(conn);
}

// Create new HTTP/2 connection
// First establish TCP connection
auto tcp_result = co_await net::tcp_connect(host, port);
if (!tcp_result) {
ELIO_LOG_ERROR("Failed to connect to {}:{}", host, port);
auto addresses = co_await net::resolve_all(host, port, config_.resolve_options);
if (addresses.empty()) {
ELIO_LOG_ERROR("Failed to resolve {}:{}", host, port);
co_return std::nullopt;
}

// Create TLS stream with ALPN
tls::tls_stream tls_stream(std::move(*tcp_result), tls_ctx_);
tls_stream.set_hostname(host);

// Perform TLS handshake
auto hs_result = co_await tls_stream.handshake();
if (!hs_result) {
ELIO_LOG_ERROR("TLS handshake failed for {}:{}", host, port);
co_return std::nullopt;
}

// Verify ALPN negotiated h2
auto alpn = tls_stream.alpn_protocol();
if (alpn != "h2") {
ELIO_LOG_ERROR("Server does not support HTTP/2 (ALPN: {})",
alpn.empty() ? "(none)" : std::string(alpn));
co_return std::nullopt;

size_t offset = 0;
if (config_.rotate_resolved_addresses) {
static std::mutex rotation_mutex;
static std::unordered_map<std::string, size_t> rotation_cursor;
std::lock_guard<std::mutex> lock(rotation_mutex);
size_t& cursor = rotation_cursor[key];
offset = cursor % addresses.size();
cursor = (cursor + 1) % addresses.size();
}

ELIO_LOG_DEBUG("HTTP/2 connection established to {}:{}", host, port);

h2_connection conn(std::move(tls_stream));

// Process initial frames (settings exchange)
if (!co_await conn.session()->process()) {
ELIO_LOG_ERROR("HTTP/2 session initialization failed");
co_return std::nullopt;

for (size_t i = 0; i < addresses.size(); ++i) {
const auto& addr = addresses[(offset + i) % addresses.size()];

auto tcp_result = co_await net::tcp_connect(addr);
if (!tcp_result) {
continue;
}

tls::tls_stream tls_stream(std::move(*tcp_result), tls_ctx_);
tls_stream.set_hostname(host);

auto hs_result = co_await tls_stream.handshake();
if (!hs_result) {
continue;
}

auto alpn = tls_stream.alpn_protocol();
if (alpn != "h2") {
ELIO_LOG_ERROR("Server does not support HTTP/2 (ALPN: {})",
alpn.empty() ? "(none)" : std::string(alpn));
continue;
}

ELIO_LOG_DEBUG("HTTP/2 connection established to {}:{}", host, port);

h2_connection conn(std::move(tls_stream));
if (!co_await conn.session()->process()) {
ELIO_LOG_ERROR("HTTP/2 session initialization failed");
continue;
}

co_return std::move(conn);
}

co_return std::move(conn);

ELIO_LOG_ERROR("Failed to connect to any resolved address for {}:{}", host, port);
co_return std::nullopt;
}

/// Return a connection to the pool
Expand Down
Loading
Loading