Skip to content

Implement caching for registered metrics#82

Open
daniil-berg wants to merge 3 commits intomainfrom
feat/cached-metrics
Open

Implement caching for registered metrics#82
daniil-berg wants to merge 3 commits intomainfrom
feat/cached-metrics

Conversation

@daniil-berg
Copy link
Copy Markdown
Owner

@daniil-berg daniil-berg commented Apr 22, 2026

As I mentioned previously, I wanted to implement caching for our registered metrics meta-data as well as the associated tags to speed up the endpoint as much as possible. This may or may not be overkill in practice, but it was a fun exercise and helped me understand the MUC much better.

Here I introduce a couple of changes, mostly to the metrics_manager, to make this work. These changes incidentally also made the manager's interface a bit nicer in my opinion. But I am curious what you think.

TL;DR

Get the manager

$manager = di::get(metrics_manager::class);

Chain with sync

$manager = di::get(metrics_manager::class)->sync(delete: true); // Populates the cache with entries for all registered metrics.

Get a specific registered metric by qualified name

$metric = di::get(metrics_manager::class)->sync(delete: true)[$qualifiedname]; // May throw a metric_not_found exception.

Get all enabled metrics that carry the foo tag

$manager = di::get(metrics_manager::class);
$metrics = $manager->filter(enabled: true, tagnames: ['foo']); // Populates the cache with entries for all collected metrics.

Get all collected and registered metrics

$manager = di::get(metrics_manager::class);
$metrics = $manager->filter(); // Populates the cache with entries for all collected metrics.

Changes

Dependency injection

The metric_collection hook now defines its own dependency injection config to get dispatched automatically, when Moodle's DI container injects it somewhere. This allows the metrics_manager to be retrieved via DI as well and there is no longer a need to actively "dispatch the hook" from the outside. Since the DI container is automatically reset in Moodle's PHPUnit setup, testing also becomes much clearer. Thus the dispatch_hook method is gone from the manager; as is the optional $collect parameter in its sync method.

The canonical way to get a metrics manager is now the same as with Moodle's hook manager:

use core\di;
use tool_monitoring\metrics_manager;

$manager = di::get(metrics_manager::class);

All metrics that hook into our metrics_collection event, are guaranteed to be collected and stored in the manager at this point. And since the DI container shares the reference to its dependencies for the lifetime of the request, subsequent retrievals of the manager will not trigger collection again.

In tests, to substitute a specific collection, we can now just do this:

$collection = new metric_collection();
// Add metrics to the collection...
di::set(metric_collection::class, $collection);
$manager = di::get(metrics_manager::class);

As long as resetAfterTest is called in the test method, this has no effect on subsequent tests.

Array-like manager

The manager allows subscript access to individual registered_metric instances by qualified name, like an associative array. It implements ArrayAccess. (It only allows reading, not removing/modifying keys.)

Metrics cache and filtering

Collected and registered metrics can be retrieved via the filter method.
Calling it without arguments will return all of them.

$manager = di::get(metrics_manager::class);
// Only get enabled metrics that carry the 'foo' tag:
foreach ($manager->filter(enabled: true, tagnames: ['foo']) as $qname => $metric) {
    // Now `$qname` is a string and `$metric` is a `registered_metric` object.
}

It always tries to get them from the cache first. With static acceleration configured, this is almost as fast as storing them on the manager itself (for the lifetime of a request).

Since the manager implements the core_cache\data_source_interface, metrics not yet cached are fetched from the DB automatically and loaded into the cache.

The manager also does explicit null-caching, so if a qualified name requested from it did not match a collected metric or that metric was not registered (yet), that fact is cached and subsequent requests no longer touch the DB.

Calling the sync method of course will register any newly picked up metrics and now also re-builds the cache.

Cacheable registered_metric

To make this all convenient, the registered_metric implements the cacheable_object_interface and it stores its tags in a separate property.

metric_tag subclass

This exists mostly for convenience and extends the core_tag_tag class. It also implements the cacheable_object_interface.

metrics_cache helper class

This is just a convenient and type safe wrapper around the cache store functions.

Tag change events

To properly invalidate caches, event observers are registered for tag changes.


I would really appreciate your feedback on this one. I know it is a lot to go through, but I promise this is the last major feature/change I want before the v1.0 release.

The manager still does the `sync` operation, but DB fetching for metrics is now done by `registered_metric`.
The manager is stateless (aside from its `metric_collection`).
Due to static accelleration by the MUC, `registered_metric` instances do not need to be stored on the manager itself.
The manager allows array-like subscript access to individual metrics.
Its `filter` method allows retrieving all or some of the registered metrics from the cache.
The manager also implements the `data_source_interface` to load missing metrics into the cache.
It also does null-caching on collected but not yet registered metrics.
The manager relies on dependency injection now. Its `dispatch_hook` method is gone.
The `metric_collection` hook now defines its own dependency injection config to get dispatched automatically.
The `registered_metric::to_db` method is now public.
`registered_metric` implements the `cacheable_object_interface`

The new `metric_tag` class extends the `core_tag_tag` for convenience.
It also implements the `cacheable_object_interface`.
Cache getting and setting is handled by the new `local\metrics_cache` class.
To properly invalidate caches, event observers are registered for tag changes.

Unit tests and Behat tests are updated.
@daniil-berg daniil-berg added this to the v1.0.0 milestone Apr 22, 2026
@daniil-berg daniil-berg self-assigned this Apr 22, 2026
@daniil-berg daniil-berg added enhancement New feature or request refactor Moving things around breaking change tests Related to unit/acceptance tests labels Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change enhancement New feature or request refactor Moving things around tests Related to unit/acceptance tests

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant