콘텐츠로 이동

C++ configuration과 host 언어별 interface

C++ 언어별 interface 목차

1. Host maintenance

Host lifecycle operation은 relocate()shutdown()으로 나눈다. relocate()는 현재 object를 선택한 운영 목적에 맞는 node로 이전하고 host를 relocated 상태로 유지한다. Application은 relocation 결과를 확인한 뒤 필요할 때 shutdown()을 호출한다. Relocation이 필요하지 않으면 shutdown()만 호출할 수 있다.

Application은 relocate()를 호출할 때 목적을 반드시 지정한다. planned_maintenance는 같은 application version의 다른 node로 object를 이전한 뒤 현재 host를 점검하거나 재시작할 때 사용한다. rolling_update는 application이 지정한 더 높은 version으로만 object를 이전할 때 사용한다.

공개 interface에는 retire(), drain()await_drained()가 없다. stop()request_stop()은 host의 shutdown()을 시작한다. MeshName을 받는 component lifecycle operation은 제공하지 않는다.

namespace zlink::framework {

enum class framework_runtime_state_t {
 preparing = 0,
 serving = 1,
 relocating = 2,
 relocated = 3,
 draining = 4,
 stopped = 5,
 error = 6
};

enum class relocation_outcome_t {
 relocated = 0,
 blocked = 1
};

enum class relocation_mode_t {
 planned_maintenance = 0,
 rolling_update = 1
};

enum class relocation_reason_t {
 none = 0,
 target_unavailable = 1,
 store_unavailable = 2,
 relocation_disabled = 3,
 state_incompatible = 4,
 deadline_exceeded = 5,
 relocation_failed = 6,
 runtime_not_ready = 7,
 manual_topology_unsupported = 8,
 shutdown_requested = 9,
 operation_in_progress = 10
};

struct relocation_options_t {
 relocation_mode_t mode;
 std::optional<std::int64_t> target_application_version;
 std::optional<std::chrono::milliseconds> deadline;
};

struct relocation_result_t {
 relocation_mode_t mode;
 std::int64_t effective_target_application_version;
 relocation_outcome_t outcome;
 relocation_reason_t reason;
};

enum class termination_outcome_t {
 stopped = 0,
 force_stopped = 1
};

enum class termination_reason_t {
 none = 0,
 deadline_exceeded = 1,
 teardown_failed = 2
};

struct termination_result_t {
 termination_outcome_t outcome;
 termination_reason_t reason;
};

} // namespace zlink::framework

relocation_options_t::mode는 필수다. planned_maintenance에서는 target_application_version을 지정하지 않는다. Framework는 source와 같은 application version만 선택하며 result의 effective_target_application_version에 source version을 기록한다. rolling_update에서는 source보다 큰 target_application_version을 반드시 지정한다. Framework는 이 값과 정확히 같은 application version만 선택하고 result에도 같은 값을 기록한다. 이러한 option 조합을 위반하면 Framework는 std::invalid_argument로 호출을 거부하고 shared operation과 host state를 변경하지 않는다.

deadline이 비어 있으면 기본 30초를 사용한다. wait_cancellation은 waiter만 중단하며 이미 시작한 shared operation을 취소하지 않는다. Mode와 effective target application version이 같은 동시 호출은 이미 실행 중인 shared operation에 합류하며 같은 terminal result를 받는다. 첫 호출의 deadline이 shared operation deadline을 고정하며, 뒤에 합류한 호출의 deadline은 이를 늘리거나 줄이지 않는다. Mode 또는 effective target application version이 다른 동시 호출은 기존 operation에 합류하거나 이를 변경하지 않고 blocked/operation_in_progress를 반환한다. 이 결과의 mode와 effective target version은 거부된 호출이 요청한 유효한 option을 반영한다.

relocate()는 continuity preflight가 실패하면 admission과 state를 바꾸지 않고 blocked를 반환한다. 성공하면 relocated/none을 반환하며 host process와 infrastructure connection은 유지한다. shutdown()blocked를 반환하지 않는다.

두 mode 모두 candidate를 다음 순서로 좁힌다.

  1. planned_maintenance는 source와 version이 같은 candidate만 남긴다. rolling_update는 요청한 target version과 정확히 같은 candidate만 남긴다.
  2. 같은 Mesh에서 source가 아니며 serving 상태인 Object Server만 남긴다.
  3. 남은 candidate에 factory, stable type과 relocation policy·adapter 호환성 검사를 적용한다.
  4. Population capacity와 reservation 가능 여부를 확인하고, source와 같은 non-empty maintenance wave에 속한 candidate를 제외한다.
  5. 같은 descriptor snapshot과 Core peer table에서 RID와 lifecycle generation이 일치하며 admitted·ready인 candidate만 남긴다.
  6. 마지막 후보 집합에서 node-wide placement weight를 적용한다.

Version 조건을 capability·capacity·weight보다 먼저 적용하므로, rolling update가 같은 version node로 fallback하거나 planned maintenance가 더 높은 version node를 선택하지 않는다. 첫 번째 단계 뒤 candidate가 없거나 이후 조건을 만족하는 target이 없으면 blocked/target_unavailable을 반환한다. 여러 relocation unit은 모두 같은 effective target version을 사용하지만 각각 다른 eligible node를 선택할 수 있다.

Local manual RouteMesh peer, ClientServer client endpoint, fanout subscriber endpoint 또는 manual fanout publisher가 하나라도 있으면 relocate()blocked/manual_topology_unsupported를 반환한다. Automatic RouteMesh는 source의 Core peer table에서 descriptor와 같은 RID·lifecycle generation이 admitted·ready가 된 뒤에만 relocating으로 전환한다. 이 제한은 shutdown()에 적용하지 않는다.

blocked/deadline_exceeded는 모든 target의 Prepared 완료와 host Relocating descriptor publication 전에 deadline이 끝난 결과다. Connection-bound work가 pre-Captured deadline 안에 terminal drain되지 않은 경우도 relocation_disabled가 아니라 이 결과를 사용한다. Bound-session request는 drain 대상이 아니며 다른 Actor request와 같이 frozen journal과 ingress hold 규칙을 따른다. Framework는 relocation staging과 reservation을 정리하고 reversible seal을 해제한 뒤 host state와 admission을 복원한다. 모든 target이 Prepared이고 Relocating publication이 성공하면 모든 relocation unit을 완료하고 relocated로 전환한다.

