콘텐츠로 이동

C++ Spot 언어별 interface

C++ 언어별 interface 목차

Bound Session의 relocation route 갱신은 Session–Actor binding §8.2가 소유한다.

1. Spot identity와 relocation 등록

User·Instance Spot factory configure callback은 disable_relocation(), recreate_on_relocation() 또는 preserve_state_with<TAdapter>() 중 하나를 명시한다.

namespace zlink::framework {

template <typename TSpot>
class spot_relocation_adapter_t {
public:
    virtual ~spot_relocation_adapter_t() = default;
    virtual task_t<std::vector<std::byte>> capture(
      TSpot &spot,
      std::stop_token operation_cancellation) = 0;
    virtual task_t<void> restore(
      TSpot &spot,
      std::vector<std::byte> payload,
      std::stop_token operation_cancellation) = 0;
};

template <typename TSpot>
class user_spot_factory_builder_t {
public:
    user_spot_factory_builder_t &set_stable_type_limit(std::int32_t limit);
    user_spot_factory_builder_t &set_execution_mode(user_spot_execution_mode_t mode);
    user_spot_factory_builder_t &set_relocation_coordination_mode(
      spot_relocation_coordination_mode_t mode);
    void disable_relocation();
    void recreate_on_relocation();
    template <typename TAdapter>
      requires std::derived_from<TAdapter, spot_relocation_adapter_t<TSpot>>
    void preserve_state_with();
};

template <typename TSpot>
class instance_spot_factory_builder_t {
public:
    instance_spot_factory_builder_t &set_stable_type_limit(std::int32_t limit);
    void disable_relocation();
    void recreate_on_relocation();
    template <typename TAdapter>
      requires std::derived_from<TAdapter, spot_relocation_adapter_t<TSpot>>
    void preserve_state_with();
};

} // namespace zlink::framework

preserve_state_with<TAdapter>()에서 TAdapterspot_relocation_adapter_t<TSpot>를 구현해야 한다. Actor adapter를 전달하거나 Spot factory에 맞지 않는 adapter를 전달하면 socket bind 전에 configuration error로 실패한다. Adapter는 application state를 opaque byte vector로만 주고받으며 typed state, 별도 contract identifier와 message wrapper를 노출하지 않는다.

Factory 등록 member의 선언은 Channel messagingmesh_node_builder_t가 소유한다.

2. Spot Framework API

Framework Spot 표면은 owner MeshNode와 zlink::framework::spot_t를 기반으로 한다.

namespace zlink::framework {

enum class spot_kind_t {
    invalid = 0,
    entry = 1,
    user = 2,
    instance = 3
};

enum class spot_close_reason_t {
    explicit_close = 0,
    host_shutdown = 1,
    relocation_out = 2,
    idle_evicted = 3
};

struct spot_closing_context_t final {
    spot_close_reason_t reason;
    std::chrono::system_clock::time_point deadline;
};

using spot_id_t = std::string;

class spot_ref_t final {
public:
    spot_ref_t(spot_id_t spot_id,
      std::uint64_t object_generation,
      std::string mesh_name,
      node_rid_t node_rid);

    const spot_id_t &spot_id() const noexcept;
    std::uint64_t object_generation() const noexcept;
    std::string_view mesh_name() const noexcept;
    const node_rid_t &node_rid() const noexcept;
};

class spot_context_t;
class entry_spot_context_t;
class instance_spot_context_t;
class spot_handler_registry_t;
class instance_spot_handler_registry_t;
struct spot_actor_join_result_t;
struct actor_create_response_t;
struct spot_create_response_t;

enum class spot_relocation_ready_outcome_t : std::uint8_t {
    continued = 0,
    relocated = 1,
};

struct spot_relocation_ready_completion_t {
    spot_relocation_ready_outcome_t outcome;
};

class spot_relocation_ready_call_t {
public:
    spot_relocation_ready_call_t(spot_relocation_ready_call_t &&) noexcept;
    spot_relocation_ready_call_t(const spot_relocation_ready_call_t &) = delete;
    void defer();
};

template <typename TActor>
class spot_t {
public:
    using actor_type = TActor;

    virtual ~spot_t() = default;
    virtual spot_context_t &context() noexcept = 0;
    virtual const spot_context_t &context() const noexcept = 0;
    virtual void configure() = 0;
    virtual task_t<spot_create_response_t> on_create(
      const message_t &request);
    virtual task_t<void> on_initialize();
    virtual task_t<void> on_closing(
      const spot_closing_context_t &context,
      std::stop_token cleanup_cancellation);
    virtual task_t<void> on_relocation_ready_completed(
      const spot_relocation_ready_completion_t &completion);
    virtual task_t<spot_actor_join_result_t> on_actor_join(
      std::string_view actor_id,
      const message_t &request) = 0;
    virtual task_t<void> on_actor_joined(TActor &actor) = 0;
    virtual task_t<void> on_leave_actor(TActor &actor) = 0;
    virtual task_t<void> on_disconnect_actor(TActor &actor);
};

template <typename TActor>
class entry_spot_t {
public:
    using actor_type = TActor;

