Cache

The CachedTable is the main point of interaction, providing transparent access to the records of a table (or a join of tables, or any result set). It gets these records either from a DbAccess, or preferrably a fast StorageTable. Access to records in both is tied together by an Indexes instance.

class tablecache.CachedTable

A cached table.

Caches a (sub-)set of records that can only be accessed relatively slowly (DB) in a relatively fast storage. Not thread-safe.

Serves sets of records that can be specified as arguments to an Indexes instance. Transparently serves them from fast storage if available, or from the DB otherwise. The cache has to be loaded with load() to add the desired records to storage. Read access is blocked until this completes. A convenience method get_first_record() that returns a single record is available.

Most methods for which records need to be specified can either be called with an IndexSpec appropriate to the cache’s Indexes instance, or more conveniently with args and kwargs that will be passed to the Indexes.IndexSpec inner class in order to construct one (the exception to this is invalidate_records(), which needs multiple IndexSpec s).

The DB state is not reflected automatically. If one or more records in the DB change (or are deleted or newly added), invalidate_records() needs to be called for the cache to reflect that. This doesn’t trigger an immediate refresh, but it guarantees that the updated record is loaded from the DB before it is served the next time.

Which subset of the records in DB is cached can be changed by calling adjust(). This operation can load new records and also expire ones no longer needed.

__init__(indexes, db_access, storage_table)
Parameters:
  • indexes (Indexes) – An Indexes instance that is used to translate query arguments into ways of loading actual records, as well as keeping track of which records are in storage.

  • db_access (DbAccess) – The DB access used as the underlying source of truth.

  • storage_table (StorageTable) – The storage table used to cache records.

Return type:

None

async adjust(*args, **kwargs)

Adjust the set of records in storage.

Takes either a single IndexSpec instance or args and kwargs to construct one.

Expires records from storage and loads new ones from the DB in order to attain the state specified via the index spec. Uses the storage’s scratch space to provide a consistent view of the storage without blocking read operations. At all points before this method returns, read operations reflect the state before the adjustment, and at all points after they reflect the state after.

Calls the cache’s indexes’ prepare_adjustment for specs on the records that should be expired and new ones to load. These are then staged in the storage’s scratch space. For each record that is expired or loaded, the adjustment’s observe_expired or observe_loaded is called. Finally, the scratch space is merged, and the indexes’ prepare_adjustment is called.

Only one adjustment or refresh (via refresh_invalid()) can be happening at once. Other ones are locked until previous ones complete. Before the adjustment, any invalid records are refreshed.

Raises:

ValueError – If the specified index doesn’t support adjusting.

Parameters:
  • args (Any) –

  • kwargs (Any) –

Return type:

None

async get_first_record(*args, **kwargs)

Get a single record.

This is a convenience function around get_records(). It returns the first record it would have with the same arguments.

Note that records don’t have a defined order, so this should only be used if exactly 0 or 1 record is expected to be returned.

Raises:

KeyError – If no such record exists.

Parameters:
  • args (Any) –

  • kwargs (Any) –

Return type:

Record

async get_records(*args, **kwargs)

Asynchronously iterate over a set of records.

Takes either a single IndexSpec instance or args and kwargs to construct one.

Asynchronously iterates over the set of records specified via spec. Records are taken from fast storage if the index covers the requested set of records and all of them are valid.

A record can become invalid if it is marked as such by a call to invalidate_record(), or if any record (no matter which one) is marked as invalid without providing scores for the index that is used to query here.

Otherwise, records are taken from the (relatively slower) DB. This implies that querying a set of records that isn’t covered (even if just by a little bit) is expensive.

Returns:

The requested records as an asynchronous iterator.

Raises:

ValueError – If the requested index doesn’t support covers.

Parameters:
  • args (Any) –

  • kwargs (Any) –

Return type:

AsyncIterable

invalidate_records(old_index_specs, new_index_specs, *, force_refresh_on_next_read=True)

Mark records in storage as invalid.

All records that are currently in storage and match any index spec in old_index_specs or new_index_specs are marked as invalid. This stores the information necessary to do a refresh (i.e. fetch from the DB) of these records. If force_refresh_on_next_read is True, any future request for any of these records is guaranteed to trigger a refresh first. This guarantee holds for read operations that start after this method returns. Reads that have already started (in a different task) may respect invalidations that happen here, but probably won’t. It is valid to specify records that haven’t actually changed (they will be refreshed as well, though).

During a refresh, all records matching the first index spec in old_index_specs are deleted, then records are loaded again using the first index spec in new_index_specs. It is valid (and perfectly reasonable for many setups) if old_index_specs == new_index_specs.

All index specs in both lists should specify the same set of records, only for different indexes. Having the first element in new_index_specs specify a proper superset of records to that in old_index_specs is possible. Some of the new records will simply be loaded unnecessarily (but records can’t exist twice, since they’e unique by their primary key). However, specifying fewer records in new_index_specs will cause records to be lost.

Each index must only be specified once in each list. All indexes for which an index spec is given must support coverage checks and certainly cover that index spec (i.e. covers returns True).

Not all indexes must be represented in the index spec lists, but those that aren’t are marked as dirty. Any reads against a dirty index will unconditionally cause a refresh (as opposed to indexes that aren’t dirty, which will only be refreshed if the records queried for have been marked as invalid). This is necessary since, without information on which records are invalid, we must assume that all of them are.

This method can be used to load new records, as long as they are covered by all given indexes.

Note: records that are updated or deleted during a refresh are not observed in an adjustment (i.e. Adjustment.observe_expired(), Adjustment.observe_loaded()). If this is needed, adjust() must be used instead.

Parameters:
  • old_index_specs (list[IndexSpec]) – Specifications of the sets of records that should be invalidated, using their old (i.e. now possibly invalid) scores. Must not be empty, and may contain a specification for any available index.

  • new_index_specs (list[IndexSpec]) – Like old_index_specs, but specifying the same records by their new (possibly updated) scores.

  • force_refresh_on_next_read (bool) – Whether to do an automatic refresh before the next read for any of the invalidated records. The refresh is executed lazily when the read arrives, not immediately. If False, the invalid records will continue to be served from storage. A manual refresh must be performed (using refresh_invalid()).

Raises:

ValueError – If the table is not yet loaded, an index is specified more than once, or one of the index specs lists is empty.

Return type:

None

async load(*args, **kwargs)

Clear storage and load all relevant data from the DB into storage.

Takes either a single IndexSpec instance or args and kwargs to construct one.

This is very similar to adjust(), except that the storage is cleared first, a ValueError is raised if the cache was already loaded, and the whole operation doesn’t take place in scratch space.

Like adjust(), calls the cache’s indexes’ prepare_adjustment to determine which records need to be loaded, and then commit_adjustment when they have. Additionally, for each loaded record the adjustment’s observe_loaded is called.

Raises:

ValueError – If the specified index doesn’t support adjusting.

Parameters:
  • args (Any) –

  • kwargs (Any) –

Return type:

None

async loaded()

Wait until the table is loaded.

Blocks until the initial load completes. Once this returns, read access becomes enabled. This can be used e.g. in a readiness check.

async refresh_invalid()

Refresh all records that have been marked as invalid.

Ensures that all records that have been marked as invalid since the last refresh are loaded again from the DB.

This operation needs to wait for any ongoing adjustments to finish. No refresh is triggered if all records are valid already, or if there is another refresh still ongoing.

Return type:

None