relocatingshutdown()이 시작되면 현재 atomic relocation unit을 terminal 상태로 확정한 뒤 나머지 relocation을 중단한다. Relocation waiter는 blocked/shutdown_requested를 받고, host는 draining으로 전환하여 종료를 계속한다. 이미 relocated인 host에서 shutdown()을 호출하면 남은 connection과 resource만 정리한다. serving에서 호출하면 relocation 없이 draining으로 전환한다.

relocation_result_ttermination_result_t의 유효한 조합은 다음과 같다. Caller는 relocation 결과가 relocated/none일 때만 모든 object의 이전이 끝났다고 판단한다.

Result Reason
relocated none
blocked target_unavailable, store_unavailable, relocation_disabled, state_incompatible, deadline_exceeded, relocation_failed, runtime_not_ready, manual_topology_unsupported, shutdown_requested, operation_in_progress
stopped none
force_stopped deadline_exceeded, teardown_failed

2. App / Host

app_t는 framework의 가장 바깥 public type이다. 사용자는 app_t::create()로 앱을 만들고, add_zlink_framework()가 반환한 options builder에서 services, handlers, zlink runtime을 한 번에 구성한 뒤 run을 호출한다. 낮은 수준의 runtime builder는 일반 애플리케이션 표면에 직접 노출하지 않는다.

namespace zlink::framework {

class app_t {
public:
 app_t();
 ~app_t();
 app_t(app_t &&) noexcept;
 app_t &operator=(app_t &&) noexcept;
 app_t(const app_t &) = delete;
 app_t &operator=(const app_t &) = delete;

 static app_t create();

 config_builder_t &config() noexcept;
 logging_builder_t &logging() noexcept;
 health_builder_t &health() noexcept;

 app_t &add_module(module_t &module);
 zlink_framework_options_t &add_zlink_framework();
 app_t &add_zlink_framework(
 std::function<void(zlink_framework_options_t &)> configure);
 template <typename TModule, typename... TArgs>
 app_t &add_zlink_framework(TArgs &&...args);
 app_t &add_hosted_service(std::unique_ptr<hosted_service_t> service);

 bool is_ready() const noexcept;
 app_t &set_message_flow_mode(message_flow_log_mode_t mode) noexcept;
 message_flow_log_mode_t message_flow_mode() const noexcept;

 task_t<relocation_result_t> relocate(
 relocation_options_t options,
 std::stop_token wait_cancellation = {});
 task_t<termination_result_t> shutdown(
 std::chrono::milliseconds deadline = std::chrono::seconds{30},
 std::stop_token wait_cancellation = {});

 int run(int argc, char **argv);
 void stop() noexcept;
 void request_stop() noexcept;
};

} // namespace zlink::framework

runint를 반환한다. 반환값은 process exit code로 사용할 수 있어야 한다. handler 예외, runtime 오류, signal shutdown은 host가 수집하고 종료 경로를 닫는다.

3. Service 등록

C++ binding에는 DI 개념이 없으므로 Framework가 handler와 hosted component의 service lifetime을 관리한다. Application은 service type과 lifetime만 등록한다. Runtime의 provider, scope와 type-erased factory invocation은 public interface에 포함하지 않는다.

namespace zlink::framework {

enum class service_lifetime_t {
 singleton = 0,
 scoped = 1,
 transient = 2
};

class service_collection_t {
public:
 service_collection_t();
 ~service_collection_t();
 service_collection_t(service_collection_t &&) noexcept;
 service_collection_t &operator=(service_collection_t &&) noexcept;
 service_collection_t(const service_collection_t &) = delete;
 service_collection_t &operator=(const service_collection_t &) = delete;

 template <typename T>
 service_collection_t &add_singleton();

 template <typename T, typename... TDependencies>
 service_collection_t &add_singleton();

 template <typename T>
 service_collection_t &add_singleton(std::unique_ptr<T> instance);

 template <typename T>
 service_collection_t &add_scoped();

 template <typename T, typename... TDependencies>
 service_collection_t &add_scoped();

 template <typename T>
 service_collection_t &add_transient();

 template <typename T, typename... TDependencies>
 service_collection_t &add_transient();

 template <typename T, typename... TDependencies, typename TFactory>
 service_collection_t &add_factory(
 TFactory factory,
 service_lifetime_t lifetime = service_lifetime_t::transient);
};

} // namespace zlink::framework

기본 생성 규칙은 아래와 같다.

  • add_singleton<T>(), add_transient<T>()는 기본 생성 가능한 타입만 자동 생성한다.
  • 생성자 의존성이 있는 타입은 add_singleton<T, Dep1, Dep2>(), add_scoped<T, Dep1, Dep2>(), add_transient<T, Dep1, Dep2>()처럼 의존 타입을 명시한다. Framework는 내부 provider에서 Dep1, Dep2를 resolve한 뒤 T(Dep1 &, Dep2 &)를 호출한다.
  • add_scoped<T>()는 framework가 소유하는 scope 안에서만 resolve한다.
  • 복잡한 외부 객체 생성이나 조건부 생성이 필요한 경우에만 add_factory<T, Dep1, Dep2>()를 사용한다. Factory는 선언한 dependency의 typed reference만 받으며 runtime provider나 scope를 받지 않는다.
  • Channel·HTTP handler class는 handler group이나 HTTP route에 등록하면 Framework가 dispatch scope에서 생성한다. Application이 같은 handler type을 service collection에 다시 등록하지 않는다.
  • Spot packet·Actor payload handler는 Spot member function이므로 별도 service가 아니다. Timer handler class는 Spot activation scope에서 한 번 생성하며 같은 activation의 tick이 재사용한다. Timer handler 생성자의 dependency 매개 변수는 해당 scope에서 resolve한다.
  • Boost.Ext.DI 같은 외부 DI 라이브러리는 public dependency로 두지 않는다.

scoped lifetime은 raw transport 기능이 아니라 Framework가 소유하는 DI lifetime이다. Framework는 handler dispatch, STREAM session, Spot activation의 scope 경계를 내부에서 만든다. channel handler는 dispatch마다 scope를 만들고, STREAM session은 session scope를 가지며, Spot과 Entry Spot은 activation scope를 가진다. actor factory는 actor creation scope에서 resolve하고, actor instance 자체는 actor runtime이 소유한다.

