Skip to content
Draft
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
974 changes: 974 additions & 0 deletions frameworks/proxygen-coro/ArenaCoroServer.cpp

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions frameworks/proxygen-coro/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.20)

project(httparena-proxygen-coro LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Proxygen's exported Fizz package calls find_dependency(Sodium). The official
# builder image keeps that upstream find module with the Proxygen source tree.
list(APPEND CMAKE_MODULE_PATH "/proxygen/build/fbcode_builder/CMake")

find_package(c-ares CONFIG REQUIRED)
add_library(cares ALIAS c-ares::cares)
find_package(proxygen CONFIG REQUIRED)

add_executable(proxygen-arena-coro ArenaCoroServer.cpp)
target_compile_options(proxygen-arena-coro PRIVATE -Wall -Wextra -Wpedantic)
target_link_libraries(
proxygen-arena-coro
PRIVATE
proxygen::proxygen
proxygen::proxygen_coro
proxygen::proxygen_coro_server
proxygen::proxygen_http_coro_filters_compression_filter_factory
Folly::folly_init_init
Folly::folly_portability_gflags
)
32 changes: 32 additions & 0 deletions frameworks/proxygen-coro/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM ghcr.io/facebook/proxygen/base:latest AS build

WORKDIR /arena
COPY CMakeLists.txt ArenaCoroServer.cpp ./
RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
&& cmake --build build --parallel "$(nproc)" \
&& strip build/proxygen-arena-coro

RUN set -eux; \
ldd build/proxygen-arena-coro \
| awk '/=> \// { print $3 } /^\// { print $1 }' | sort -u > /tmp/runtime-libs.txt; \
tar -chf /tmp/runtime-libs.tar --files-from=/tmp/runtime-libs.txt

FROM ubuntu:24.04@sha256:019e8eb29a85e74d64925745884f2ec79aa27e3feab36353d24656f4d6b89467

ENV LD_LIBRARY_PATH=/opt/proxygen/lib

COPY --from=build /tmp/runtime-libs.tar /tmp/runtime-libs.tar
RUN tar -xf /tmp/runtime-libs.tar -C / \
&& rm /tmp/runtime-libs.tar

COPY --from=build /arena/build/proxygen-arena-coro /usr/local/bin/proxygen-arena-coro
COPY entrypoint.sh /usr/local/bin/proxygen-coro-entrypoint
RUN chmod +x /usr/local/bin/proxygen-coro-entrypoint \
&& groupadd --system --gid 10001 httparena \
&& useradd --system --uid 10001 --gid httparena --no-create-home \
--home-dir /nonexistent --shell /usr/sbin/nologin httparena

EXPOSE 8080/tcp 8081/tcp 8082/tcp 8443/tcp 8443/udp

USER httparena
ENTRYPOINT ["/usr/local/bin/proxygen-coro-entrypoint"]
52 changes: 52 additions & 0 deletions frameworks/proxygen-coro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# proxygen-coro

This engine exercises Proxygen's native coroutine server stack rather than the
callback `RequestHandler` / `HTTPTransactionHandler` APIs used by the regular
`proxygen` entry.

`ArenaCoroServer.cpp` implements `proxygen::coro::HTTPHandler`, consumes
requests through `HTTPSourceHolder`, and returns `HTTPFixedSource` responses.
Uploads are counted while asynchronously draining BODY events. WebSockets use
a long-lived custom `HTTPSource`: its response calls
`setEgressWebsocketUpgrade()`, then parses and emits RFC 6455 frames over the
raw upgraded BODY event stream. Response compression is provided by the coro
`ServerCompressionFilterFactory`.

## Listener layout

One process owns all five listeners:

- `8080/tcp`: HTTP/1.1 and WebSockets
- `8081/tcp`: HTTP/1.1 over TLS, ALPN `http/1.1`
- `8082/tcp`: prior-knowledge h2c
- `8443/tcp`: HTTP/2 over TLS, ALPN `h2`
- `8443/udp`: HTTP/3 over QUIC, ALPN `h3`

The four TCP listeners are acceptors on one coro `HTTPServer`, so they share a
single affinity-aware I/O pool. Proxygen's coro API selects either TCP or QUIC
per server, so HTTP/3 uses a second in-process pool; whichever transport is not
being benchmarked remains idle. Override the available-CPU default with
`PROXYGEN_CORO_THREADS`.

HTTP/2 advertises 1024 concurrent streams with a 1 MiB stream window and a
10 MiB connection window. HTTP/3 mirrors Proxygen's coroutine benchmark
settings: GSO batches of 48 packets, continuous-memory writes, a large
congestion window, and a 48-packet connection write limit.

## Upstream image and build

The self-contained Docker build tracks the official
`ghcr.io/facebook/proxygen/base:latest` builder image. The runtime stage copies
only the compiled binary and its dynamically linked libraries into a pinned
Ubuntu 24.04 image, then runs as the non-root `httparena` user (UID/GID 10001).

From the repository root:

```bash
./scripts/validate.sh proxygen-coro
./scripts/run.sh proxygen-coro
```

The implementation follows the upstream coroutine echo server and the
`H12DownstreamSessionTest.WebSocketUpgrade` test, which documents upgraded
raw bytes flowing through coroutine BODY events.
13 changes: 13 additions & 0 deletions frameworks/proxygen-coro/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail

exec /usr/local/bin/proxygen-arena-coro \
--ip=:: \
--http_port=8080 \
--tls_port=8081 \
--h2c_port=8082 \
--h2_port=8443 \
--h3_port=8443 \
--cert=/certs/server.crt \
--key=/certs/server.key \
--threads="${PROXYGEN_CORO_THREADS:-0}"
30 changes: 30 additions & 0 deletions frameworks/proxygen-coro/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"display_name": "proxygen-coro",
"language": "C++",
"type": "engine",
"engine": "proxygen",
"description": "Meta's Proxygen native coroutine HTTPServer and HTTPSource APIs across HTTP/1.1, HTTP/1.1 TLS, h2c, HTTP/2 TLS, HTTP/3 QUIC, and RFC 6455 WebSockets.",
"repo": "https://github.com/facebook/proxygen",
"enabled": true,
"tests": [
"baseline",
"json",
"json-comp",
"json-tls",
"upload",
"static",
"static-tls",
"pipelined",
"limited-conn",
"baseline-h2",
"baseline-h2c",
"json-h2c",
"static-h2",
"baseline-h3",
"static-h3",
"echo-ws",
"echo-ws-pipeline",
"echo-ws-limited"
],
"maintainers": []
}
214 changes: 214 additions & 0 deletions frameworks/proxygen/ArenaCommon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
#pragma once

