C++ Location · Relocation Store · Redis Per-Language Interface¶
C++ per-language interface table of contents · Location Runtime · Redis Location Store
This document fixes the minimal public SPI an external provider implements, the location option and operational query an application uses, and the public declaration of the official Redis extension. Authority, owner lease, reservation, capacity, aggregate, and the relocation state machine are encoded by the Framework as opaque records. The provider doesn't know that domain type or processing stage.
The Store primitive and abstract Store class are owned by the opt-in
CMake target zlink::framework_provider_abstractions. A provider
implementation can implement the Store by linking only this target,
without depending on an Actor/Spot application target. The C++
namespace keeps the existing zlink::framework.
1. Root Option And Registration¶
namespace zlink::framework {
struct location_options_t {
std::chrono::milliseconds owner_lease_renew_interval{5000};
std::chrono::milliseconds owner_lease_ttl{15000};
std::chrono::milliseconds polling_interval{1000};
std::chrono::milliseconds store_failure_grace{30000};
std::chrono::milliseconds owner_lease_fencing_margin{5000};
std::chrono::milliseconds owner_lease_renew_timeout{3000};
std::chrono::milliseconds route_cache_max_age{15000};
std::chrono::milliseconds message_follow_duration{30000};
std::chrono::milliseconds session_relocation_seal_timeout{3000};
std::chrono::milliseconds relocation_cutover_wait_timeout{1000};
std::uint64_t relocation_payload_chunk_limit_bytes{262144};
std::uint64_t relocation_in_flight_payload_budget_bytes{16777216};
std::uint64_t relocation_node_in_flight_payload_budget_bytes{0};
};
} // namespace zlink::framework
The Store registration member is owned by
Configuration and host's
zlink_framework_options_t. The application registers each of the two
capabilities once with the existing add_location_store(...) and
add_relocation_store(...). A use_* member of the same meaning, or a
Redis-specific registration helper, isn't provided.
Once registration succeeds, the Framework takes over the Store
instance's logical lifetime. After registration, the caller doesn't
call or replace the Store directly. The std::shared_ptr the Framework
holds is released only after ending the dependent runtime and
in-progress operations first. If two Stores share a connection,
whether the connection closes when the last Store is released, or an
external owner keeps the connection, is managed by the provider.
2. Location Store¶
namespace zlink::framework {
struct store_key_t {
std::string value;
};
struct store_version_t {
std::string value;
};
struct store_scan_cursor_t {
std::string value;
};
struct store_value_t {
std::vector<std::byte> bytes;
store_version_t version;
std::optional<std::chrono::system_clock::time_point> expires_at;
std::chrono::system_clock::time_point store_now{};
};
struct store_missing_t {
std::chrono::system_clock::time_point store_now{};
};
struct store_found_t {
store_value_t value;
};
using store_read_result_t = std::variant<store_missing_t, store_found_t>;
struct store_missing_condition_t {
store_key_t key;
};
struct store_version_condition_t {
store_key_t key;
store_version_t expected;
};
using store_condition_t =
std::variant<store_missing_condition_t, store_version_condition_t>;
struct store_put_t {
store_key_t key;
std::vector<std::byte> bytes;
std::optional<std::chrono::milliseconds> retention;
};
struct store_delete_t {
store_key_t key;
};
using store_mutation_t = std::variant<store_put_t, store_delete_t>;
struct store_write_request_t {
std::vector<store_condition_t> conditions;
std::vector<store_mutation_t> mutations;
};
struct store_put_version_t {
store_key_t key;
store_version_t version;
};
struct store_write_applied_t {
std::vector<store_put_version_t> put_versions;
std::chrono::system_clock::time_point store_now{};
};
struct store_write_conflict_t {
std::chrono::system_clock::time_point store_now{};
};
using store_write_result_t =
std::variant<store_write_applied_t, store_write_conflict_t>;
struct store_scan_request_t {
std::string prefix;
std::optional<store_scan_cursor_t> cursor;
std::uint32_t limit = 100;
};
struct store_scan_item_t {
store_key_t key;
store_value_t value;
};
struct store_scan_page_t {
std::vector<store_scan_item_t> items;
std::optional<store_scan_cursor_t> next_cursor;
std::chrono::system_clock::time_point store_now{};
};
struct store_scan_expired_t {};
using store_scan_result_t =
std::variant<store_scan_page_t, store_scan_expired_t>;
class location_store_t {
public:
virtual ~location_store_t() = default;
virtual task_t<store_read_result_t> read(store_key_t key) = 0;
virtual task_t<store_write_result_t> write(
store_write_request_t request) = 0;
virtual task_t<store_scan_result_t> scan(
store_scan_request_t request) = 0;
};
} // namespace zlink::framework
Key is an opaque UTF-8 1..1024-byte string the Framework issues,
using case-sensitive comparison. Version and cursor are opaque UTF-8
1..4096-byte strings the provider issues. Value is at most 1 MiB. If
retention is absent, it doesn't expire, and the provider clock is
used for expiry judgment. Since store_now is a time obtained from the
same provider observation, the Framework doesn't use the local clock
for TTL judgment. The specified retention must be positive.
write(...) first checks every condition, and only if all are true
does it apply every mutation as one atomic commit. If even one
condition is false, both mutation and version increase are 0, and it
returns store_write_conflict_t. Condition only provides Missing or
Version comparison. The conflict result doesn't carry domain
state or the current value — the Framework does a direct read of the
needed key.
One write request allows at most 2,048 unique keys combining condition and mutation, and at most 4 MiB of encoded size. A condition or mutation on the same key can't be duplicated.
scan(...) is the required operation recovery and maintenance use to
find a bounded key set. Prefix is UTF-8 0..1024 bytes, and limit is
1..1000. The snapshot the first page created is fixed through the
last page. If the provider can't keep the snapshot any longer, it
returns store_scan_expired_t, and the Framework discards the partial
result and reads from the first page again. A page can return fewer
items than limit once it reaches 4 MiB encoded.
3. Relocation Store¶
namespace zlink::framework {
struct blob_reference_t {
std::string value;
};
struct blob_stored_t {
std::chrono::system_clock::time_point expires_at{};
std::chrono::system_clock::time_point store_now{};
};
struct blob_already_stored_t {
std::chrono::system_clock::time_point expires_at{};
std::chrono::system_clock::time_point store_now{};
};
struct blob_conflict_t {
std::chrono::system_clock::time_point store_now{};
};
using blob_put_result_t =
std::variant<blob_stored_t, blob_already_stored_t, blob_conflict_t>;
struct blob_missing_t {
std::chrono::system_clock::time_point store_now{};
};
struct blob_found_t {
std::vector<std::byte> bytes;
std::chrono::system_clock::time_point expires_at{};
std::chrono::system_clock::time_point store_now{};
};
using blob_read_result_t = std::variant<blob_missing_t, blob_found_t>;
struct blob_renewed_t {
std::chrono::system_clock::time_point expires_at{};
std::chrono::system_clock::time_point store_now{};
};
using blob_renew_result_t = std::variant<blob_missing_t, blob_renewed_t>;
class relocation_store_t {
public:
virtual ~relocation_store_t() = default;
virtual task_t<blob_put_result_t> put(
blob_reference_t reference,
std::span<const std::byte> payload,
std::chrono::milliseconds retention) = 0;
virtual task_t<blob_read_result_t> read(
blob_reference_t reference) = 0;
virtual task_t<blob_renew_result_t> renew(
blob_reference_t reference,
std::chrono::milliseconds retention) = 0;
// an idempotent operation that succeeds even when the reference doesn't exist.
virtual task_t<void> erase(blob_reference_t reference) = 0;
};
} // namespace zlink::framework
Reference is an opaque UTF-8 1..4096-byte string the Framework
issues before put, using match. A deleted or expired reference
also isn't reused for different content. Re-putting the same reference
with the same bytes returns blob_already_stored_t; putting different
bytes returns blob_conflict_t. With this rule, the Framework can
reconcile the storage result after a timeout or connection error by
doing a direct read of the same reference. retention must be
positive.
One data chunk is at most 64 MiB of application bytes; the encoded blob limit the provider receives is owned by Relocation Store Redis §3. The state/queue/timer handoff payload of Actor and Spot relocation doesn't pass through this Store — the Framework splits the payload in source memory into chunks and sends them directly over the source–target ordered mesh connection. The normal-execution responsibilities that remain in the Store are recording the first message and creation information of Instance Spot cold activation, and recording the reply payload and terminal result of pending requests that complete after relocation. Checksum and record encoding are owned by the Framework, and the provider doesn't interpret the payload.
The input span the provider receives is valid only until the asynchronous operation ends. Ownership of the returned byte vector transfers to the caller.
4. Call Failure And Reconciliation¶
The C++ interface doesn't directly port a different language's cancellation token. If host shutdown or deadline is confirmed before the Framework starts an operation, it doesn't call the provider. If a timeout, transport error, or process interruption occurs after a call has started, whether the commit was applied may be uncertain. The Framework reconciles the result with the Location Store's read and version, or the Relocation Store's Framework-issued reference.
A caller error, such as an input range violation, is validated before starting the operation. Conflict, Missing, Expired, and AlreadyStored are normal result variants, and aren't expressed as an exception. A provider failure converts to the runtime's provider-failure classification, and doesn't expose internal information, such as a Redis key or script, to the application error.
5. Operational Query¶
The application looks up an aggregate projection instead of directly reading a stored key or private record.
namespace zlink::framework {
enum class location_role_t : std::uint16_t {
invalid = 0,
spot = 2,
router = 3,
dealer = 4,
pub = 5,
sub = 6
};
struct location_page_request_t {
int page_size = 100;
std::optional<std::string> continuation_token;
};
template <typename T>
struct location_page_t {
std::vector<T> items;
std::optional<std::string> continuation_token;
};
class location_readiness_t {
public:
virtual ~location_readiness_t() = default;
virtual task_t<bool> is_peer_ready(
std::string mesh_name,
location_role_t role,
std::optional<zlink::routing_id_t> node_rid = std::nullopt) = 0;
};
struct location_runtime_status_t {
bool store_healthy = false;
std::optional<std::chrono::system_clock::time_point> last_refresh_at;
bool owner_lease_healthy = false;
std::optional<std::chrono::system_clock::time_point> owner_lease_renewed_at;
};
enum class location_topology_state_t {
discovered = 1,
connecting = 2,
ready = 3,
lost = 4,
error = 5,
stopped = 6
};
struct location_topology_filter_t {
std::optional<std::string> mesh_name;
std::optional<zlink::routing_id_t> node_rid;
std::optional<location_topology_state_t> state;
};
struct location_topology_entry_t {
std::string mesh_name;
zlink::routing_id_t node_rid;
std::string endpoint;
bool draining = false;
location_topology_state_t state = location_topology_state_t::discovered;
std::chrono::system_clock::time_point updated_at{};
};
struct location_service_summary_filter_t {
std::optional<std::string> mesh_name;
};
struct location_service_summary_t {
std::string mesh_name;
std::uint32_t total_count = 0;
std::uint32_t ready_count = 0;
std::uint32_t error_count = 0;
std::uint32_t stopped_count = 0;
std::chrono::system_clock::time_point last_updated_at{};
};
enum class location_object_kind_t {
actor = 0,
user_spot = 1,
instance_spot = 2
};
enum class location_object_state_t {
creating = 0,
ready = 1,
unavailable = 2
};
struct location_object_entry_t {
std::string global_id;
std::uint64_t object_generation = 0;
std::string mesh_name;
zlink::routing_id_t node_rid;
location_object_state_t state = location_object_state_t::creating;
std::string stable_type;
};
struct location_object_filter_t {
location_object_kind_t object_kind;
std::optional<std::string> stable_type;
std::optional<std::string> mesh_name;
};
class location_runtime_query_t {
public:
virtual ~location_runtime_query_t() = default;
virtual task_t<location_runtime_status_t> get_status() = 0;
virtual task_t<location_page_t<location_topology_entry_t>> list_topology(
location_topology_filter_t filter,
location_page_request_t page = {}) = 0;
virtual task_t<location_page_t<location_service_summary_t>>
list_service_summaries(
location_service_summary_filter_t filter,
location_page_request_t page = {}) = 0;
virtual task_t<std::optional<location_object_entry_t>>
find_actor_location(actor_id_t actor_id) = 0;
virtual task_t<std::optional<location_object_entry_t>>
find_spot_location(spot_id_t spot_id) = 0;
virtual task_t<location_page_t<location_object_entry_t>>
list_object_locations(
location_object_filter_t filter,
location_page_request_t page = {}) = 0;
};
} // namespace zlink::framework
NodeRid is a transport routing identity, so it keeps the public
zlink::routing_id_t. Store version, private owner token, and provider
clock aren't exposed in the operational query.
Direct lookup by Actor ID and Spot ID each queries one current object
location. Missing returns an empty std::optional; Creating returns a
creating entry; Ready returns a ready entry; and an unavailable
current owner after commit returns an unavailable entry. Spot
lookup treats User Spot and Instance Spot under the same Spot-ID lookup
contract. A list requires object_kind, and takes stable type and
MeshName as optional filters. Page size is 1..1000, the encoded page is
at most 4 MiB, and the continuation token is an opaque value issued by
the query. A Store query failure fails the whole operation with
framework_error_kind_t::unavailable and does not return a partial page.
6. Redis Extension¶
namespace zlink::framework::redis {
struct redis_location_options_t {
std::string connection_string;
std::string key_prefix;
std::chrono::milliseconds operation_timeout{5000};
};
struct redis_relocation_options_t {
std::string connection_string;
std::string key_prefix;
std::chrono::milliseconds operation_timeout{5000};
};
class redis_location_store_t final : public location_store_t {
public:
explicit redis_location_store_t(redis_location_options_t options);
~redis_location_store_t() override;
};
class redis_relocation_store_t final : public relocation_store_t {
public:
explicit redis_relocation_store_t(redis_relocation_options_t options);
~redis_relocation_store_t() override;
};
} // namespace zlink::framework::redis
The provider surface the official Redis package makes public is two
options and two Store implementation classes. Redis key layout, Lua
script, private record encoding, retry, and connection lease are
implementation details. The two Stores can share the same Redis
deployment, or be physically separate. Even when using the same
deployment, they use different key_prefixes, and don't require a
cross-store transaction.
7. Types Not Made Public¶
The following types and operations are Framework-private records or Redis implementation details.
- Authority/owner-lease/reservation/capacity/fence/aggregate DTO
- A domain operation such as
reserve,commit,abort,prepare_aggregate - Relocation phase/manifest/participant DTO and a provider-generated relocation reference
- Raw Redis command adapter, script, and key codec
- Spot/Actor-dedicated Store and per-capability Store interface
Session Relocation Seal Timeout¶
session_relocation_seal_timeout is a startup-only positive millisecond duration with a
3,000 ms default. Zero, negative, infinite, or out-of-range values are configuration errors
before socket bind.
Relocation Transfer Options¶
relocation_cutover_wait_timeout is a startup-only positive millisecond duration with a
1,000 ms default. It is the time the target waits for cutover after the relay-ready reply,
and it equals the time the source keeps the boundary batch retransmission copy. Zero,
negative, infinite, or out-of-range values are configuration errors before socket bind.
relocation_payload_chunk_limit_bytes is the size cap of one encoded chunk that a
relocation payload is split into, with a 262,144-byte (256 KiB) default. Zero and values
exceeding the transport-negotiated frame limit are configuration errors before socket
bind.
relocation_in_flight_payload_budget_bytes is the cap on the accounted byte sum of
relocation chunks concurrently in flight for one peer connection, with a 16,777,216-byte
(16 MiB) default. relocation_node_in_flight_payload_budget_bytes applies the same sum
cap node-wide, with a default of 0. For both budgets, 0 means the budget isn't applied.
All three size options are startup-only, and the runtime doesn't adjust them
automatically.