예시는 아래와 같다.

options.services()
 .add_singleton<order_repository_t>()
 .add_transient<order_service_t, order_repository_t>()
 .add_transient<order_handler_t, order_service_t>();

4. Hosted Service 와 Module

hosted service는 app lifecycle에 묶이는 background worker다.

namespace zlink::framework {

class hosted_service_t {
public:
 virtual ~hosted_service_t() = default;
 virtual task_t<void> start(service_provider_t &services) = 0;
 virtual void request_stop() noexcept {}
 virtual void stop() noexcept = 0;
};

class module_t {
public:
 virtual ~module_t() = default;
 virtual void configure(zlink_framework_options_t &options) = 0;
};

template <typename TModule>
concept framework_module_contract_t =
 requires(TModule &module,
 zlink_framework_options_t &options) {
 module.configure(options);
 };

} // namespace zlink::framework

module은 서비스 등록, runtime 구성과 handler 등록을 zlink_framework_options_t 하나로 묶는다. 별도 low-level builder와 handler registry를 넘겨 runtime wiring 순서를 application에 노출하지 않는다.

app_t::add_zlink_framework().NETAddZLinkFramework(...)에 대응하는 C++ 고수준 구성 진입점이다. 반환된 builder를 직접 이어서 구성하므로 구성 전체를 감싸는 lambda가 필요하지 않다. 기존 callback overload는 호환성을 위해 유지한다. C++에는 assembly reflection이 없으므로 .NETAddHandlersFromAssemblyOf(...)만 그대로 옮기지 않는다. 그 대신 handler group을 먼저 고르고, 그 group 안에 handler 타입을 명시해서 options.handlers().group(group_name).add<THandler>(), add_send<THandler>(), add_publish<THandler>()로 등록한다. 나머지 codec, discovery, RouteMesh membership, handler group 구성은 .NET과 같은 읽기 수준을 유지한다.

JSON은 기본 codec이므로 별도 등록하지 않는다. 사용자가 모든 request/reply message type을 codec 설정에 나열하지 않는다. C++ framework는 options.handlers().group(...).add<THandler>()에서 handler의 request_type, reply_type을 읽고 기본 JSON serializer를 내부에서 선택한다. send handler는 message_type, publish handler는 event_type을 읽어 같은 방식으로 serializer와 handler registry 항목을 등록한다. options.codecs().use(...)는 일반 message type을 나열하는 단계가 아니라, 기본 JSON으로 표현할 수 없는 payload나 별도 binary serializer extension을 연결하는 고급 확장점이다. 따라서 request/send/publish handler를 같은 group 이름으로 묶고, channel builder의 .add_handler_group(...)에서 channel에 연결할 수 있다. handler group은 channel 종류와 맞아야 한다. RouteMesh ChannelName은 request/send handler group을 받을 수 있고, fanout channel은 publish handler group만 받을 수 있다. 맞지 않는 group을 연결하면 options 작성 시점에 설정 오류로 실패한다. 같은 channel에 같은 packet 이름의 handler가 두 번 노출되면 host가 message를 받기 전에 startup 설정 오류로 실패한다. 중복 handler는 실행 중 request protocol 오류로 처리하지 않는다. 이 규칙은 fluent options의 handler group 경로에 적용한다. channel이 group을 먼저 참조한 뒤 handler가 들어오는 경우와 handler가 먼저 등록되고 channel이 나중에 group을 참조하는 경우 모두 중복을 허용하지 않는다. MeshNode는 ROUTER listen endpoint와 0개 이상의 ChannelName membership을 가질 수 있다. 호출 또는 Node direct 전용 MeshNode는 membership 없이 시작할 수 있고, Channel handler를 제공하는 MeshNode는 Server membership을 하나 이상 등록해야 한다. 각 Server ChannelName은 request/send handler group을 가질 수 있다. fanout subscriber는 publish handler group을 하나 이상 등록해야 한다. Channel·HTTP handler에 생성자 의존성이 있으면 생성자 매개 변수로 dep1_t &, dep2_t &를 받는다. framework는 각 dispatch scope에서 dependency를 resolve해 그 생성자로 handler를 만든다. Handler를 singleton service로 등록하지 않는다. logger_t<THandler>는 framework 기본 dependency다. handler 생성자가 logger_t<THandler> &를 받으면 사용자가 별도 service registration을 작성하지 않아도 DI가 .NETILogger<T>처럼 category logger를 주입한다. 로그 출력 대상은 handler 등록이 아니라 app.logging().use_console(), app.logging().use_file(...) 같은 host logging 설정에서 정한다. custom category가 필요하면 logger_factory_t를 dependency로 받아 handler 내부에서 category logger를 만들 수 있다.

auto &options = app.add_zlink_framework();
options.use_filter<audit_filter_t>();
options.metadata()
 .allow_session_to_actor("trace-id")
 .allow_actor_to_session("trace-id");

auto mesh = options.add_route_mesh(sample_names_t::application_mesh);
mesh.listen(7300).set_routing_id(topology.application_rid);
mesh.channel(sample_names_t::api_channel).server().add_handler_group("api");
mesh.channel(sample_names_t::play_channel).client();

 // 시작 시 사용할 message-flow 관측 수준을 설정한다.
options.configure_dispatch().message_flow(
 zlink::framework::message_flow_log_mode_t::errors);

options.handlers()
 .group("api")
 .add<authenticate_player_handler_t>()
 .add<match_bingo_api_handler_t>()
 .add_send<player_command_handler_t>();

options.handlers()
 .group("events")
 .add_publish<notification_event_handler_t>();

자동 peer discovery를 사용하면 등록된 location_store_t provider에서 같은 MeshName의 MeshNode descriptor를 찾는다. 공식 Redis package는 사용할 수 있는 provider 가운데 하나다. 수동 peer는 peer_connections().connect(endpoint) 또는 expected RID를 함께 받는 overload로 등록한다. fanout subscriber의 endpoint 목록은 RouteMesh peer intent와 별도다.

이 구조에서는 샘플 main.cpp, role *HostFactory, 일반 사용자 설정 예제가 handler member function pointer, handler용 DI factory lambda, monitoring channel 문자열, serializer smoke 검증, message type을 모두 나열하는 codec 등록 같은 세부 구현을 직접 알 필요가 없다. 그런 내용이 보이면 framework options builder가 충분히 깊지 않은 것으로 본다.

