Class ThreadSafeEntityCache

Nested Relationships

Nested Types

Class Documentation

class ThreadSafeEntityCache

Thread-safe, index-optimized cache for SOVD entities.

Design principles:

  1. Primary storage in vectors (cache-friendly, predictable memory)

  2. Hash indexes for O(1) lookup by ID

  3. Relationship indexes for O(1) aggregation queries

  4. Reader-writer lock for concurrent access

  5. Batch updates to minimize lock contention

Thread Safety:

  • Multiple readers can access concurrently (shared lock)

  • Writers get exclusive access (unique lock)

  • Readers never block each other

  • Writers block all readers and other writers

Usage:

  • Discovery thread calls update_*() methods (writer)

  • HTTP handlers call get_*() methods (reader)

Public Functions

ThreadSafeEntityCache() = default
void update_all(std::vector<Area> areas, std::vector<Component> components, std::vector<App> apps, std::vector<Function> functions, std::unordered_map<std::string, std::string> node_to_app = {})

Atomic batch update of all entities.

This is the preferred update method as it:

  1. Minimizes lock hold time

  2. Ensures readers never see partial state

  3. Rebuilds all indexes in one pass

Parameters:
  • areas – All discovered areas

  • components – All discovered components

  • apps – All discovered apps

  • functions – All discovered functions

  • node_to_app – Node FQN to app entity ID mapping from linking result (optional)

void update_areas(std::vector<Area> areas)

Incremental update for single entity type.

Use sparingly - prefer update_all() for full refresh. Each call acquires exclusive lock and rebuilds relevant indexes.

void update_components(std::vector<Component> components)
void update_apps(std::vector<App> apps)
void update_functions(std::vector<Function> functions)
void update_topic_types(std::unordered_map<std::string, std::string> topic_types)

Update cached topic type mapping.

Called during cache refresh to cache topic name -> message type mapping. This avoids expensive ROS graph queries on every /data request.

Parameters:

topic_types – Map of topic name to message type

std::vector<Area> get_areas() const
std::vector<Component> get_components() const
std::vector<App> get_apps() const
std::vector<Function> get_functions() const
std::optional<Area> get_area(const std::string &id) const
std::optional<Component> get_component(const std::string &id) const
std::optional<App> get_app(const std::string &id) const
std::optional<Function> get_function(const std::string &id) const
AppLinksSnapshot get_app_with_links(const std::string &id) const
AppDependenciesSnapshot get_app_with_dependencies(const std::string &id) const
bool has_area(const std::string &id) const
bool has_component(const std::string &id) const
bool has_app(const std::string &id) const
bool has_function(const std::string &id) const
std::string get_topic_type(const std::string &topic_name) const

Get cached message type for a topic.

Parameters:

topic_name – Full topic path (e.g., “/sensor/temperature”)

Returns:

Message type string, or empty if not cached

std::unordered_map<std::string, std::string> get_node_to_app() const

Get the node FQN to app entity ID mapping.

Used by trigger subscribers to resolve ROS 2 node FQNs to manifest entity IDs. The mapping is populated from the linking result during cache refresh.

Returns:

Map of node FQN -> app entity ID (empty if no linking available)

std::string resolve_node_to_app(const std::string &node_fqn) const

Look up a single node FQN to app entity ID mapping (shared lock, no map copy)

std::optional<EntityRef> find_entity(const std::string &id) const
SovdEntityType get_entity_type(const std::string &id) const
std::vector<std::string> get_apps_for_component(const std::string &component_id) const

Get Apps hosted on a Component.

Returns:

Vector of App IDs (empty if component not found)

std::vector<std::string> get_components_for_area(const std::string &area_id) const

Get Components in an Area.

Returns:

Vector of Component IDs (empty if area not found)

std::vector<std::string> get_apps_for_function(const std::string &function_id) const

Get Apps implementing a Function.

Returns:

Vector of App IDs (empty if function not found)

std::vector<std::string> get_subareas(const std::string &area_id) const

Get subareas of an Area.

Returns:

Vector of Area IDs (empty if area not found or no subareas)

AggregatedOperations get_app_operations(const std::string &app_id) const

Aggregate operations for an App (no aggregation, returns own ops)

AggregatedOperations get_component_operations(const std::string &component_id) const

Aggregate operations for a Component.

Returns: Component’s own operations + all operations from hosted Apps. Deduplicates by operation full_path.

AggregatedOperations get_area_operations(const std::string &area_id) const

