Entity Cache

The entity store uses the entity cache to optimize the retrieval of entities.

When an entity is "retrieved", the events in its event stream are read and projected onto the entity.

Each time an entity is retrieved, the resulting entity is recorded in a cache. Any subsequent retrieval of the entity requires that only the events recorded since the previous retrieval are read and projected onto the entity.

The entity cache is composed of two parts: the in-memory cache that stores any entity retrieved by a its store, and the on-disk persistent cache of entity snapshots that are used to create an entity's cache record if one is not already present in the cache at the time of the retrieval.

To avoid the cost of projecting all of an entity's events when the entity is not in the cache (as when a service has just been started), an entity is periodically persisted to disk in a snapshot stream. If an entity is retrieved and there's no cache record in the in-memory cache for it, the latest snapshot will be retrieved and inserted into the in-memory cached before the latest events are read and projected.

Note: It's quite rare to have to interact directly with the entity cache. Entities are cached automatically by "retrieving" an entity from an entity store. The entity cache is transparent and in the background for the vast majority of uses. This user guide is provided as an affordance, but it's unlikely to be necessary to the use of the whole toolkit.

Entity Cache Facts

  • The cache stores not only the entities, but also their stream version
  • Caches are not shared between stores
  • The cache data lifecycle can last for the life of the Ruby process, the life of the current thread, or the just life of the cache object itself
  • Entities are not cleared from the cache once they are inserted into it
  • The cache is made of two stores: the in-memory internal cache, and an optional external entity snapshot writer and reader
  • The on-disk snapshot of an entity is only retrieved when an entity retrieval is actuated and there is no existing cache record for the entity in the cache

EntityCache Class

The EntityCache class is a concrete class from the EntityCache library and namespace.

The EntityCache class provides:

  • The get method for retrieving a cache record by the cached entity's ID
  • The put method for inserting an entity and its caching metadata into the cache

Cache Record

An entity that has been cached is contained in a cache record.

The cache record contains the entity as well as metadata about the caching of the entity in both the internal, in-memory cache and the optional external snapshot store.

# Cache record definition
Record = Struct.new(
  :id,
  :entity,
  :version,
  :time,
  :persisted_version,
  :persisted_time
)

Attributes

Name Description Type
id The ID of the cached entity String
entity This cached entity Object
version The stream version of the cached entity Integer
time Time that the entity was cached Time
persisted_versiion Version of the most recent entity snapshot stored Integer
persisted_time Time that the most recent entity snapshot was stored Time

Caching an Entity

Entities are cached automatically by "retrieving" an entity from an entity store. The entity cache is transparent and in the background for the vast majority of uses. See the entity store user guide for more details on the entity store.

put(id, entity, version, time: clock.now, persisted_version: nil, persisted_time: clock.now)

Returns

The cache record that was either created and inserted into the cache, or the cache record that was updated if there was already a record in the cache for the entity.

Parameters

Name Description Type
id The ID of the entity being cached String
entity This entity being cached Object
version The stream version of the entity being cached Integer
time Time that the entity is cached Time
persisted_versiion Version of the most recent entity snapshot stored Integer
persisted_time Time that the most recent entity snapshot was stored Time

Get a Record from the Cache

get(id)

Returns

The cache record corresponding to the ID, or nil if no cache record is found.

Parameters

Name Description Type
id The ID of the entity for the cache record being retrieved String

Note: If the external snapshot store is configured, and if no cache record is found in the internal cache, a retrieval of the latest snapshot is actuated. If a snapshot is retrieved, it is inserted into the cache, and subsequently returned to the caller of the get method.

Deleting Cache Records from the Internal Store

delete(id)

Returns

Returns the cache record that corresponds to the ID, or nil if there is no cache record for the ID.

Parameters

Name Description Type
id The entity ID of the cache record being deleted String

Counting Cache Records in the Internal Store

count()

Returns

Returns the count of records in the internal store.

Determining Whether the Cache's Internal Store Has Any Records

empty?()

Returns

Returns false if the internal store has any cache records, and true if the internal store has no records.

Scoping

A cache's scope controls whether and how a cache is shared between different instances of the same entity store class.

Entity store instances can be constructed and destructed as often as messages are dispatched to handlers. However, the entity caches should not be garbage collected when their entity store goes out of scope and is garbage collected.

The reason for this is that an entity cache has accumulated current projections of entities from their event streams, and loosing those caches when a store instance is garbage collected would have a detrimental and noticeable impact on performance.

A cache's scope can be one of:

  • :thread
  • :global
  • :exclusive

Thread

The thread scope is the default scope.