zlink_framework_options_t의 사용자 표면은 fluent options builder로 제한한다. 일반 사용자 설정에는 낮은 수준 channel runtime builder를 직접 노출하지 않는다. C++ 내부 runtime builder에는 낮은 수준 API가 남아 있을 수 있지만, 샘플과 guide 수준의 설정은 아래처럼 역할이 바로 보이는 형태를 사용한다.

options.configure_dispatch(...)는 interface graph를 만들지 않고 dispatch_options_t value를 람다에 넘긴다. 이 value는 Spot과 STREAM dispatch mode, unhandled request/send/publish 정책, message flow diagnostics 설정을 담는다. native dispatch token, queue slot, handler lookup table은 이 표면에 나오지 않는다. diagnostics sample rate는 0.0에서 1.0 사이여야 하며 NaN은 허용하지 않는다. send와 publish는 reply path가 없으므로 unhandled 정책에 reply_error를 사용할 수 없다.

options.add_location_store(redis_location_store);

auto mesh = options.add_route_mesh(sample_names_t::application_mesh)
 .listen(7300)
 .set_routing_id(topology.application_rid);
mesh.channel(sample_names_t::api_channel)
 .server()
 .add_handler_group("api");

options.add_fanout_channel(sample_names_t::notification_channel)
 .enable_publisher(7400)
 .set_routing_id(topology.notification_publisher_rid)
 .enable_subscriber() // 같은 ChannelName의 publisher를 location store에서 자동으로 발견한다.
 .add_handler_group("events");

mesh.channel(sample_names_t::game_channel).client();
mesh.peer_connections().connect(topology.play_router_endpoint);
mesh.add_entry_spot<session_entry_spot_t>();

options.add_stream_node(sample_names_t::stream_name)
 .bind(7500)
 .enable_actor_dispatch()
 .register_session<client_session_t>()

Entry Spot의 SpotId는 Framework가 <prefix>-entry-<lowercase-canonical-uuid-v4> 형식으로 발급한다. Application이 Entry Spot의 RoutingId나 고정 SpotId를 설정하는 public option은 제공하지 않는다.

enable_actor_dispatch()는 session Actor dispatch에 global ActorId lookup과 ActorRef bind를 사용하도록 설정한다. Target MeshName을 받거나 첫 MeshNode에서 추론하지 않는다. Actor dispatch를 사용하지 않는 STREAM node는 호출하지 않는다. 같은 builder에서 두 번 호출하면 startup이 실패한다. register_session<TSession>().NETRegisterSession<TSession>()에 맞춘 typed session 등록 표면이다. TSessionpacket_stream_session_t를 상속해야 하며, framework service collection에 stream-session scope 서비스로 등록된다. TSession::session_name이 있으면 그 값을 native packet session 이름으로 사용하고, 없으면 타입 이름 기반 message name을 사용한다. register_session(name)은 session 이름을 직접 지정해야 하는 low-level 구성에 남긴다. 하나의 stream node에는 packet session을 하나만 선언한다. register_session<T>()register_session(...)을 중복 호출하면 마지막 값으로 덮어쓰지 않고 설정 오류로 처리한다.

MeshNode는 listen(...)으로 ROUTER endpoint를 열고 channel(...) 뒤에 role을 선택한다. 자동 peer는 Redis descriptor로, 수동 peer는 peer_connections()로 구성한다. Node·Channel·Spot·Actor 메시지는 같은 MeshNode ROUTER를 사용한다. fluent options에서 channel 이름, handler group 이름, endpoint, MeshName, stream node 이름처럼 식별자나 연결 주소로 쓰이는 값은 빈 문자열이나 공백 문자열을 허용하지 않는다. 잘못된 값은 low-level socket/runtime까지 전달하지 않고 builder 호출 또는 options 적용 시점의 framework error로 닫는다. Spot 코드는 owner MeshNode의 client로 Node direct, ChannelName select-one과 Logical Multicast를 사용한다. Logical Multicast는 별도 PUB/SUB 역할을 구성하지 않는다. classic fanout만 독립 PUB/SUB socket을 사용한다.

4.1 HTTP Hosting

HTTP hosting은 ASP.NET Core Minimal API의 MapGet, MapPost, MapPut, MapDelete에 대응하는 C++ framework 표면이다. MVC controller, Razor page, template rendering, WebSocket transport는 범위에 넣지 않는다. 대신 route handler, DI scope, JSON binding, middleware/filter, logging, validation, error mapping, zlink channel 호출은 같은 application host 안에서 제공한다.

namespace zlink::framework {

class http_options_builder_t {
public:
 http_options_builder_t &listen(std::string endpoint);
 http_options_builder_t &configure_tls(
 std::function<void(http_tls_options_builder_t &)> configure);
 http_options_builder_t &configure_server(
 std::function<void(http_server_options_builder_t &)> configure);

 template <typename THandler>
 http_options_builder_t &map_get(std::string path);

 template <typename THandler>
 http_options_builder_t &map_post(std::string path);

 template <typename THandler>
 http_options_builder_t &map_put(std::string path);

 template <typename THandler>
 http_options_builder_t &map_delete(std::string path);

 template <typename TMiddleware>
 http_options_builder_t &use();

 http_options_builder_t &map_health(std::string path);
 http_options_builder_t &map_readiness(std::string path);
 http_options_builder_t &map_liveness(std::string path);
};

struct http_context_t {
 http_method_t method;
 std::string path;
 std::string correlation_id;
 std::map<std::string, std::string> request_headers;
 std::map<std::string, std::string> response_headers;
 std::optional<std::string> response_body;
 int response_status;

 http_context_t &response_header(std::string name, std::string value);
 http_context_t &json_response(int status, std::string body);
};

struct http_request_t {
 http_method_t method;
 std::string path;
 std::string target;
 std::string query_string;
 std::string correlation_id;
 std::map<std::string, std::string> headers;
 std::map<std::string, std::string> route_values;
 std::map<std::string, std::string> query_values;
 std::string body;
 std::string content_type;
 std::string remote_endpoint;
};

struct http_response_t {
 int status = 200;
 std::string body;
 std::string content_type = "application/json";
 std::map<std::string, std::string> headers;