Aggregate operations for an Area (x-medkit extension)

Returns: All operations from all Components in the Area. Recursive through Component→App hierarchy.

Note: This is a ros2_medkit extension. SOVD spec doesn’t allow Area to have resource collections.

AggregatedOperations get_function_operations(const std::string &function_id) const

Aggregate operations for a Function.

Returns: All operations from all Apps implementing this Function.

AggregatedData get_entity_data(const std::string &entity_id) const

Aggregate data (topics) for any entity by ID.

Unified method that works for all entity types. Aggregates topics from the entity and its children.

Parameters:

entity_id – Entity ID to get data for

Returns:

AggregatedData with topics, or empty if entity not found

AggregatedData get_app_data(const std::string &app_id) const

Aggregate data (topics) for an App.

AggregatedData get_component_data(const std::string &component_id) const

Aggregate data (topics) for a Component.

Returns: Component’s own topics + all topics from hosted Apps.

AggregatedData get_area_data(const std::string &area_id) const

Aggregate data (topics) for an Area.

Returns: All topics from all Components in the Area.

AggregatedData get_function_data(const std::string &function_id) const

Aggregate data (topics) for a Function.

Returns: All topics from all Apps implementing this Function.

AggregatedConfigurations get_entity_configurations(const std::string &entity_id) const

Aggregate configurations (node FQNs) for any entity by ID.

Unified method that works for all entity types. For Apps, returns the single bound node. For Components/Areas/Functions, aggregates all child app nodes.

Parameters:

entity_id – Entity ID to get configurations for

Returns:

AggregatedConfigurations with node FQNs, or empty if entity not found

AggregatedConfigurations get_app_configurations(const std::string &app_id) const

Get configurations for an App (returns its single bound node)

AggregatedConfigurations get_component_configurations(const std::string &component_id) const

Aggregate configurations for a Component.

Returns: All node FQNs from hosted Apps.

AggregatedConfigurations get_area_configurations(const std::string &area_id) const

Aggregate configurations for an Area.

Returns: All node FQNs from all Components in the Area.

AggregatedConfigurations get_function_configurations(const std::string &function_id) const

Aggregate configurations for a Function.

Returns: All node FQNs from all Apps implementing this Function.

std::optional<EntityRef> find_operation_owner(const std::string &operation_path) const

Find which entity owns an operation by full path.

Parameters:

operation_path – e.g., “/nav2/navigate_to_pose”

Returns:

EntityRef if found

EntityCacheStats get_stats() const

Get cache statistics.

std::string validate() const

Validate internal consistency.

Checks:

  1. All indexes point to valid vector entries

  2. Relationship indexes are consistent

  3. No duplicate IDs within entity type

Returns:

Empty string if valid, error message otherwise

std::chrono::system_clock::time_point get_last_update() const

Get last update timestamp.

uint64_t generation() const

Get the current generation counter.

The generation counter is incremented on every cache mutation (update_all, update_areas, update_components, update_apps, update_functions). Consumers can use this to detect when cached derived data (e.g., OpenAPI specs) is stale and needs regeneration.

struct AppDependenciesSnapshot

Atomic snapshot of an App together with its declared depends_on apps.

handle_app_depends_on iterates app.depends_on and resolves each id via get_app() - one shared_lock per dependency. A writer refresh can land between the app fetch and any of the per-dependency fetches, so a 5-dependency app can return 5 apps from 5 different cache generations. This helper takes a single shared_lock and returns the app together with every dependency resolved in the same generation.

app is empty if app_id is not in the cache. dependencies lists every entry in app->depends_on in declaration order; the optional is empty when the referenced dependency cannot be resolved (broken ref).

Public Members

std::optional<App> app
std::vector<std::pair<std::string, std::optional<App>>> dependencies
struct AppLinksSnapshot

Atomic snapshot of an App together with its parent Component and Area.

Three sequential get_app/get_component/get_area calls each acquire a fresh shared_lock and a writer refresh can advance the generation between them, so handlers that traverse App -> Component -> Area can observe a mixed-generation view (e.g. app from gen N, component from N+1, area from N). This helper resolves the chain under a single shared_lock so the result is internally consistent.

app is empty if app_id is not in the cache. component is empty if the app has no component_id or the referenced component is missing. area is empty if the component has no area or the referenced area is missing. The two latter cases are distinguishable from “no parent” because app.component_id / component.area remain set on the returned models.

Public Members

std::optional<App> app
std::optional<Component> component
std::optional<Area> area