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 the cache_t alias.
  • :implements 60520267-94E5-4369-97E9-7AED54CDEDD2 — builds a local aux variable (of the declared aux type) from entries (the just-built cache_t::entries_map), emitted inside load() right before replace_partition.
  • :implements C0EDFBDF-F38F-4E02-874F-A086FB435FB0 — extra public methods reading the aux index (e.g. compute_visible_party_ids), emitted alongside lookup().

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

Emacs 29.3 (Org mode 9.6.15)