 http_response_t &header(std::string name, std::string value);
};

class handler_options_builder_t {
public:
 class group_builder_t {
 public:
 template <typename THandler>
 group_builder_t &add();

 template <typename THandler>
 group_builder_t &add_send();

 template <typename THandler>
 group_builder_t &add_publish();
 };

 group_builder_t group(std::string group_name);
};

class metadata_policy_builder_t {
public:
 metadata_policy_builder_t &add_forwarded_metadata_key(std::string key);
 metadata_policy_builder_t &allow_session_to_actor(std::string key);
 metadata_policy_builder_t &allow_actor_to_session(std::string key);
};

class codec_registration_context_t {
public:
 template <typename TPayload>
 codec_registration_context_t &add_serializer(
 typename serializer_t<TPayload>::serialize_fn_t serialize,
 typename serializer_t<TPayload>::deserialize_fn_t deserialize,
 std::string content_type = "application/octet-stream");
};

class codec_options_builder_t {
public:
 template <typename TExtension>
 codec_options_builder_t &use();
 template <typename TExtension>
 codec_options_builder_t &use(const TExtension &extension);
};

enum class core_hwm_profile_t {
 compact = 0,
 low_latency = 1,
 balanced = 2,
 throughput = 3
};

enum class application_job_queue_profile_t {
 compact = 0,
 low_latency = 1,
 balanced = 2,
 throughput = 3
};
enum class application_job_queue_pressure_state_t {
 running = 0,
 paused = 1
};

class zlink_framework_options_t {
public:
 handler_options_builder_t handlers();
 codec_options_builder_t codecs();
 metadata_policy_builder_t metadata();
 network_options_t &configure_network();
 worker_options_t &worker();
 dispatch_options_t &configure_dispatch();
 dispatch_options_t dispatch_options() const;
 core_hwm_options_t &configure_core_hwm();
 inbound_dispatch_options_t &configure_inbound_dispatch();
 location_options_t &configure_locations();
 location_options_t location_options() const;
 zlink_framework_options_t &set_max_pending(std::size_t count);
 zlink_framework_options_t &set_application_version(
 std::int64_t application_version);
 zlink_framework_options_t &set_maintenance_wave(
 std::optional<std::string> maintenance_wave);
 zlink_framework_options_t &set_default_request_timeout(
 std::chrono::milliseconds timeout);
 service_collection_t &services() noexcept;
 zlink_framework_options_t &add_location_store(
 std::shared_ptr<location_store_t> store);
 template <typename TStore>
 typename TStore::options_builder_type add_location_store();
 client_server_channel_builder_t add_client_server_channel(
 std::string channel_name);
 fanout_channel_builder_t add_fanout_channel(std::string channel_name);
 mesh_node_builder_t add_route_mesh(std::string mesh_name);
 stream_node_options_builder_t add_stream_node(std::string stream_name);
 stream_compression_options_builder_t configure_stream_compression();
 http_options_builder_t &http() noexcept;
 template <typename TFilter>
 zlink_framework_options_t &use_filter();

 zlink_framework_options_t &handler_coroutine_workers(
 std::size_t worker_count);
 std::size_t handler_coroutine_workers() const noexcept;
 zlink_framework_options_t &add_relocation_store(
 std::shared_ptr<relocation_store_t> store);
 template <typename TStore>
 typename TStore::options_builder_type add_relocation_store();
};

} // namespace zlink::framework

codec_options_builder_t::use(extension)은 host를 시작하기 전에 extension의 register_framework_codecs(codec_registration_context_t&)를 호출한다. Extension은 add_serializer<TPayload>(...)로 serializer를 등록한다. 이때 compile-time template argument TPayload가 송신 codec 선택에 사용하는 declared payload descriptor다. 실제 instance의 runtime type으로 다시 고르지 않는다. 이 type에 custom serializer가 없으면 기본 JSON 경로를 사용한다.

content_type에는 parameter가 없는 ASCII type/subtype을 전달한다. Registry는 앞뒤 SP와 TAB을 제거하고 ASCII 대문자를 소문자로 바꾼 canonical content-type을 key로 사용한다. 같은 canonical content-type을 다시 등록하면 나중 등록이 앞의 등록을 교체한다. Parameter, 값 내부의 공백, non-ASCII 문자 또는 비어 있는 token은 framework_error_kind_t::protocol_error다.

Host startup이 끝나면 registry는 바뀌지 않는다. 이후 등록 시도는 framework_error_kind_t::invalid_operation이다. 송신 선택 결과는 declared payload descriptor 1,024개까지 저장한다. 한도에 도달해도 기존 entry를 제거하지 않는다. 그 뒤 처음 보는 type은 송신할 때마다 registry를 다시 확인하며 결과를 저장하지 않는다.

수신 경로는 wire에서 받은 canonical content-type을 registry key와 정확히 비교한다. 등록되지 않았거나 canonical form이 아닌 값은 JSON으로 다시 해석하지 않고 framework_error_kind_t::protocol_error로 완료한다.

http_options_builder_t의 snapshot 생성과 validation 실행은 host startup 내부 책임이며 public interface가 아니다. application은 위 builder method로만 HTTP 설정을 구성한다.

Location runtime을 사용하는 application은 add_location_store<TStore>()가 반환한 store options builder로 연결 정보와 key prefix를 이어서 설정한다. 미리 만든 store instance를 받는 overload도 호환성을 위해 유지한다. Application은 Location Store를 정확히 하나 등록한다. RecreateOnRelocation 또는 PreserveStateWith factory가 하나라도 있거나 Instance Spot factory가 하나라도 있으면 add_relocation_store(...)로 Relocation Store도 정확히 하나 등록한다. Instance Spot factory가 없고 DisableRelocation factory만 있는 same-node 구성에는 Relocation Store가 필요하지 않다. 필요한 Store가 없거나 같은 capability가 중복 등록되면 Framework는 socket bind 전에 configuration error로 종료한다. Relocation Store는 Actor·Spot relocation의 state·queue·timer handoff payload를 보관하지 않는다 — handoff payload는 source memory에서 target으로 직접 전송한다. Store는 Instance Spot cold activation의 최초 message·생성 정보 기록과, relocation 뒤 완료되는 pending request의 terminal 결과 기록을 소유하므로 이 등록 요구는 그대로 유지된다.

