ores.cpp.nats-event-cache.cache_header
Table of Contents
Consumer-side entity-mirror cache class. nats-event-cache facet: an
eventing::service::cache::partitioned_cache instantiation, keyed by
tenant id then by the entity's primary key, with a load(tenant_id)
method that calls the entity's read_for_cache RPC (see
ores.cpp.protocol.protocol_header) and replaces that tenant's
partition.
See the Template variable reference for the complete list of available
variables and their semantics, and the facet doc for the cached_by /
read_for_cache flags this archetype requires.
load() encodes/decodes through ores::nats::default_wire_codec()
(the same process-wide, format-configurable codec every hand-written
and generated NATS request/reply path uses — see
ores.service::messaging::decode=/=reply), not rfl::json directly,
so the cache respects ORES_NATS_WIRE_FORMAT like everything else on
the wire.
Includes ores.utility/rfl/reflectors.hpp directly: wire_codec's
templated encode=/=decode still need it whenever the entity carries
a boost::uuids::uuid=/=tenant_id field (every read_for_cache
entity does, since read_for_cache itself requires has_tenant_id),
and a non-template header-only class's inline member bodies are fully
compiled in every translation unit that includes it – unlike
party_cache, which only ever compiled by accident because every
real consumer TU also pulled in a generated *_handler.hpp (which
transitively includes reflectors.hpp via
ores.service/messaging/handler_helpers.hpp) alongside it. Found via
ores.refdata.client's standalone cache test, the first TU to
include this header on its own.
Authentication
read_for_cache requires a valid signed JWT (see the nats-handler
archetype). The generated class takes an optional
std::function<std::string(bool)> token_provider constructor
parameter — pass
ores::iam::client::make_service_token_provider(...)'s return value
(or any callable with the same shape) and every load() attaches a
fresh Authorization: Bearer <jwt> header. Omitted (nullptr), no
header is sent — only valid against a producer whose read_for_cache
handler has no auth check, which none should after this fix.
Optional aux index
Entities whose consumer needs more than key/value lookups (e.g. a
parent/child hierarchy derived from the mirrored data) opt in with
cache_aux_type: <TypeName> (C++ Flags) and supply three paste-point
blocks in their own model, matching the party_cache prototype this
generalizes (children_map built from parent_party_id):
:implements ADD930C7-EF33-44B4-A0A2-49254FCC7ACD— the aux type's declaration (e.g.using children_map = immer::map<...>;), emitted inside the class before thecache_talias.:implements 60520267-94E5-4369-97E9-7AED54CDEDD2— builds a localauxvariable (of the declared aux type) fromentries(the just-builtcache_t::entries_map), emitted insideload()right beforereplace_partition.:implements C0EDFBDF-F38F-4E02-874F-A086FB435FB0— extra public methods reading the aux index (e.g.compute_visible_party_ids), emitted alongsidelookup().
Entities that don't set cache_aux_type get all three paste points
collapsed to nothing and AuxIndex defaults to std::monostate,
exactly as before this section existed.
Template
The full template source. Edit here and re-tangle with
compass build --direct tangle_codegen_templates to regenerate
library/templates/cpp_nats_event_cache.hpp.mustache.
{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.nats-event-cache.cache_header.org. Edit the org source. }}
{{{cpp_license}}}
{{#domain_entity}}
#ifndef ORES_{{cache_component_upper}}_{{cache_subcomponent_upper}}_SERVICE_CACHE_{{entity_singular_upper}}_CACHE_HPP
#define ORES_{{cache_component_upper}}_{{cache_subcomponent_upper}}_SERVICE_CACHE_{{entity_singular_upper}}_CACHE_HPP
#include "ores.eventing.core/service/cache/partitioned_cache.hpp"
#include "ores.logging/make_logger.hpp"
#include "ores.nats/domain/headers.hpp"
#include "ores.nats/domain/message.hpp"
#include "ores.nats/domain/wire_codec.hpp"
#include "ores.nats/service/client.hpp"
#include "ores.{{component_include}}/domain/{{entity_singular}}.hpp"
#include "ores.{{component_include}}/messaging/{{entity_singular}}_protocol.hpp"
{{#primary_key.is_uuid}}
#include <boost/container_hash/hash.hpp>
#include <boost/uuid/uuid.hpp>
{{^cache_aux_type}}
#include <variant>
{{/cache_aux_type}}
{{/primary_key.is_uuid}}
{{#cache_aux_type}}
#include <immer/map.hpp>
#include <immer/map_transient.hpp>
#include <vector>
{{/cache_aux_type}}
#include "ores.utility/rfl/reflectors.hpp"
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <utility>
namespace ores::{{cache_component}}::service::cache {
namespace {
inline auto& {{entity_singular}}_cache_lg() {
static auto instance = ores::logging::make_logger(
"ores.{{cache_component}}.service.cache.{{entity_singular}}_cache");
return instance;
}
} // namespace
/**
* @brief In-process per-tenant cache of {{component}} {{entity_plural}} data.
*
* Populated via NATS request to {{component}}'s read_{{entity_plural}}_for_cache
* subject at startup, and reloaded on receipt of a {{entity_singular}}
* changed-event notification for the affected tenant (see the
* nats-event-cache.registrar_wiring archetype). See the "Generic
* entity-mirror cache primitive + codegen facet" story.
*
* read_{{entity_plural}}_for_cache requires a valid signed JWT (see the
* nats-handler archetype); pass a @c token_provider — typically
* ores::iam::client::make_service_token_provider's return value — so
* every load() attaches a fresh service-account Bearer token. Omit it
* only against a producer that has not opted into the auth check.
*/
class {{entity_singular}}_cache {
{{#primary_key.is_uuid}}
using key_hash = boost::hash<{{primary_key.cpp_type}}>;
{{/primary_key.is_uuid}}
<<paste:ADD930C7-EF33-44B4-A0A2-49254FCC7ACD>>
{{#primary_key.is_uuid}}
using cache_t = ores::eventing::service::cache::partitioned_cache<std::string,
{{primary_key.cpp_type}}, ores::{{component}}::domain::{{entity_singular}},
{{#cache_aux_type}}{{cache_aux_type}}{{/cache_aux_type}}{{^cache_aux_type}}std::monostate{{/cache_aux_type}},
key_hash>;
{{/primary_key.is_uuid}}
{{^primary_key.is_uuid}}
using cache_t = ores::eventing::service::cache::partitioned_cache<std::string,
{{primary_key.cpp_type}}, ores::{{component}}::domain::{{entity_singular}}
{{#cache_aux_type}}, {{cache_aux_type}}{{/cache_aux_type}}>;
{{/primary_key.is_uuid}}
public:
explicit {{entity_singular}}_cache(ores::nats::service::client& nats,
std::function<std::string(bool)> token_provider = nullptr)
: nats_(nats)
, token_provider_(std::move(token_provider)) {}
{{entity_singular}}_cache(const {{entity_singular}}_cache&) = delete;
{{entity_singular}}_cache& operator=(const {{entity_singular}}_cache&) = delete;
{{entity_singular}}_cache({{entity_singular}}_cache&&) = delete;
{{entity_singular}}_cache& operator=({{entity_singular}}_cache&&) = delete;
/**
* @brief Arms (or replaces) the token provider after construction. For a
* consumer that must construct this cache during its own Phase 1
* (wiring/readiness) but can only mint a token during Phase 2
* (post-readiness) — e.g. a self-referential consumer authenticating
* against its own just-registered subjects. See "Service Bootstrap
* Phases" in the architecture docs.
*/
void set_token_provider(std::function<std::string(bool)> token_provider) {
token_provider_ = std::move(token_provider);
}
/**
* @brief Loads (or reloads) one tenant's partition.
*
* @return An empty string on success. On failure, a human-readable
* message describing what went wrong -- the failure is still logged
* (@c warn) either way, but callers that can surface it to a user
* (e.g. a UI's status/error signal) need it returned, not just
* buried in a log a user never sees. Silently-swallowed background
* failures are the right default for the warm-up/refresh path
* (@c nats-event-cache.registrar_wiring calls this fire-and-forget
* from a detached thread), but the return value lets any caller
* that *can* react to a failure opt in.
*/
[[nodiscard]] std::string load(const std::string& tenant_id) {
using namespace ores::logging;
try {
const auto& codec = ores::nats::default_wire_codec();
const auto bytes = codec.encode(
ores::{{component}}::messaging::read_{{entity_plural}}_for_cache_request{
.tenant_id = tenant_id});
std::unordered_map<std::string, std::string> headers;
if (token_provider_)
headers[std::string(ores::nats::headers::authorization)] =
std::string(ores::nats::headers::bearer_prefix) + token_provider_(false);
const auto reply = nats_.request_sync(
ores::{{component}}::messaging::read_{{entity_plural}}_for_cache_request::nats_subject,
bytes, std::move(headers));
auto resp =
codec.decode<ores::{{component}}::messaging::read_{{entity_plural}}_for_cache_response>(
reply.data);
if (!resp || !resp->success) {
const auto msg = resp ? resp->message : "parse error (malformed or error reply)";
BOOST_LOG_SEV({{entity_singular}}_cache_lg(), warn)
<< "{{entity_pascal}} cache load failed for tenant " << tenant_id << ": " << msg;
return msg;
}
auto entries_t = cache_t::entries_map{}.transient();
const auto count = resp->{{entity_plural_short}}.size();
for (auto& v : resp->{{entity_plural_short}})
entries_t.set(v.{{primary_key.column}}, std::move(v));
auto entries = entries_t.persistent();
<<paste:60520267-94E5-4369-97E9-7AED54CDEDD2>>
cache_.replace_partition(tenant_id, entries{{#cache_aux_type}}, aux{{/cache_aux_type}});
BOOST_LOG_SEV({{entity_singular}}_cache_lg(), debug)
<< "Loaded " << count << " {{entity_plural}} for tenant " << tenant_id;
return {};
} catch (const std::exception& e) {
BOOST_LOG_SEV({{entity_singular}}_cache_lg(), warn)
<< "{{entity_pascal}} cache load exception for tenant " << tenant_id << ": "
<< e.what();
return e.what();
}
}
std::optional<ores::{{component}}::domain::{{entity_singular}}>
lookup(const std::string& tenant_id, const {{primary_key.cpp_type}}& key) const {
return cache_.get(tenant_id, key);
}
<<paste:C0EDFBDF-F38F-4E02-874F-A086FB435FB0>>
private:
ores::nats::service::client& nats_;
std::function<std::string(bool)> token_provider_;
cache_t cache_;
};
} // namespace ores::{{cache_component}}::service::cache
#endif
{{/domain_entity}}
See also
- Parent facet: ores.cpp.nats-event-cache
- Template variable reference