-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[AI-6101] Add support for customizable cache keys for persistent cache #21316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b3487a9
b270356
d68445f
80ab48b
4c4ed30
f59d322
5dea5c4
b055b39
9cadb23
79922bc
69c0fbf
fb3b214
8b2775a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add support for customizable cache keys to be used by the agent persistent cache. This allows integrations developers to define when the cache will be invalidated for each integration. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# (C) Datadog, Inc. 2025-present | ||
# All rights reserved | ||
# Licensed under a 3-clause BSD style license (see LICENSE) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# (C) Datadog, Inc. 2025-present | ||
# All rights reserved | ||
# Licensed under a 3-clause BSD style license (see LICENSE) | ||
from __future__ import annotations | ||
|
||
from abc import ABC, abstractmethod | ||
from typing import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from datadog_checks.base import AgentCheck | ||
|
||
|
||
class CacheKey(ABC): | ||
""" | ||
Abstract base class for cache keys management. | ||
|
||
Any implementation of this class provides the logic to generate cache keys to be used in the Agent persistent | ||
cache. | ||
""" | ||
|
||
def __init__(self, check: AgentCheck): | ||
self.check = check | ||
self.__cache_key: str | None = None | ||
|
||
def key(self) -> str: | ||
""" | ||
Returns the cache key for the particular implementation. | ||
""" | ||
if self.__cache_key is not None: | ||
return self.__cache_key | ||
|
||
check_id_prefix = ":".join(self.check.check_id.split(":")[:-1]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no purpose to the key/base key dichotomy, it only adds unnecessary length to the filesystem path in the default case. Given integration
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason why I have split those 2 is to enforce some check related isolation between different checks using the same custom key. My intention here is to avoid a developer setting a For example, imagine a partner that decide that their base key is going to be class PartnerKey(CheckKey):
def base_key(self) -> str:
return "partner_static_key" And they start using this kind of key for all integrations they develop that require the same behavior. I can easily see a scenario in which they would naively expect this key to only affect this check, but without the enforcement on our side of a check-scoped prefix, this would not be true. Without the separation between With that separation they can still create this scenario but they would need to intentionally override the |
||
self.__cache_key = f"{check_id_prefix}:{self.base_key()}" | ||
|
||
return self.__cache_key | ||
|
||
@abstractmethod | ||
def base_key(self) -> str: | ||
""" | ||
Abstract method that derives the cache key for the particular implementation. | ||
This method must return a stable key that only differs between instances based on the | ||
specific implmentation of the invalidation logic. | ||
""" | ||
|
||
def key_for(self, context: str) -> str: | ||
""" | ||
Returns a key that is a combination of the base key and the provided context. | ||
""" | ||
return f"{self.key()}_{context}" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# (C) Datadog, Inc. 2025-present | ||
# All rights reserved | ||
# Licensed under a 3-clause BSD style license (see LICENSE) | ||
from __future__ import annotations | ||
|
||
from collections.abc import Collection | ||
from typing import TYPE_CHECKING | ||
|
||
from datadog_checks.base.utils.containers import hash_mutable | ||
|
||
from .base import CacheKey | ||
|
||
if TYPE_CHECKING: | ||
from datadog_checks.base import AgentCheck | ||
|
||
|
||
class ConfigSetCacheKey(CacheKey): | ||
""" | ||
Cache key that invalidates the cache when a subset of the check's config options changes. | ||
|
||
Parameters: | ||
check: the check instance the key is going to be used for. | ||
init_config_options: the subset of init_config options to use to generate the cache key. | ||
instance_config_options: the subset of config options to use to generate the cache key. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
check: AgentCheck, | ||
*, | ||
init_config_options: Collection[str] | None = None, | ||
instance_config_options: Collection[str] | None = None, | ||
): | ||
super().__init__(check) | ||
self.init_config_options = set(init_config_options) if init_config_options else set() | ||
self.instance_config_options = set(instance_config_options) if instance_config_options else set() | ||
|
||
if not self.init_config_options and not self.instance_config_options: | ||
raise ValueError("At least one of init_config_options or instance_config_options must be provided") | ||
|
||
# Config cannot change on the fly, so we can cache the key | ||
self.__key: str | None = None | ||
|
||
def base_key(self) -> str: | ||
if self.__key is not None: | ||
return self.__key | ||
|
||
init_config_values = tuple( | ||
value for key, value in self.check.init_config.items() if key in self.init_config_options | ||
) | ||
instance_config_values = tuple( | ||
value for key, value in self.check.instance.items() if key in self.instance_config_options | ||
) | ||
|
||
selected_values = init_config_values + instance_config_values | ||
self.__key = str(hash_mutable(selected_values)).replace("-", "") | ||
return self.__key |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,21 @@ | ||||||||||||||||||||
# (C) Datadog, Inc. 2025-present | ||||||||||||||||||||
# All rights reserved | ||||||||||||||||||||
# Licensed under a 3-clause BSD style license (see LICENSE) | ||||||||||||||||||||
from __future__ import annotations | ||||||||||||||||||||
|
||||||||||||||||||||
from .base import CacheKey | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
class FullConfigCacheKey(CacheKey): | ||||||||||||||||||||
""" | ||||||||||||||||||||
Cache key based on the check_id of the check where it is being used. | ||||||||||||||||||||
The check_id includes a digest of the full configuration of the check. The cache is invalidated | ||||||||||||||||||||
whenever the configuration of the check changes. | ||||||||||||||||||||
""" | ||||||||||||||||||||
|
||||||||||||||||||||
def key(self) -> str: | ||||||||||||||||||||
return self.check.check_id | ||||||||||||||||||||
|
||||||||||||||||||||
def base_key(self) -> str: | ||||||||||||||||||||
return self.check.check_id | ||||||||||||||||||||
Comment on lines
+17
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
|
||
from datadog_checks.base import AgentCheck, to_native_string | ||
from datadog_checks.base import __version__ as base_package_version | ||
from datadog_checks.base.utils.cache_key.base import CacheKey | ||
|
||
from .utils import BaseModelTest | ||
|
||
|
@@ -558,6 +559,44 @@ def test_cursor(self, datadog_agent): | |
) | ||
assert check.get_log_cursor() == {'data': '2'} | ||
|
||
def test_cursor_with_custom_cache_key_after_restart(self): | ||
class ConstantCacheKey(CacheKey): | ||
def base_key(self) -> str: | ||
return "always_the_same" | ||
|
||
class TestCheck(AgentCheck): | ||
def persistent_cache_key(self) -> CacheKey: | ||
return ConstantCacheKey(self) | ||
Comment on lines
+563
to
+569
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can move this out of the two tests. |
||
|
||
check = TestCheck(name="test", init_config={}, instances=[{}]) | ||
check.check_id = 'test:bar:123' | ||
check.send_log({'message': 'foo'}, cursor={'data': '1'}) | ||
|
||
assert check.get_log_cursor() == {'data': '1'} | ||
|
||
new_check = TestCheck(name="test", init_config={}, instances=[{}]) | ||
new_check.check_id = 'test:bar:123456' | ||
assert new_check.get_log_cursor() == {'data': '1'} | ||
|
||
def test_cursor_invalidated_for_different_persistent_check_id_part(self): | ||
class ConstantCacheKey(CacheKey): | ||
def base_key(self) -> str: | ||
return "always_the_same" | ||
|
||
class TestCheck(AgentCheck): | ||
def persistent_cache_key(self) -> CacheKey: | ||
return ConstantCacheKey(self) | ||
|
||
check = TestCheck(name="test", init_config={}, instances=[{}]) | ||
check.check_id = 'test:bar:123' | ||
check.send_log({'message': 'foo'}, cursor={'data': '1'}) | ||
|
||
assert check.get_log_cursor() == {'data': '1'} | ||
|
||
new_check = TestCheck(name="another_test", init_config={}, instances=[{}]) | ||
new_check.check_id = 'test2:bar:456' | ||
Comment on lines
+596
to
+597
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not clear to me whether it is the |
||
assert new_check.get_log_cursor() is None | ||
|
||
def test_no_cursor(self, datadog_agent): | ||
check = AgentCheck('check_name', {}, [{}]) | ||
check.check_id = 'test' | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.