configure_network()는 process 전체의 BindHost와 AdvertiseHost 기본값을 반환하며 listener별 설정이 이 값을 재정의한다. worker()는 bounded worker pool의 최소·최대 thread 수, idle timeout과 queue 상한을 반환한다. 두 option은 host 시작 전에만 변경할 수 있다.

configure_dispatch()는 host 전체 진단·unhandled dispatch 설정을 반환한다. configure_inbound_dispatch()는 Core HWM과 Application Job Queue 설정을 반환한다. C++의 기존 configure_core_hwm()은 Core-only 설정 surface로 유지되지만 job profile·manual cap·80/60 pressure threshold는 inbound-dispatch surface만 소유한다. Core memory limit·manual budget·profile은 Core에 그대로 전달하며 C++은 managed-runtime hint를 만들지 않는다. 두 surface의 Core HWM 항목은 동일한 startup 상태를 보는 호환 view이며 별도 profile이나 controller를 만들지 않는다. Core profile과 job queue profile의 기본값은 각각 balanced지만 서로 독립된 enum과 계산이다. Manual job cap은 1..2,147,483,647이고 생략하면 common spec의 startup CPU snapshot과 32/64/128/256 계수로 계산한다. Pressure threshold 기본값은 pause 80, resume 60이다. Pause는 1..100, resume은 0..99의 정수이고 resume은 pause보다 작아야 한다. 이 범위·순서 위반과 capacity overflow는 socket bind 전에 실패하며 runtime 중 다시 계산하지 않는다.

Application version과 maintenance wave는 host 전체에 한 번 설정한다. Version은 기본값 0인 non-negative signed 64-bit deployment ordinal이고 모든 local MeshNode가 같은 값을 게시한다. Empty optional wave는 maintenance wave exclusion을 사용하지 않는다는 뜻이다.

사용 예시는 아래와 같다.

auto &options = app.add_zlink_framework();
auto mesh = options.add_route_mesh(sample_names_t::application_mesh);
mesh.listen(7300) // 이 RouteMesh가 peer message를 받을 endpoint를 연다.
 .set_routing_id(topology.application_rid); // 같은 mesh 안에서 이 node를 식별한다.
mesh.channel(sample_names_t::api_channel)
 .server() // 이 node를 api_channel의 요청 처리 후보로 게시한다.
 .add_handler_group("api"); // 등록할 DI handler group을 Channel handler와 연결한다.
mesh.channel(sample_names_t::play_channel)
 .client(); // Server membership 없이 play_channel 호출 경로만 등록한다.

options.http()
 .listen(topology.api_http_endpoint) // HTTP client가 연결할 listener를 연다.
 .map_post<create_game_http_handler_t>("/games"); // POST /games를 DI handler에 연결한다.

HTTP handler의 type alias, DI constructor와 handle(...) shape declaration은 C++ HTTP hosting의 Handler signature 형식이 이 규칙을 소유한다. 이 언어별 interface 문서는 같은 application handler class를 다시 선언하지 않는다.

map_get<THandler>(...), map_post<THandler>(...), map_put<THandler>(...), map_delete<THandler>(...)는 handler type을 DI에 등록하고, request_typereply_type의 JSON serializer를 등록하며, HTTP route table에 method + path를 연결한다. request마다 DI scope를 만들고 handler를 resolve한다. handler가 반환한 DTO는 JSON response body가 되고, 기본 status는 200 OK다.

http_request_thttp_response_t는 framework public type이다. Raw HTTP handler도 Boost.Beast request, socket, SSL stream을 받지 않는다. map_*<THandler>(...)는 handler shape를 compile-time으로 판별하며 그 문서가 정한 호출 우선순위와 실패 조건을 그대로 적용한다.

route parameter와 query string은 request_type DTO에 binding한다. 예를 들어 /games/{gameId}/moves?actorId=p1로 들어온 값은 body DTO와 합쳐 handler request가 된다. 같은 필드가 body, route, query에 동시에 있으면 route, query, body 순서로 우선한다. 이 우선순위는 URL에 드러난 식별자가 request body보다 더 명시적인 입력이라는 ASP.NET Core식 route handler 사용성을 따르기 위한 규칙이다.

use<TMiddleware>()는 exception, logging, validation, auth, correlation id 같은 cross-cutting 처리를 route handler 앞뒤에 연결한다. middleware/filter는 Beast나 Asio 타입을 받지 않고 http_context_t와 framework DTO만 다룬다. middleware가 before(http_context_t&) 또는 after(http_context_t&)를 제공하면 runtime은 route handler 전후에 호출한다. request의 X-Correlation-Id 또는 X-Request-Idhttp_context_t::correlation_id로 들어가고 response의 X-Correlation-Id로 전파된다. middleware가 before(...)에서 json_response(...)를 설정하면 runtime은 handler를 호출하지 않고 해당 JSON response를 반환한다. map_health(...), map_readiness(...), map_liveness(...)app.health() report를 HTTP endpoint로 노출한다.

listen(...)http://https:// endpoint를 모두 받는다. https:// endpoint를 사용하면 configure_tls(...)로 server certificate와 private key를 설정해야 한다. TLS 설정 public 표면은 파일 경로, PEM data, reload policy 같은 framework 값만 사용하고 OpenSSL 또는 Boost.Asio SSL 타입을 노출하지 않는다.

HTTP runtime은 hosted_service_t로 app lifecycle에 묶인다. Boost.Beast, Boost.Asio, OpenSSL/SSL context 타입은 runtime 구현에만 있고 public header에는 나타나지 않는다. HTTP error response는 framework_error_kind_t를 기반으로 400, 404, 405, 500, 503, 504로 매핑한다.

handler 안에서 다른 channel로 request를 보낼 때도 호출자는 낮은 수준의 request/reply template 쌍이나 blocking wait를 보지 않아야 한다. .NETawait client.RequestAsync<TReply>(...)와 같은 읽기 수준을 C++에서는 아래처럼 표현한다.

샘플 namespace에서는 using zlink::framework::task_t;를 두고 task_t<T>처럼 짧게 쓴다. zlink::framework::task_t<T>를 handler signature마다 반복하면 async 의미보다 namespace 노이즈가 먼저 보이기 때문이다. framework public contract 문서에서는 전체 이름을 쓸 수 있지만, application sample과 guide 예제는 짧은 alias를 기본으로 한다.

