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
125 changes: 122 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.20)
cmake_minimum_required(VERSION 3.25)

project(o_rly VERSION 0.1.0 LANGUAGES CXX)

Expand Down Expand Up @@ -26,8 +26,49 @@ list(APPEND CMAKE_MODULE_PATH
"${PROJECT_SOURCE_DIR}/deps/moxygen/build/fbcode_builder/CMake"
)

# Folly's config calls FindBoost which was removed in CMake 3.30 (CMP0167).
# Folly itself works fine with Boost's own config-mode package; silence the
# policy warning until upstream fixes their config files.
cmake_policy(SET CMP0167 OLD)

find_package(moxygen REQUIRED)

# --- yaml-cpp (FetchContent) ---

include(FetchContent)

set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(YAML_CPP_FORMAT_SOURCE OFF CACHE BOOL "" FORCE)

# yaml-cpp 0.8.0 uses cmake_minimum_required(VERSION 3.0.2) which triggers a
# deprecation warning on CMake ≥ 3.27. Suppress it for this dependency only.
set(_orly_save_warn_deprecated "${CMAKE_WARN_DEPRECATED}")
set(CMAKE_WARN_DEPRECATED OFF CACHE BOOL "" FORCE)

FetchContent_Declare(yaml-cpp
GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git
GIT_TAG 0.8.0
SYSTEM
)
FetchContent_MakeAvailable(yaml-cpp)

set(CMAKE_WARN_DEPRECATED "${_orly_save_warn_deprecated}" CACHE BOOL "" FORCE)

# --- reflect-cpp (FetchContent, needs yaml-cpp available) ---

set(REFLECTCPP_YAML ON CACHE BOOL "" FORCE)
set(REFLECTCPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)

FetchContent_Declare(reflectcpp
GIT_REPOSITORY https://github.com/getml/reflect-cpp.git
GIT_TAG v0.18.0
SYSTEM
)
FetchContent_MakeAvailable(reflectcpp)

# --- Core library ---