The thread scope will store an entity store's cache in thread-local storage. This keeps stores of the same class that are operating independently in separate threads isolated from each other and free of concurrency collisions or contention.

Global

The global scope means that the cache is scoped to the lifecycle of a particular entity store class.

Ruby classes remain in scope for the entire lifecycle of a Ruby process. Therefore, a store's cache will remain in-memory until the process is terminated.

The global scope is rarely used in-practice except for certain circumstances where a store might be used in a utility script.

Exclusive

Exclusive scope means that a cache is used exclusively by one instance of an entity store. When the entity store goes out of scope and is garbage collected, the cache will be garbage collected as well.

The exclusive scope is useful in development when testing. In such situations, having a cache that out-lives the lifecycle of a store can create false and unexpected test results.

ENTITY_CACHE_SCOPE Environment Variable

The ENTITY_CACHE_SCOPE environment variable can be used to override the default value.

It can be set to one of the following values:

  • thread
  • global
  • exclusive
ENTITY_CACHE_SCOPE=global start_service.sh

Set ENTITY_CACHE_SCOPE on a Development Machine

On a development machine, the ENTITY_CACHE_SCOPE environment variable should be set to exclusive. This is because the other scopes would cause the same cache instance to be reused by a particular entity store class for the life of a Ruby process.

The effect of this is that the same entity cache instance would be used for the entirety of a test run, which would allow dirty state to interfere potentially in tests that use the store. And this could cause non-deterministic test results and false test results.

In bench testing work on a development machine, an individual cache instance should not be shared with other instances of the same entity store class.

It's good practice on development machines to export the ENTITY_CACHE_SCOPE environment variable in the user profile scripts on a development machine.

export ENTITY_CACHE_SCOPE=exclusive

Clearing the In-Memory Cache

The entity cache is never cleared in an operational system. Once an entity is inserted into the entity cache, it remains there until the Ruby process that the cache is running in is terminated, or until a cache record is explicitly removed using the internal store's delete method.

Because services are restarted for upgrades or other maintenance and operational reasons, entity caches are typically cleared on a sufficiently-regular basis such that memory utilization does not become an issue.

However, long-lived services that are very stable and have no maintenance or operational reasons to be restarted will accumulate cache records in-memory indefinitely. In practice, this is usually not an issue and can be counteracted easily with system-level process monitoring tools that simply restarts a service when it reaches a given memory consumption threshold.

This is a perfectly safe operation because services are designed to be both autonomous and idempotent as a matter of course, and the component host infrastructure does service shutdown in a safe and graceful way.

If a service is either not autonomous or not idempotent, then serious malfunctions will be evident long before memory consumption becomes an issue.

Constructing an Entity Cache

Entity caches can be constructed in one of two ways:

  • Via the constructor
  • Via the initializer

Via the Constructor

self.build(subject, scope: thread, persist_interval: nil, external_store: nil, external_store_session: nil)

The constructor not only instantiates the cache, but also invokes the cache's configure instance method, which constructs the cache's operational dependencies.

Returns

Instance of the EntityCache class.

Parameters

Name Description Type
subject The entity class that the cache manages Class
scope One of the three entity cache scopes, :thread, :global, or :exclusive Symbol
persist_interval Interval, in messages read and projected, in which the entity state is written to its snapshot stream Integer
external_store A class that implements the snapshotting protocol Class
external_store_session An optional, existing session object that the snapshot implementation uses to interact with the database, rather than allowing the cache to create a new session MessageStore::Postgres::Session
cache = Write.build(
  SomeEntity,
  scope: :thread,
  persist_interval: 100,
  external_store: EntitySnapshot::Postgres,
  external_store_session: MessageStore::Postgres::Session.build
)

Via the Initializer

self.initialize(subject)

Returns

Instance of the EntityCache class.

Parameters

Name Description Type
subject The entity class that the cache manages Class

By constructing a cache using the initializer, the cache's dependencies are not set to operational dependencies. They remain inert substitutes.

TIP

See the useful objects user guide for background on inert substitutes.

Log Tags

The following tags are applied to log messages recorded by an entity cache:

Tag Description
cache Applied to all log messages recorded by an entity cache

The following tags may be applied to log messages recorded by an entity cache:

Tag Description
get Applied to log messages recorded while getting an entity from the cache
miss Applied to log messages recorded when getting an entity from the cache and the entity is not in the cache
hit Applied to log messages recorded when getting an entity from the cache and the entity is found in the cache
put Applied to log messages recorded while putting an entity into the cache
restore Applied to log messages recorded while restoring an entity to the cache

See the logging user guide for more on log tags.