task_t<match_bingo_api_res_t> handle(const match_bingo_api_req_t &request)
{
 allocate_bingo_room_res_t allocated = co_await _client
 .request(sample_names_t::play_channel,
 allocate_bingo_room_req_t { request.mode })
 .async<allocate_bingo_room_res_t>();

 co_return match_bingo_api_res_t { allocated.room_id };
}

샘플 handler는 .async().result().value()로 결과를 직접 꺼내지 않는다. 그런 코드는 handler가 runtime 안에서 blocking wait를 수행하는 것처럼 보이고, 모든 언어 버전에서 같은 async 모델을 제공한다는 목표와 맞지 않다.

class order_module_t final : public zlink::framework::module_t {
public:
 void configure(
 zlink::framework::zlink_framework_options_t &options) override
 {
 options.services().add_singleton<order_repository_t>();
 options.services()
 .add_factory<order_service_t, order_repository_t>(
 [](order_repository_t &repository) {
 return std::make_unique<order_service_t>(repository);
 });
 options.services().add_transient<order_handler_t>();
 options.handlers().group("orders").add_publish<order_handler_t>();
 }
};

5. Configuration 과 Logging

configuration은 JSON, environment variables, CLI args를 기본 Framework 표면으로 둔다.

namespace zlink::framework {

enum class optional_t {
 no = 0,
 yes = 1
};

class configuration_model_t {
public:
 configuration_model_t &set(std::string key, std::string value);
 bool contains(std::string_view key) const;
 bool has_section(std::string_view key) const;
 std::optional<std::string> get(std::string_view key) const;
};

class configuration_section_t {
public:
 configuration_section_t(
 const configuration_model_t &model,
 std::string prefix);
 std::string key() const;
 bool contains(std::string_view key) const;
 std::optional<std::string> get(std::string_view key) const;
 std::string require(std::string_view key) const;
};

template <typename T>
concept configuration_bindable =
 requires(const configuration_section_t &section) {
 { T::bind(section) } -> std::same_as<T>;
 };

class config_builder_t {
public:
 configuration_model_t &model() noexcept;
 const configuration_model_t &model() const noexcept;
 config_builder_t &load_json(std::string path);
 config_builder_t &load_json(std::string path, optional_t optional);
 config_builder_t &load_env(std::string prefix);
 config_builder_t &load_cli(int argc, char **argv);
 config_builder_t &use_environment(std::string name);
 std::string environment() const;
 bool is_environment(std::string_view name) const;
 configuration_section_t section(std::string prefix) const;

 template <configuration_bindable T>
 std::optional<T> bind(std::string prefix) const;

 template <configuration_bindable T>
 T bind_required(std::string prefix) const;
};

enum class log_level_t {
 trace = 0,
 debug = 1,
 info = 2,
 warn = 3,
 error = 4,
 critical = 5,
 off = 6
};
enum class logging_backend_t { builtin = 0, structured = 1 };
enum class logging_overflow_policy_t {
 drop_debug = 0,
 drop_oldest = 1,
 block = 2
};
struct log_field_t { std::string key; std::string value; };
struct log_record_t {
 log_level_t level = log_level_t::info;
 std::string category;
 std::string message;
 std::vector<log_field_t> fields;
 std::chrono::system_clock::time_point timestamp;
 std::thread::id thread_id;
};
struct logging_async_options_t {
 std::size_t queue_capacity = 8192;
 logging_overflow_policy_t overflow_policy =
 logging_overflow_policy_t::drop_debug;
};
struct rotating_file_options_t {
 std::size_t max_file_size = 10 * 1024 * 1024;
 std::size_t max_files = 5;
};

template <typename TCategory = void>
class logger_t {
public:
 logger_t() = default;
 bool is_enabled(log_level_t level) const noexcept;
 void log(log_level_t level,
 std::string message,
 std::initializer_list<log_field_t> fields = {}) const;
 void log_with_fields(log_level_t level,
 std::string message,
 std::vector<log_field_t> fields) const;
 void trace(std::string message,
 std::initializer_list<log_field_t> fields = {}) const;
 void debug(std::string message,
 std::initializer_list<log_field_t> fields = {}) const;
 void info(std::string message,
 std::initializer_list<log_field_t> fields = {}) const;
 void warn(std::string message,
 std::initializer_list<log_field_t> fields = {}) const;
 void error(std::string message,
 std::initializer_list<log_field_t> fields = {}) const;
 void critical(std::string message,
 std::initializer_list<log_field_t> fields = {}) const;
 const std::string &category() const noexcept;
};

class logger_factory_t {
public:
 logger_factory_t();
 logger_t<> create(std::string category) const;

 template <typename TCategory>
 logger_t<TCategory> create() const;
};

class logging_builder_t {
public:
 using sink_t = std::function<void(const log_record_t &)>;

 logging_builder_t &use_console();
 logging_builder_t &use_file(std::string path);
 logging_builder_t &use_rotating_file(
 std::string path,
 rotating_file_options_t options = {});
 logging_builder_t &use_callback_sink(sink_t sink);
 logging_builder_t &use_provider(std::string name, sink_t sink);
 logging_builder_t &use_async(logging_async_options_t options = {});
 logging_builder_t &use_backend(logging_backend_t backend);
 logging_builder_t &disable_record_capture();
 logging_builder_t &set_max_captured_records(std::size_t max);
 logging_builder_t &set_min_level(log_level_t level);
 logging_builder_t &set_level(std::string level);

 bool console_enabled() const noexcept;
 bool has_output_sink() const noexcept;
 bool async_enabled() const noexcept;
 logging_backend_t backend() const noexcept;
 log_level_t min_level() const noexcept;
 const std::string &level() const noexcept;
 const std::vector<std::string> &file_paths() const noexcept;
 const std::vector<std::string> &provider_names() const noexcept;
 const std::vector<log_record_t> &captured_records() const noexcept;
 logger_factory_t factory() const;
 logger_t<> create_logger(std::string category) const;
};

} // namespace zlink::framework

JSON loader는 nlohmann/json을 사용한다. YAML은 필요하면 configuration extension으로 둔다. Application은 표준 logging provider와 health 표면을 구성한다. Runtime event DTO, metric payload callback, exporter lifecycle과 provider 내부 registry는 public contract가 아니다.