add_library(o_rly_core STATIC
src/ORelay.cpp
src/ORelayServer.cpp
Expand All @@ -47,15 +88,59 @@ target_link_libraries(o_rly_core PUBLIC

target_compile_options(o_rly_core PRIVATE -Wall -Wextra -Wpedantic)

# --- Config types (resolved Config structs, header-only, no rfl dependency) ---
# Library consumers that only need the Config API link this.

add_library(o_rly_config INTERFACE)

target_include_directories(o_rly_config
INTERFACE
${PROJECT_SOURCE_DIR}/include
)

target_link_libraries(o_rly_config INTERFACE
Folly::folly_network_address
)

# --- Config loader (YAML parsing + validation + resolution + init) ---
# Application-level: ParsedConfig rfl structs, loadConfig(), resolveConfig(),
# handleConfigSubcommand(). Links rfl/yaml-cpp; consumers that only need
# resolved Config types can depend on o_rly_config alone.

add_library(o_rly_config_loader STATIC
src/config/loader.cpp
src/config/config_resolver.cpp
src/config/config_init.cpp
)

target_include_directories(o_rly_config_loader
PUBLIC
${PROJECT_SOURCE_DIR}/include
)

target_link_libraries(o_rly_config_loader PUBLIC
o_rly_config
reflectcpp
yaml-cpp
Folly::folly_file_util
)

target_compile_options(o_rly_config_loader PRIVATE -Wall -Wextra -Wpedantic)

# --- Main executable ---

add_executable(o_rly
src/main.cpp
)

target_link_libraries(o_rly PRIVATE o_rly_core)
target_link_libraries(o_rly PRIVATE o_rly_core o_rly_config_loader)

# --- Tests ---

if(ORLY_BUILD_TESTS)
enable_testing()
find_package(GTest REQUIRED)

add_executable(o_rly_relay_test
tests/ORelayTest.cpp
${PROJECT_SOURCE_DIR}/deps/moxygen/moxygen/test/TestUtils.cpp
Expand All @@ -71,8 +156,42 @@ if(ORLY_BUILD_TESTS)
)
include(GoogleTest)
gtest_discover_tests(o_rly_relay_test)

add_executable(o_rly_config_test
tests/config/loader_test.cpp
)
target_link_libraries(o_rly_config_test PRIVATE
o_rly_config_loader
GTest::gtest_main
GTest::gmock
# LINK_LIBRARY:WHOLE_ARCHIVE requires CMake >= 3.24 (cmake_minimum_required is 3.25).
"$<LINK_LIBRARY:WHOLE_ARCHIVE,gflags_nothreads_static>"
)
# gflags_nothreads_static also appears as a plain transitive dep from folly;
# override the feature so CMake doesn't warn about mixed link strategies.
set_property(TARGET o_rly_config_test PROPERTY
LINK_LIBRARY_OVERRIDE "WHOLE_ARCHIVE,gflags_nothreads_static"
)
target_compile_definitions(o_rly_config_test PRIVATE
CONFIG_EXAMPLE_PATH="${PROJECT_SOURCE_DIR}/config.example.yaml"
)
gtest_discover_tests(o_rly_config_test)

add_executable(o_rly_config_resolver_test
tests/config/config_resolver_test.cpp
)
target_link_libraries(o_rly_config_resolver_test PRIVATE
o_rly_config_loader
GTest::gtest_main
GTest::gmock
"$<LINK_LIBRARY:WHOLE_ARCHIVE,gflags_nothreads_static>"
)
set_property(TARGET o_rly_config_resolver_test PROPERTY
LINK_LIBRARY_OVERRIDE "WHOLE_ARCHIVE,gflags_nothreads_static"
)
gtest_discover_tests(o_rly_config_resolver_test)
endif()

include(${PROJECT_SOURCE_DIR}/cmake/Lint.cmake)

install(TARGETS o_rly o_rly_core)
install(TARGETS o_rly o_rly_core o_rly_config_loader)
20 changes: 20 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# o-rly relay configuration
# See: o_rly dump-config-schema | scripts/gen-config-reference.sh

listeners:
- name: main
udp:
socket:
address: "::" # Bind address (default: "::" = all interfaces)
port: 9668 # Listen port (1-65535)
tls:
# cert_file: /path/to/cert.pem # Required when insecure: false
# key_file: /path/to/key.pem # Required when insecure: false
insecure: true # Skip TLS for local development
endpoint: "/moq-relay" # WebTransport endpoint path
# moqt_versions: [14, 16] # MOQT draft versions (empty = all supported)

cache:
enabled: true # Enable relay cache
max_tracks: 100 # Max number of cached tracks
max_groups_per_track: 3 # Max groups per track in cache
4 changes: 2 additions & 2 deletions design/configuration.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ listeners:
initial_congestion_control: bbr
initial_rtt_ms: 100

tls_credentials:
tls:
# TODO: Tls credential providers
# - Do we support more then one provider at a time?

Expand All @@ -31,7 +31,7 @@ listeners:
port: 443
transport:
proxygen: # I don't know who will implement webtransport/h2
tls_credantials: ~ #TODO, see above
tls: ~ #TODO, see above

service_defaults:
slow_subscriber_queue_max_bytes: 1000
Expand Down
2 changes: 1 addition & 1 deletion design/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ global ─── process-wide settings (worker_threads, log_level,
│ │ └── transport ─── implementation-specific config (e.g. mvfst, proxygen)
│ │ └── protocols, tunables
│ ├── moqt_implementation ─── MOQT library to use (e.g. moxygen, libquicr)
│ └── tls_credentials ─── TLS cert/key provider config
│ └── tls ─── TLS cert/key provider config
├── service_defaults ─── defaults inherited by all services
└── services[] ─── matched by MOQT authority + path (maps to CLIENT_SETUP), defines MOQT scope
├── listeners ─── optional: restrict which listeners serve this service
Expand Down
116 changes: 116 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Config System

## Overview

Config goes through three stages:

```
YAML file → ParsedConfig → resolveConfig() → Config (via ResolvedConfig)
(loader.h) (config_resolver.h) (config.h)
```

1. **Load & parse** — `loadConfig()` reads a YAML file and deserializes it into `ParsedConfig` structs using reflect-cpp (`rfl::yaml::read`). This handles syntax and type checking. Optional `strict` mode rejects unknown fields.

2. **Validate & resolve** — `resolveConfig()` takes a `ParsedConfig` and applies semantic validation (e.g. "port must be non-zero", "cert required when not insecure"). It transforms the parsed representation into the final `Config` types and collects warnings for non-fatal issues.

3. **Use** — The rest of the codebase only sees `Config` (and its nested types like `ListenerConfig`, `CacheConfig`). These are plain C++ structs with no reflect-cpp dependency.

The split between `ParsedConfig` and `Config` exists because the YAML-facing shape (flat fields, optionals, description annotations) differs from what the application needs (e.g. `folly::SocketAddress`, `std::variant<Insecure, TlsConfig>`).

## Key files

| File | Role |
|---|---|
| `include/o_rly/config/loader/parsed_config.h` | YAML-facing config structs with `rfl::Description` annotations |
| `include/o_rly/config/config.h` | Final config types used by the application |
| `include/o_rly/config/resolved_config.h` | `ResolvedConfig` — wraps `Config` + warnings |
| `include/o_rly/config/loader/loader.h` | `loadConfig()`, `generateSchema()` |
| `include/o_rly/config/loader/config_resolver.h` | `resolveConfig()` |
| `include/o_rly/config/loader/config_init.h` | CLI subcommand handling (`validate-config`, `dump-config-schema`) |
| `src/config/loader.cpp` | Load & parse implementation |
| `src/config/config_resolver.cpp` | Validation & resolution logic |
| `src/config/config_init.cpp` | Subcommand orchestration |
| `config.example.yaml` | Example config file |

## How to add a new config field

### 1. Add to `ParsedConfig` (`parsed_config.h`)

Add a field to the appropriate `Parsed*Config` struct. Every field is wrapped in `rfl::Description<"...", T>` which serves double duty: the string becomes the JSON schema description, and the type `T` is what gets deserialized from YAML.

```cpp
struct ParsedCacheConfig {
rfl::Description<"Enable relay cache", bool> enabled;
rfl::Description<"Max cached tracks, ignored when disabled", uint32_t> max_tracks;
// New field:
rfl::Description<"Eviction policy", std::string> eviction_policy;
};
```

Use `std::optional<T>` for fields that can be omitted in the YAML file:

```cpp
rfl::Description<"Optional timeout in ms", std::optional<uint32_t>> timeout_ms;
```

Field names use `snake_case` — reflect-cpp maps them directly to YAML keys.

**Accessing values:** `rfl::Description` wraps the inner type, accessed via `.value()`. For `std::optional` fields that's two levels: `field.value()` returns the `std::optional`, then `.value()` or `.value_or()` unwraps it.

### 2. Add to `Config` (`config.h`)

Add the corresponding field to the final config struct. Use concrete types — no reflect-cpp wrappers, no optionals (resolve defaults in step 3).

```cpp
struct CacheConfig {
size_t maxCachedTracks;
size_t maxCachedGroupsPerTrack;
// New field:
std::string evictionPolicy;
};
```

### 3. Validate and resolve (`config_resolver.cpp`)

In `resolveConfig()`:
- Add validation rules to the validation section (push errors/warnings as needed).
- Map the parsed field to the final `Config` field in the resolution section.

```cpp
// Validation
if (cache.eviction_policy.value() != "lru" && cache.eviction_policy.value() != "fifo") {
errors.push_back("cache.eviction_policy must be 'lru' or 'fifo'");
}

// Resolution
CacheConfig cacheConfig{
.maxCachedTracks = ...,
.maxCachedGroupsPerTrack = ...,
.evictionPolicy = cache.eviction_policy.value(),
};
```

### 4. Update `config.example.yaml`

Add the new field with a comment explaining it.

### 5. Adding a new config section

If you need a new top-level section (not just a field), also:
- Create a new `Parsed*Config` struct in `parsed_config.h` and add it to `ParsedConfig`.
- Create a new final struct in `config.h` and add it to `Config`.
- Handle it in `resolveConfig()`.

## Subcommands

- `o_rly dump-config-schema` — prints the JSON schema (auto-generated from `ParsedConfig` + `rfl::Description` annotations).
- `o_rly validate-config --config <path>` — loads, parses, and validates without starting the server.
- `--strict_config` flag — rejects unknown YAML fields and promotes warnings to errors.

## reflect-cpp notes

- We use reflect-cpp v0.18.0 for YAML deserialization and JSON schema generation.
- `rfl::Description<"text", T>` annotates fields for schema generation. Access the inner value with `.value()`.
- `rfl::yaml::read<T>()` deserializes; `rfl::yaml::read<T, rfl::NoExtraFields>()` for strict mode.
- `rfl::json::to_schema<T>()` generates JSON schema from the type structure.
- Compile-time string utilities in `string_literal.h` help build description strings from constants.
38 changes: 38 additions & 0 deletions include/o_rly/config/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include <cstddef>
#include <string>
#include <variant>

#include <folly/SocketAddress.h>

namespace openmoq::o_rly::config {

struct TlsConfig {
std::string certFile;
std::string keyFile;
};

struct Insecure {};

using TlsMode = std::variant<Insecure, TlsConfig>;

struct CacheConfig {
size_t maxCachedTracks; // 0 when cache disabled
size_t maxCachedGroupsPerTrack;
};

struct ListenerConfig {
std::string name;
folly::SocketAddress address;
TlsMode tlsMode;
std::string endpoint;
std::string moqtVersions; // comma-separated string
};

struct Config {
ListenerConfig listener;
CacheConfig cache;
};

} // namespace openmoq::o_rly::config
27 changes: 27 additions & 0 deletions include/o_rly/config/loader/config_init.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once

#include <string>
#include <string_view>

#include <folly/Expected.h>

#include "o_rly/config/resolved_config.h"

namespace openmoq::o_rly::config {

constexpr std::string_view kDumpConfigSchemaCommand = "dump-config-schema";
constexpr std::string_view kValidateConfigCommand = "validate-config";

/// Returns usage lines for config-specific subcommands.
std::string configSubcommandUsage();

/// Handle config-specific subcommands or load+resolve config.
/// Returns ResolvedConfig (with warnings) or an exit code.
folly::Expected<ResolvedConfig, int> handleConfigSubcommand(
std::string_view subcommand,
std::string_view configPath,
bool strictConfig,
const char* programName
);

} // namespace openmoq::o_rly::config
Loading