    virtual ~entry_spot_t() = default;
    virtual entry_spot_context_t &context() noexcept = 0;
    virtual const entry_spot_context_t &context() const noexcept = 0;
    virtual void configure() = 0;
    virtual task_t<void> on_initialize();
    virtual task_t<void> on_closing(
      const spot_closing_context_t &context,
      std::stop_token cleanup_cancellation);
    virtual task_t<actor_create_response_t> on_create_actor(
      TActor &actor,
      const message_t &create_request);
    virtual task_t<spot_actor_join_result_t> on_actor_join(
      std::string_view actor_id,
      const message_t &request) = 0;
    virtual task_t<void> on_actor_joined(TActor &actor) = 0;
    virtual task_t<void> on_leave_actor(TActor &actor) = 0;
    virtual task_t<void> on_disconnect_actor(TActor &actor);
};

class instance_spot_t {
public:
    virtual ~instance_spot_t() = default;
    virtual instance_spot_context_t &context() noexcept = 0;
    virtual const instance_spot_context_t &context() const noexcept = 0;
    virtual void configure() = 0;
    virtual task_t<void> on_initialize();
    virtual task_t<void> on_closing(
      const spot_closing_context_t &context,
      std::stop_token cleanup_cancellation);
};

class spot_common_context_t {
public:
    std::string_view mesh_name() const;
    node_rid_t node_rid() const;
    spot_id_t spot_id() const;
    std::uint64_t object_generation() const noexcept;
    channel_client_t outbound() const;

    template <typename TCommand>
    spot_send_call_t send_to_spot(spot_id_t target, TCommand command);

    template <typename TRequest>
    spot_request_call_t request_to_spot(
      spot_id_t target,
      TRequest request);

    template <typename TEvent>
    publish_call_t publish(
      std::string channel_name,
      std::string topic,
      TEvent event);

    template <typename THandler>
    timer_t add_timer(std::string name,
      std::chrono::milliseconds period,
      timer_options_t options = {});

    template <typename TWork>
    auto run_cpu_worker(TWork work);

    template <typename TWork>
    auto run_io_worker(TWork work);

};

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

    spot_handler_registry_t handlers();
    spot_relocation_ready_call_t relocation_ready();

    template <typename TActor>
    task_t<void> leave_actor(TActor &actor);

    task_t<bool> close();
};

class entry_spot_context_t : public spot_common_context_t {
public:
    ~entry_spot_context_t();
    entry_spot_context_t(entry_spot_context_t &&) noexcept;
    entry_spot_context_t &operator=(entry_spot_context_t &&) = delete;
    entry_spot_context_t(const entry_spot_context_t &) = delete;
    entry_spot_context_t &operator=(const entry_spot_context_t &) = delete;
    spot_handler_registry_t handlers();

    template <typename TActor>
    task_t<void> destroy_actor(TActor &actor);

    task_t<void> destroy_actor(const actor_ref_t &actor);
};

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

    instance_spot_handler_registry_t handlers();
    task_t<bool> close();
};

struct spot_actor_join_result_t {
    bool accepted = false;
    std::optional<zlink::framework::message_t> reply;