#include <array>
#include <cctype>
#include <charconv>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

#include <folly/base64.h>
#include <folly/dynamic.h>
#include <folly/json.h>

namespace httparena {

inline constexpr uint64_t kMaxWebSocketMessage = 16ULL * 1024 * 1024;
inline constexpr std::string_view kJsonPrefix = "/json/";
inline constexpr std::string_view kStaticPrefix = "/static/";
inline constexpr std::string_view kStaticRoot = "/data/static/";

inline bool parseInteger(std::string_view input, int64_t &value) {
while (!input.empty() &&
std::isspace(static_cast<unsigned char>(input.front()))) {
input.remove_prefix(1);
}
while (!input.empty() &&
std::isspace(static_cast<unsigned char>(input.back()))) {
input.remove_suffix(1);
}
if (input.empty()) {
return false;
}
const auto result =
std::from_chars(input.data(), input.data() + input.size(), value);
return result.ec == std::errc() && result.ptr == input.data() + input.size();
}

inline bool checkedAdd(int64_t lhs, int64_t rhs, int64_t &result) {
#if defined(__GNUC__) || defined(__clang__)
return !__builtin_add_overflow(lhs, rhs, &result);
#else
if ((rhs > 0 && lhs > std::numeric_limits<int64_t>::max() - rhs) ||
(rhs < 0 && lhs < std::numeric_limits<int64_t>::min() - rhs)) {
return false;
}
result = lhs + rhs;
return true;
#endif
}

inline bool checkedMultiply(int64_t lhs, int64_t rhs, int64_t &result) {
#if defined(__GNUC__) || defined(__clang__)
return !__builtin_mul_overflow(lhs, rhs, &result);
#else
if (lhs > 0) {
if ((rhs > 0 && lhs > std::numeric_limits<int64_t>::max() / rhs) ||
(rhs < 0 && rhs < std::numeric_limits<int64_t>::min() / lhs)) {
return false;
}
} else if (lhs < 0) {
if ((rhs > 0 && lhs < std::numeric_limits<int64_t>::min() / rhs) ||
(rhs < 0 && rhs < std::numeric_limits<int64_t>::max() / lhs)) {
return false;
}
}
result = lhs * rhs;
return true;
#endif
}

inline std::string contentType(std::string_view name) {
const auto endsWith = [name](std::string_view suffix) {
return name.size() >= suffix.size() &&
name.substr(name.size() - suffix.size()) == suffix;
};
if (endsWith(".css")) {
return "text/css";
}
if (endsWith(".js")) {
return "application/javascript";
}
if (endsWith(".html")) {
return "text/html";
}
if (endsWith(".json")) {
return "application/json";
}
if (endsWith(".svg")) {
return "image/svg+xml";
}
if (endsWith(".webp")) {
return "image/webp";
}
if (endsWith(".woff2")) {
return "font/woff2";
}
return "application/octet-stream";
}

inline std::shared_ptr<const folly::dynamic> loadDataset() {
std::ifstream input("/data/dataset.json", std::ios::binary);
if (!input) {
throw std::runtime_error("cannot open /data/dataset.json");
}
std::string contents((std::istreambuf_iterator<char>(input)),
std::istreambuf_iterator<char>());
auto dataset = folly::parseJson(contents);
if (!dataset.isArray() || dataset.size() < 50) {
throw std::runtime_error("/data/dataset.json must contain 50 items");
}
return std::make_shared<const folly::dynamic>(std::move(dataset));
}

inline bool validWebSocketKey(std::string_view key) noexcept {
if (key.size() != 24) {
return false;
}
std::array<char, 18> decoded{};
const auto result = folly::base64Decode(key, decoded.data());
return result.is_success && result.o == decoded.data() + 16;
}

inline bool validUtf8(const uint8_t *data, size_t size) noexcept {
const auto continuation = [](uint8_t byte) {
return byte >= 0x80 && byte <= 0xbf;
};

size_t index = 0;
while (index < size) {
const uint8_t first = data[index];
if (first <= 0x7f) {
++index;
continue;
}
if (first >= 0xc2 && first <= 0xdf) {
if (index + 1 >= size || !continuation(data[index + 1])) {
return false;
}
index += 2;
continue;
}
if (first == 0xe0) {
if (index + 2 >= size || data[index + 1] < 0xa0 ||
data[index + 1] > 0xbf || !continuation(data[index + 2])) {
return false;
}
index += 3;
continue;
}
if ((first >= 0xe1 && first <= 0xec) || (first >= 0xee && first <= 0xef)) {
if (index + 2 >= size || !continuation(data[index + 1]) ||
!continuation(data[index + 2])) {
return false;
}
index += 3;
continue;
}
if (first == 0xed) {
if (index + 2 >= size || data[index + 1] < 0x80 ||
data[index + 1] > 0x9f || !continuation(data[index + 2])) {
return false;
}
index += 3;
continue;
}
if (first == 0xf0) {
if (index + 3 >= size || data[index + 1] < 0x90 ||
data[index + 1] > 0xbf || !continuation(data[index + 2]) ||
!continuation(data[index + 3])) {
return false;
}
index += 4;
continue;
}
if (first >= 0xf1 && first <= 0xf3) {
if (index + 3 >= size || !continuation(data[index + 1]) ||
!continuation(data[index + 2]) || !continuation(data[index + 3])) {
return false;
}
index += 4;
continue;
}
if (first == 0xf4) {
if (index + 3 >= size || data[index + 1] < 0x80 ||
data[index + 1] > 0x8f || !continuation(data[index + 2]) ||
!continuation(data[index + 3])) {
return false;
}
index += 4;
continue;
}
return false;
}
return true;
}

inline bool validUtf8(const std::vector<uint8_t> &data) noexcept {
return validUtf8(data.data(), data.size());
}

inline bool validWebSocketCloseCode(uint16_t code) noexcept {
const bool definedProtocolCode = code >= 1000 && code <= 1014 &&
code != 1004 && code != 1005 && code != 1006;
const bool applicationCode = code >= 3000 && code <= 4999;
return definedProtocolCode || applicationCode;
}

} // namespace httparena
Loading