5.1 Instance Spot metric

Framework가 표준 metric provider에 기록하는 Instance activation 계기는 다음 여섯 이름을 byte 단위로 그대로 사용한다. 종류, 단위, label과 닫힌 outcome 값은 Runtime metrics §4가 소유한다.

  • zlink.instance_spot.activations
  • zlink.instance_spot.activation.duration
  • zlink.instance_spot.pending.messages
  • zlink.instance_spot.pending.bytes
  • zlink.instance_spot.claim.conflicts
  • zlink.instance_spot.takeovers

One-way activation 실패는 별도 계기를 만들지 않고 zlink.mesh_node.messages.droppedsurface=instance_spot으로 기록한다. instance_spot_type label은 startup에 등록한 bounded type 이름만 사용하며 Spot ID, owner ID와 internal authority fields를 label로 사용하지 않는다.

5.2 message-flow dispatch error event

미등록 메시지와 dispatch 실패는 application이 구성한 표준 logger·telemetry provider에 event_id=zlink.dispatch_error, outcome=failed인 structured record로 기록한다. Channel별, Spot별 진단 provider 등록은 공개 계약에 포함하지 않는다. request 실패는 reply path가 있으면 error reply 로 끝나고, local actor call 처럼 reply frame 이 없는 경로는 task_t 또는 pending operation 을 Framework error로 완료한다. one-way 실패는 drop되지만 기본 structured log와 counter를 남긴다.

dispatch option declaration은 Monitoring interface가 소유한다.

Framework는 dispatch 오류와 message flow를 application이 구성한 표준 logger·trace·metric provider에 structured record로 기록한다. Callback observer, runtime error sink와 raw event DTO는 공개하지 않는다. Provider 실패는 원래 dispatch 결과와 terminal completion을 바꾸지 않고 별도 진단으로 격리한다.

시작 전 기본 mode는 configure_dispatch().message_flow(...)에서 정한다. 실행 중 mode 변경은 app_t::set_message_flow_mode(...)만 소유하며, channel이나 Spot별 toggle은 제공하지 않는다.

6. HTTP route와 middleware

HTTP hosting 시나리오는 60·61이 소유한다. 여기서는 public 타입만 고정한다.

enum class http_method_t { get, post, put, delete_ };

class http_route_t
{
public:
 http_method_t method;
 std::string path;
 std::string handler_name;
 bool context_response_precedence = false; // context가 만든 response를 우선한다
 bool validates_json_content_type = true; // JSON content type을 검증한다
};

struct http_tls_options_t {
 std::string certificate_file;
 std::string private_key_file;
};
struct http_endpoint_t { std::string uri; std::optional<http_tls_options_t> tls; };
struct http_server_options_t {
 std::size_t max_connections = 1024;
 std::size_t max_request_body_size = 1024 * 1024;
 std::size_t max_header_size = 64 * 1024;
 std::chrono::milliseconds request_headers_timeout{5000};
 std::chrono::milliseconds request_body_timeout{5000};
 std::chrono::milliseconds write_timeout{5000};
 std::chrono::milliseconds keep_alive_timeout{5000};
 std::chrono::milliseconds graceful_shutdown_timeout{5000};
 std::size_t max_keep_alive_requests = 100;
};
struct http_options_snapshot_t {
 std::vector<http_endpoint_t> endpoints;
 std::vector<http_route_t> routes;
 std::vector<std::string> middleware_names;
 http_server_options_t server;
 std::optional<std::string> health_path;
 std::optional<std::string> readiness_path;
 std::optional<std::string> liveness_path;
};

class http_tls_options_builder_t {
public:
 explicit http_tls_options_builder_t(http_tls_options_t &options) noexcept;
 http_tls_options_builder_t &certificate_file(std::string path);
 http_tls_options_builder_t &private_key_file(std::string path);
};

class http_server_options_builder_t {
public:
 explicit http_server_options_builder_t(
 http_server_options_t &options) noexcept;
 http_server_options_builder_t &set_max_connections(std::size_t value);
 http_server_options_builder_t &set_max_request_body_size(std::size_t bytes);
 http_server_options_builder_t &set_max_header_size(std::size_t bytes);
 http_server_options_builder_t &set_request_headers_timeout(
 std::chrono::milliseconds value);
 http_server_options_builder_t &set_request_body_timeout(
 std::chrono::milliseconds value);
 http_server_options_builder_t &set_write_timeout(
 std::chrono::milliseconds value);
 http_server_options_builder_t &set_keep_alive_timeout(
 std::chrono::milliseconds value);
 http_server_options_builder_t &set_graceful_shutdown_timeout(
 std::chrono::milliseconds value);
 http_server_options_builder_t &set_max_keep_alive_requests(
 std::size_t value);
};
  • middleware는 before/after 쌍이다. next delegate 방식이 아니다 — handler filter와 모양이 다르다.
  • middleware 인스턴스는 create_instance로 만들고 DI provider를 함께 받는다.

7. Transport

enum class transport_scheme_t {
 tcp = 0,
 ipc = 1,
 tls = 2,
 websocket = 3,
 websocket_tls = 4
};

class transport_endpoint_t
{
public:
 transport_endpoint_t (transport_scheme_t scheme, std::string uri);
 transport_scheme_t scheme() const noexcept;
 const std::string &uri() const noexcept;
 static transport_endpoint_t parse(std::string uri);
};

endpoint는 scheme과 URI를 함께 갖는다. scheme→transport 매핑의 의미는 Stream Connector §3이 소유한다.

8. 등록 builder

등록 표면은 builder 계층이다. 각 builder가 자기 역할의 설정만 소유한다.

정식 channel·MeshNode builder declaration은 Channel messaging이 소유한다. STREAM builder와 압축 option은 STREAM session, handler·codec·metadata builder는 이 문서의 configuration 등록 표면이 소유한다.

  • RouteMesh ChannelName과 classic fanout channel은 서로 다른 namespace와 socket 계약이다.
  • Spot·Actor 등록은 owner mesh_node_builder_t에 둔다.

drain 중 claim 진행의 의미는 Graceful Drain §5가 소유한다.

9. Configuration 조회

section은 prefix로 잘라낸 view다. 설정 소스를 계층으로 합치는 규칙은 01 §5가 소유한다.