    static spot_actor_join_result_t accept(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static spot_actor_join_result_t accept(TReply reply);

    static spot_actor_join_result_t reject(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static spot_actor_join_result_t reject(TReply reply);
};

struct actor_create_response_t {
    bool accepted = true;
    std::optional<zlink::framework::message_t> reply;

    static actor_create_response_t accept(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static actor_create_response_t accept(TReply reply);

    static actor_create_response_t reject(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static actor_create_response_t reject(TReply reply);
};

enum class spot_create_state_t {
    existing = 0,
    created = 1,
    rejected = 2
};

struct spot_create_response_t {
    bool accepted = true;
    std::optional<zlink::framework::message_t> reply;

    static spot_create_response_t accept(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static spot_create_response_t accept(TReply reply);

    static spot_create_response_t reject(
      std::optional<message_t> reply = std::nullopt);

    template <typename TReply>
    static spot_create_response_t reject(TReply reply);
};

struct spot_create_result_t {
    spot_ref_t spot;
    spot_create_state_t state = spot_create_state_t::created;
    std::optional<zlink::framework::message_t> reply;
};

class spot_handler_registry_t {
public:
    template <auto Method>
    spot_handler_registry_t &add_handler(std::string packet_name = {});

    template <auto Method>
    spot_handler_registry_t &add_subscribe(
      std::string channel_name,
      std::string topic);

    template <auto Method>
    spot_handler_registry_t &add_actor_send(std::string packet_name = {});

    template <auto Method>
    spot_handler_registry_t &add_actor_request(std::string packet_name = {});
};

class instance_spot_handler_registry_t {
public:
    template <auto Method>
    instance_spot_handler_registry_t &add_handler(
      std::string packet_name = {});
};

class bound_session_t {
public:
    template <typename TMessage>
    bound_session_send_call_t send(const TMessage &message);

    task_t<void> disconnect();
};

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

    actor_join_call_t &timeout(std::chrono::milliseconds timeout);

    // 현재 handler가 정상 종료한 뒤 Join을 시작하도록 등록한다.
    void defer();
};

class actor_context_t {
public:
    const actor_ref_t &actor_ref() const noexcept;
    const actor_id_t &actor_id() const noexcept;
    std::uint64_t object_generation() const noexcept;
    std::string_view mesh_name() const noexcept;
    std::optional<spot_id_t> spot_id() const;
    bound_session_t bound_session() const;

    actor_join_call_t join_spot(spot_id_t spot_id);

    actor_join_call_t join_spot(spot_id_t spot_id,
      const zlink::framework::message_t &request);

    actor_join_call_t join_entry_spot();

    actor_join_call_t join_entry_spot(
      const zlink::framework::message_t &request);

    template <typename TRequest>
    actor_join_call_t join_spot(spot_id_t spot_id,
      const TRequest &request);

    template <typename TRequest>
    actor_join_call_t join_entry_spot(const TRequest &request);
};

} // namespace zlink::framework

spot_context_t::publish(...)는 target ChannelName과 topic을 함께 받는다. publish는 MeshNode ROUTER를 통해 remote MeshNode마다 한 번 제출하고, 수신 node는 node-local subscription만 검사한다. 각 remote ROUTER와 local mailbox는 대상별로 수락하며, 한 대상의 실패가 앞에서 수락된 전송을 취소하지 않는다. Spot·Actor 등록은 owner mesh_node_builder_t에 속한다.

mesh_node_socket_config_t는 RouteMesh SS의 Framework-level message-size 설정을 제공하지 않는다. Sender와 receiver는 Framework-level complete-message 상한으로 message를 거부하지 않는다. Transport와 service-wire 표현 한계, HWM과 mailbox budget은 별도 자원·wire guard로 유지한다. send_timeout을 지정하지 않으면 framework 기본값 1초를 사용한다. receive_timeout을 지정하지 않으면 수신 대기 상한을 따로 두지 않는다. HWM은 0 이상이어야 한다.

Spot Actor Join / Relocation 관련 interface도 이 문서에 기록된 정식 계약이며, 그 동작 의미는 공통 스펙을 따른다. 구현이나 contract test가 이 시그니처와 다르면 계약 불일치로 처리한다. join_entry_spot(...)은 target node RID를 받지 않으며 Framework가 현재 eligible Entry Spot을 선택한다.

spot_close_reason_t의 값은 explicit_close=0, host_shutdown=1, relocation_out=2, idle_evicted=3다. idle_evictedInstance Spot 전용 이유이며 Entry Spot과 User Spot에는 전달하지 않는다. 유휴 판정 조건과 정리 뒤 재활성화 규칙은 Spot 모델 §6.2가 소유한다. Context의 deadline은 closing operation의 absolute UTC time이다. Framework는 callback invocation 전에는 cleanup_cancellation에 stop을 요청하지 않고 deadline이 끝날 때 요청한다. Entry·User·Instance Spot만 callback을 받고 Actor별 closing callback은 제공하지 않는다. Host Shutdown은 Actor membership과 local instance가 유효한 상태에서 callback을 실행하고 completion 뒤 scope와 authority를 정리한다. Standalone Actor relocation은 Entry Spot을 닫지 않으므로 이 callback을 호출하지 않는다.

entry_spot_context_t::destroy_actor(...)는 Entry Spot에서만 호출한다. user Spot에 있는 Actor는 먼저 leave_actor(...) 또는 Entry Spot join을 완료해야 한다. Destroy는 membership 이동이 아니므로 on_leave_actor를 다시 호출하지 않으며, 같은 Actor instance의 중복 destroy는 lifecycle callback을 추가로 실행하지 않고 성공으로 끝난다. 전체 순서는 Actor model §6을 따른다.

Actor와 User·Instance Spot의 cross-node materialization 동작은 factory 등록에 연결한 factory builder가 정한다. 별도 relocation adapter registry나 operation별 adapter는 제공하지 않는다. preserve_state_with<TAdapter>()를 선택한 Spot factory만 spot_relocation_adapter_t<TSpot>로 Spot application state를 capture·restore한다. Whole User Spot relocation은 Spot root에 Spot adapter를 사용하고 각 Actor participant에는 actor_relocation_adapter_t<TActor>를 사용한다. Same-node operation과 disable_relocation()· recreate_on_relocation()은 Spot adapter를 호출하지 않는다. disable_relocation()을 선택한 cross-node operation은 capture 전에 거부한다.

Spot adapter의 capture(...) 결과에는 relocation adapter 전용 size 상한이 없으며 빈 vector는 유효하다. Framework는 payload를 Relocation Store에 기록하지 않고, source memory에서 relocation_payload_chunk_limit_bytes 이하의 chunk로 나눠 source–target ordered mesh 연결로 직접 전송한다. 반환한 vector의 소유권은 Framework로 이동하고 restore(...)에 전달한 vector는 해당 비동기 호출이 소유한다. Capture가 throw하거나 failed task로 끝나면 durable abort와 source normalization 뒤 admission을 복원한다. Restore가 실패한 instance는 폐기하고 새 attempt의 factory가 만든 instance에 같은 immutable payload를 적용한다. Framework가 operation deadline 때문에 callback을 취소하면 deadline_exceeded로 분류한다. Recovery 때문에 두 method가 at-least-once 호출되거나 stale attempt와 successor에서 겹칠 수 있으므로 adapter는 retry-safe해야 한다. Framework는 adapter의 external side effect에 exactly-once를 보장하지 않는다.

C++의 일반 Spot packet과 Actor payload handler는 spot_context_t::handlers()가 등록한다. Actor handler는 containing Spot의 member이며 호출 대상 Spot instance를 this로 사용하고, mutable Actor, 읽기 전용 message_context_t와 payload를 인자로 받는다. 다른 Spot의 상태를 바꾸는 message는 global spot_id_t direct call로 제출한다. Actor lifecycle은 registry 등록 표면이 아니다. 별도 Actor handler object를 만들지 않으므로 Actor별 mutable state는 Actor가 소유한다. Entry Spot과 PerActor User Spot의 Spot member function은 서로 다른 Actor에서 동시에 호출될 수 있으며 Spot field에 Actor별 state를 저장하면 안 된다. Actor별 실행 resource는 Actor activation에 귀속하고 Same-node Join에서는 유지하며 cross-node Join과 relocation 뒤 target activation에서 다시 만든다. 별도 class인 timer handler는 Spot activation마다 한 번 만들고 재사용한다. Timer handler 생성자의 dependency 매개 변수도 같은 Spot activation scope에서 resolve한다. Spot close와 source relocation에서는 handler와 scope를 정리한다. Target activation에서는 새 handler와 scope를 만든다. Application이 timer handler를 singleton·scoped·transient service로 따로 등록하거나 lifetime을 선택하지 않는다. Public handler lifetime option은 제공하지 않는다. User Spot과 Entry Spot은 actor ID와 join request를 받는 on_actor_join(...)에서 accept 또는 reject를 반환한다. Commit 이후 callback은 해당 factory가 만든 concrete Actor reference를 직접 받는다. 따라서 별도 membership DTO를 lifecycle callback에 끼워 넣지 않는다. Joined, leave와 disconnect callback은 task_t<void>를 반환하며 task가 완료되어야 lifecycle callback이 완료된 것으로 본다. SpotWide User Spot 또는 Instance Spot의 callback 안에서 channel 왕복을 기다릴 때 yield()를 사용하면 shared Spot turn을 반환하고, 응답 뒤 같은 Spot 실행 queue에서 새 turn으로 callback을 재개한다. Entry Spot과 PerActor User Spot에서는 yield()를 사용할 수 없다. 일반 Spot 타입은 concrete Actor type을 지정한 zlink::framework::spot_t<TActor>를 상속해야 하고, Entry Spot 타입은 zlink::framework::entry_spot_t<TActor>를 상속해야 한다. 두 base class가 lifecycle callback의 virtual contract를 고정하며, add_spot<TSpot>()add_entry_spot<TEntrySpot>()가 이 계약을 compile-time으로 확인한다. 이름이나 파일 위치와 method 존재 여부만으로 역할을 추론하지 않는다.

Instance Spot은 instance_spot_t를 상속하며 Actor callback을 갖지 않는다. Application instance는 Framework가 생성할 때 결합한 instance_spot_context_tcontext()로 노출한다. Framework는 인자 없는 configure(), message를 받지 않는 on_initialize(), on_closing(context, cleanup_cancellation)을 actor-free lifecycle로 사용한다. configure(...)에서는 direct packet과 timer handler만 등록할 수 있다. Instance context의 전용 registry에는 Actor handler와 Logical Multicast subscription 등록 member가 존재하지 않는다. 같은 MeshNode에서 stable instance_spot_type이나 같은 Spot class를 User Spot factory와 Instance factory에 중복 등록해도 socket bind 전에 설정 오류로 실패한다.

spot_send_call_tspot_request_call_tinstance_spot()은 type을 생략한 marker이고, instance_spot(std::string stable_type)은 type을 지정한 marker다. in_mesh(std::string mesh_name)은 Mesh 입력을 표현한다. Send terminal은 async()task_t<void>, request terminal은 async<TReply>()·yield<TReply>()task_t<TReply>다. 정확한 call member는 Channel messaging가 소유한다.

Cold activation의 type·Mesh 선택, 생성 순서와 최초 message 보존은 Spot address messaging §4가 소유한다. 완료 경계는 Spot address messaging §5를 따른다.

Close에서는 on_closing(context, cleanup_cancellation)을 한 번 호출하고 fencing 조건을 만족하는 location row만 해제한다.

SpotWide User Spot과 member Actor의 relocation은 generic aggregate로 처리한다. Active membership이 있다는 이유만으로 host relocation을 차단하지 않으며 aggregate owner와 membership을 한 commit에서 전환한다. PerActor User Spot은 Spot authority를 먼저 전환하고 Actor를 각각 이전한다. spot_context_t::close()instance_spot_context_t::close()는 context가 보유한 current SpotRef를 사용한다.

일반 User Spot close는 active Actor membership이 하나라도 있으면 false로 끝나고 admission과 authority를 유지한다. Caller가 member Actor의 leave 또는 destroy를 완료한 뒤에만 close할 수 있으며, Framework가 close를 위해 Actor를 숨겨서 이동하거나 제거하지 않는다. Host relocation에서 SpotWide User Spot은 current member Actor 전체를 하나의 aggregate로 이전하며 participant 총수에 고정 상한을 두지 않는다. PerActor User Spot은 stateless shell만 새 target에 다시 만들고 Actor를 독립된 unit으로 이전한다.

Stored creation intent의 재개 범위와 steady Ready owner 실패의 구분은 Object lifecycle §3가 소유한다.

spot_ref_t는 global SpotId, 1..9223372036854775807 범위의 ObjectGeneration과 조회 시점 MeshName·NodeRid를 담은 immutable location snapshot이다. 일반 message target으로 사용하지 않으며 별도 handle, resolver와 address type은 제공하지 않는다. SpotId와 stable type은 UTF-8 1..255 byte 값이며 trim, case folding과 Unicode normalization을 적용하지 않는다. spot_id_t는 UTF-8 encoded 크기 1..255 bytes의 case-sensitive std::string이다.

class player_actor_t;

class bingo_room_spot_t : public zlink::framework::spot_t<player_actor_t>,
                          public bingo_room_t {
public:
    explicit bingo_room_spot_t(
      zlink::framework::spot_context_t context)
      : context_(std::move(context)) {}

    zlink::framework::spot_context_t &context() noexcept override {
        return context_;
    }

    const zlink::framework::spot_context_t &context()
      const noexcept override {
        return context_;
    }

    void configure() override;

    zlink::framework::task_t<zlink::framework::spot_actor_join_result_t>
    on_actor_join(
      std::string_view actor_id,
      const zlink::framework::message_t &request) override;

    zlink::framework::task_t<void> on_actor_joined(
      player_actor_t &actor) override;

    zlink::framework::task_t<void> on_leave_actor(
      player_actor_t &actor) override;

    zlink::framework::task_t<void> on_disconnect_actor(
      player_actor_t &actor) override;

    start_bingo_game_res_t start_game(
      player_actor_t &actor,
      const zlink::framework::message_context_t &message_context,
      const start_bingo_game_req_t &request);

private:
    zlink::framework::spot_context_t context_;
};

class player_actor_t : public zlink::framework::actor_t {
};

class bingo_entry_spot_t
  : public zlink::framework::entry_spot_t<player_actor_t> {
public:
    explicit bingo_entry_spot_t(
      zlink::framework::entry_spot_context_t context)
      : context_(std::move(context)) {}

    zlink::framework::entry_spot_context_t &context() noexcept override {
        return context_;
    }

    const zlink::framework::entry_spot_context_t &context()
      const noexcept override {
        return context_;
    }

    void configure() override;

private:
    zlink::framework::entry_spot_context_t context_;
};

세 Context는 Framework만 만들 수 있는 move-only handle이다. Factory는 전달받은 handle을 application object의 read-only member로 move하고 context()에서 그 handle을 반환한다. Default construction, copy와 assignment로 identity를 만들거나 교체할 수 없다.

route_mesh_runtime_options_t는 public DI singleton이다. 등록되지 않은 ChannelName을 조회하면 구성 오류로 실패한다. 실행 중에는 ChannelName weight만 변경할 수 있다. 최대 메시지 크기는 startup 뒤 변경할 수 없다. Weight는 0부터 10000까지이고 기본값은 100이다. 범위 밖 값은 startup 설정과 runtime 변경에서 configuration error다. 0은 해당 membership을 새 select-one과 Logical Multicast remote target에서 제외한다.

Spot과 Entry Spot은 activation scope가 수명을 소유한다. 기본 생성 가능한 타입은 타입만 등록한다. 생성자 의존성이 있거나 application이 생성 방법을 결정해야 하는 타입은 factory overload로 등록한다. factory는 Spot을 활성화할 때 framework가 호출하며, 반환한 instance의 수명도 같은 activation scope에서 관리한다.

일반 Spot packet member는 payload와 message_context_t를 받고, subscription member는 payload와 publish_message_context_t를 받는다. actor join admission을 처리하는 member는 std::string_view actor_idzlink::framework::message_t request를 받으며, spot_actor_join_result_t로 accepted 여부와 optional reply zlink::framework::message_t를 반환한다. actor type과 source/target Spot 및 node 정보는 framework 내부 routing과 검증에만 사용한다. accepted가 true일 때만 actor 위치를 user Spot으로 commit하고 on_actor_joined(TActor&)를 호출한다. accepted가 false이면 actor 위치를 바꾸지 않고 post-joined callback도 호출하지 않는다. Commit 이후 결과는 callback 이름으로 구분한다. Maintenance가 Actor를 target Entry Spot에 materialize할 때 Snapshot은 Actor adapter restore(...)를 먼저 완료하고 Recreate는 payload restore 없이 factory materialization을 완료한다. 그 다음 queue·Actor timer를 복원하고 Location authority·Entry membership을 commit한 뒤 Actor message 처리를 시작한다. Bound Session의 relocation route 갱신은 Session–Actor binding §8.2가 소유한다. Infrastructure relocation은 target joined, source leave 또는 별도 relocation callback을 호출하지 않는다.

일반 same-node·remote User·Entry Spot join만 기존 on_actor_join(...), on_actor_joined(...)와 source on_leave_actor(...) 계약을 사용한다. SpotWide User Spot aggregate와 PerActor User Spot의 Actor relocation에서는 member Actor의 membership callback을 호출하지 않는다. PerActor Spot policy는 RecreateOnRelocation만 허용하고 Spot adapter를 등록하지 않는다. Spot field와 Spot-level schedule은 이전하지 않는다. 유지해야 하는 shared state와 schedule은 application의 Redis·database·service 같은 외부 저장소에 둔다. Target runtime-private shell은 같은 public Spot ID와 object generation을 사용하며 Spot authority 전에는 public lookup에 노출하지 않는다. Authority 전환 뒤 ToSpot, Create와 Join은 target, ToActor는 Actor별 current owner를 사용한다. Stale source route는 operation identity, generation, deadline, correlation과 reply route를 보존해 relay한다. Actor queue seal부터 one-way cutover submit의 성공 또는 실패 terminal까지 source-local 1초는 운영 목표이며 초과해도 relocation을 취소하거나 rollback하지 않는다.

spot_context_t::relocation_ready().defer()spot_wideapplication_signaled을 함께 등록한 Spot turn에서만 유효하다. Framework는 이동하지 않았거나 relay-ready reply가 accepted 상태가 되기 전에 abort했으면 source에서 continued, 이동했으면 target에서 relocated completion을 on_relocation_ready_completed(...)에 전달한다. 기본 virtual 구현은 no-op이다. Callback 완료 전에는 보류한 application message와 timer를 실행하지 않는다.

기본 framework_managed, per_actor, Entry·Instance Spot, Spot turn 밖과 같은 turn의 중복 defer()는 queue mutation 전에 invalid_operation으로 실패한다. defer() 뒤 같은 turn의 다른 Framework operation도 같은 오류다. Recovery에서 callback이 다시 실행될 수 있으므로 override는 retry-safe해야 한다. actor packet member는 containing Spot에 선언하며 mutable Actor, message_context_t, DTO 순서로 받는다. 호출 대상인 containing Spot은 member function의 this다. actor disconnected callback도 같은 concrete Actor reference를 받는다. Runtime의 private dispatch가 message_t를 DTO로 바꾸고 현재 Spot instance와 Actor를 찾아 typed member function을 호출한다. Application은 invoker, service provider, serializer registry와 descriptor 조회 표면을 받지 않는다. 샘플도 public registration과 call 경로를 통과해야 framework 동작을 확인했다고 볼 수 있다. Entry Spot membership 상태에서도 actor packet은 일반 Spot packet으로 등록하지 않는다. spot_context_t::handlers()에서 add_actor_request<Method>() 또는 add_actor_send<Method>()로 등록하며, member는 mutable Actor, message context와 DTO를 받는다. stream header metadata 전체를 actor handler에 그대로 노출하지 않는다. 사용자는 options.metadata().allow_session_to_actor("trace-id")처럼 application metadata forwarding 정책을 선언하고, framework는 허용된 key만 message_context_t::metadata에 넣는다. handler는 metadata.find(...) 또는 metadata.contains(...)로 값을 조회한다. values()는 단순 반복을 제공하고, find(...)contains(...)는 handler code가 std::map 구조에 직접 묶이지 않게 한다. 빈 metadata key와 공백만 있는 key는 의미가 모호하므로 두 방향의 allowlist method 모두 이런 key를 거부한다. 이 정책은 stream frame 구조나 ActorGateway 내부 frame을 public handler 표면에 드러내지 않기 위한 경계다.

timer_t는 Framework timer registration의 lifetime과 취소를 표현하는 public handle이며 callback은 owner Spot의 직렬 실행 queue에 제출된다. Entry Spot timer도 서로 다른 Entry Spot instance를 전역 직렬화하지 않는다.

Timer backend 선택은 비동기 실행 정책을 따른다. timer_tick_t는 공통 timer dispatch metadata만 제공한다.

ActorGateway session relay의 public 표면은 session_actor_manager_t, session_actor_t, actor_context_t, bound_session_t다. MeshNode transport metadata는 이 표면에 노출하지 않는다. actor context의 join_spot(...) request는 DTO 또는 zlink::framework::message_t다. JSON DTO는 기본 serializer를 사용하므로 message type별 codec 설정이 필요 없다. Protobuf, MessagePack, custom binary payload처럼 기본 JSON으로 표현할 수 없는 타입만 startup/options 에 serializer extension을 연결하고 업무 코드는 같은 join 호출을 유지한다. join 결과는 actor_t::on_join_completed(...)에 전달하는 승인과 거절 variant다. 승인 값만 이동 이후의 actor ref를 가지며 두 값 모두 application reply zlink::framework::message_t를 담는다. Entry Spot join도 같은 completion 타입을 사용한다. request를 생략한 overload는 빈 message_t를 사용한다. raw payload 처리는 framework 내부 invoker가 맡으며 application public actor context에 별도 raw join overload를 두지 않는다.

호출 실행 표면은 공통 비동기 call 계약을 C++ coroutine 관례로 표현한다. request(...), send(...), join_spot(...)join_entry_spot(...)은 call object를 반환한다. One-way call의 async()은 send timeout까지 bounded admission 결과를 담은 task_t를 반환한다. Session Actor relay(...)는 별도 call object를 만들지 않고 정상 완료 값을 만들지 않는 task_t<void>를 반환한다. Request는 async()이 reply 완료를 기다리는 지점이다. 일반 channel request_call_t는 metadata와 request timeout을, send_call_t는 metadata만 submit 전에 모으고, submit 시점에 framework envelope 정책으로 넘긴다. typed packet name은 registration descriptor가 결정한다.

Actor Join은 다른 messaging call과 실행 경계가 다르다. defer()는 target을 조회하거나 Store I/O를 시작하지 않고 현재 handler에 Join intent와 비활성 queue barrier만 등록한다. handler가 정상 종료하면 barrier를 활성화하여 Join을 시작하고, handler가 실패하면 등록 내용을 폐기한다. defer()는 값을 반환하지 않으며 async()yield() terminal을 제공하지 않는다. Join 결과는 나중에 actor_t::on_join_completed(...)로 알린다. 기본 timeout은 5초이고, 명시하는 값은 millisecond 기준 1..INT_MAX 범위여야 한다. Framework는 defer() 시점의 monotonic clock으로 absolute deadline을 고정한다.

yield()SpotWide User Spot 또는 Instance Spot의 shared turn에서만 그 turn을 반환한다. 그 밖의 문맥에서는 operation을 제출하거나 turn을 반환하지 않고 invalid_operation으로 완료한다. Worker call도 같은 실행 문맥 제한을 적용한다. CPU worker는 동기 작업, I/O worker는 task_t<TResult>를 반환하는 작업을 받는다. 한 call object에서 terminator를 두 번 시작하면 protocol error로 완료한다.

auto reply = co_await client
  .request("profile", query) // ChannelName만으로 호출 대상을 선택한다.
  .async<profile_reply_t>();

use_profile(reply);

public framework async 표면에 std::future를 사용하지 않는다. blocking wait는 handler, timer, STREAM session callback, actor relay 경로에서 허용하지 않는다.

오류 종류는 .NET framework의 ZLinkFrameworkErrorKind를 C++ naming으로 투영한다. async()은 실패 시 같은 정보를 가진 framework_exception_t를 throw한다.

3. Timer

enum class timer_overrun_policy_t {
    skip_late_ticks = 0,
    catch_up_bounded = 1,
    delay_next_tick = 2
};

struct timer_options_t {
    timer_overrun_policy_t overrun_policy =
      timer_overrun_policy_t::skip_late_ticks;
    std::uint64_t max_catch_up_ticks = 1;
    bool stop_on_unhandled_exception = false;
};

struct timer_tick_t {
    std::string name;
    std::uint64_t delivery_index = 0;
    std::uint64_t scheduled_index = 0;
    std::chrono::milliseconds period{0};
    std::chrono::milliseconds scheduled_elapsed{0};
    std::chrono::milliseconds started_elapsed{0};
    std::chrono::milliseconds delay{0};
    std::uint64_t skipped_ticks = 0;
};

struct timer_failure_event_t {
    std::string timer_name;
    std::type_index handler_type;
    std::uint64_t delivery_index = 0;
    bool stopped = false;
    std::string message;
};

class timer_t {
public:
    timer_t();
    ~timer_t();
    timer_t(timer_t &&) noexcept;
    timer_t &operator=(timer_t &&) noexcept;
    timer_t(const timer_t &) = default;
    timer_t &operator=(const timer_t &) = default;

    bool is_disposed() const noexcept;
    void cancel() noexcept;
};

Timer option을 생략하면 overrun_policyskip_late_ticks, max_catch_up_ticks1이다. max_catch_up_ticksoverrun_policy == catch_up_bounded일 때만 사용하고 1..INT_MAX 범위인지 검증한다. 다른 policy에서는 이 값을 사용하지 않으며 이 범위로 validation하지 않는다. 이 규칙은 기존 timer_options_t field를 해석하는 계약이며 새 public member를 추가하지 않는다.

timer 등록 검증은 stage-wrapper §4.1이 소유한다.

Framework timer는 owner Actor·Spot에 속한 logical registration이다. Cross-node relocation에서는 timer 이름, handler type, period, timer_options_t, scheduling cursor와 seal 시점의 pending tick을 relocation payload에 자동으로 포함한다. Application의 relocation adapter는 timer를 capture·restore하거나 target에서 다시 등록하지 않는다. Target runtime은 payload의 logical registration으로 timer를 복원한다. Source는 queue를 seal한 뒤 새 tick을 dispatch하지 않으며 target은 restore와 authority commit을 마치고 dispatch admission이 열린 뒤에만 복원한 pending tick과 다음 tick을 owner mailbox에 제출한다.

4. SPOT 표면

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

    spot_create_call_t &in_mesh(std::string mesh_name);
    spot_create_call_t &creation_request(message_t request);

    template <typename TRequest>
    spot_create_call_t &creation_request(TRequest request);

    spot_create_call_t &timeout(std::chrono::milliseconds timeout);
    task_t<spot_create_result_t> async();
    task_t<spot_create_result_t> yield();
};

class spot_manager_t {
public:
    virtual ~spot_manager_t() = default;
    virtual spot_create_call_t create(std::string stable_type) = 0;
    virtual spot_create_call_t get_or_create(
      spot_id_t spot_id,
      std::string stable_type) = 0;

    virtual task_t<std::optional<spot_ref_t>> find(spot_id_t spot_id) = 0;
    virtual task_t<bool> close(spot_ref_t spot) = 0;
};

spot_manager_t는 User Spot만 생성한다. Create는 Framework가 global SpotId를 생성하고, GetOrCreate는 caller가 제공한 global SpotId를 사용한다. Instance Spot create/get-or-create member와 kind 인자는 제공하지 않는다. Call option과 async() terminal은 각각 한 번만 사용할 수 있다. Existing authority가 Instance kind이거나 stable type이 다르면 type_mismatch, eligible capacity가 없으면 capacity_exceeded다. Terminal async()spot_ref_t, existing·created·rejected state와 creation callback reply를 spot_create_result_t 하나로 반환한다.

Find는 current Ready User SpotRef만 반환하고 생성하지 않는다. Instance authority는 manager의 Find 결과에 포함하지 않는다. Close는 User Spot의 SpotRef만 변경한다. Instance Spot은 instance_spot_context_t::close()가 context에 보관한 current SpotRef로 local close를 수행한다. 같은 User Spot incarnation이 없으면 manager Closefalse, 다른 generation이면 invalid_operation, 이동 중이면 unavailable이다. Public list, resolver와 handle은 제공하지 않는다.

5. Public trace category

이 문서의 declaration은 public trace의 spot-instanceactor-relocation category에 속한다. 공통 의미는 Spot address와 messagingSpot·Actor membership이 소유한다.

lifecycle callback의 호출 순서는 MeshNode §7가 소유한다 — handler 구성 → 생성 callback → 수락된 경우에만 초기화 → 종료는 한 번.