Skip to content

Commit 974e743

Browse files
gerrit(lib): add cached marker to version section
We want to know how stale the cache is for of gerrit version check. By adding the UNIX timestamp and TTL value to the section header via the cached marker, we get this in the GUI out-of-the-box. CMK-34901 Change-Id: I722a121164c638f08c6970790161cd82a1d21529
1 parent 11df38f commit 974e743

4 files changed

Lines changed: 68 additions & 18 deletions

File tree

.werks/20252.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[//]: # (werk v3)
2+
# Gerrit: show cache age for version check
3+
4+
key | value
5+
---------- | ---
6+
date | 2026-08-06T14:55:51.043709+00:00
7+
version | 2.5.0p12
8+
class | feature
9+
edition | community
10+
component | checks
11+
level | 1
12+
compatible | yes
13+
14+
The Gerrit version check caches its API call to reduce load on the monitored
15+
instance (see werk #17594), but there was no way to tell how stale a cached
16+
result was.
17+
18+
The special agent now embeds the cache timestamp and TTL into the
19+
`gerrit_version` section header. Checkmk's core picks this up automatically,
20+
so the service now shows cache age information ("Cache generated X ago",
21+
cache interval, elapsed cache lifespan) in the GUI without any further
22+
configuration.
23+
24+
There is nothing for the user to do; this information is shown automatically
25+
once the site is updated.

cmk/plugins/gerrit/lib/agent.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import json
1313
import sys
1414
from collections.abc import Sequence
15+
from typing import NewType
1516

1617
from cmk.password_store.v1_unstable import parser_add_secret_option, resolve_secret_option
1718
from cmk.plugins.gerrit.lib.cache import cache_ttl
@@ -35,7 +36,7 @@ def main() -> int:
3536

3637
ctx = GerritRunContext(
3738
hostname=args.hostname,
38-
ttl=TTLCache(version=args.version_cache),
39+
ttl=TTLCache(version=int(args.version_cache)),
3940
collectors=Collectors(version=GerritVersion(api_url=api_url, auth=auth)),
4041
)
4142

@@ -106,18 +107,34 @@ class GerritRunContext:
106107

107108

108109
def run(ctx: GerritRunContext) -> int:
109-
version_storage = Storage(f"{AGENT}_version", ctx.hostname)
110-
version_cache = cache_ttl(version_storage, ttl=ctx.ttl.version)
111-
collect_version = version_cache(ctx.collectors.version.collect)
112-
_write_section(collect_version(), name="gerrit_version")
110+
process_version_section(ctx)
113111

114112
return 0
115113

116114

117-
def _write_section(data: object, *, name: str) -> None:
118-
section_payload = json.dumps(data, sort_keys=True)
119-
sys.stdout.write(f"<<<{name}:sep(0)>>>\n")
120-
sys.stdout.write(f"{section_payload}\n")
115+
def process_version_section(ctx: GerritRunContext) -> None:
116+
name = "gerrit_version"
117+
storage = Storage(name, ctx.hostname)
118+
cache_wrapper = cache_ttl(storage, ttl=ctx.ttl.version)
119+
data, ts = cache_wrapper(ctx.collectors.version.collect)()
120+
cache_marker = build_cache_marker(ts=ts, ttl=ctx.ttl.version) if ts is not None else None
121+
write_section(data, name=name, cache_marker=cache_marker)
122+
123+
124+
Marker = NewType("Marker", str)
125+
"""Marker indicates that a string is prefixed with a colon and can be added to a section header."""
126+
127+
128+
def build_cache_marker(ts: float, ttl: int) -> Marker:
129+
return Marker(f":cached({int(ts)},{ttl})")
130+
131+
132+
def write_section(data: object, *, name: str, cache_marker: Marker | None = None) -> None:
133+
header = f"{name}:sep(0){cache_marker or ''}"
134+
content = json.dumps(data, sort_keys=True)
135+
136+
sys.stdout.write(f"<<<{header}>>>\n")
137+
sys.stdout.write(f"{content}\n")
121138

122139

123140
if __name__ == "__main__":

cmk/plugins/gerrit/lib/cache.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@
1515
_HASH_NAMESPACE: Final = uuid.UUID("5871b8db-dcef-4c22-9b36-e81d7d4d66bb")
1616

1717

18-
def cache_ttl[**P, R](store: Storage, *, ttl: int) -> Callable[[Callable[P, R]], Callable[P, R]]:
18+
def cache_ttl[**P, R](
19+
store: Storage, *, ttl: int
20+
) -> Callable[[Callable[P, R]], Callable[P, tuple[R, float | None]]]:
1921
if ttl < 0:
2022
raise ValueError("Time to live value must be a positive integer.")
2123

2224
class Cache[T](TypedDict):
2325
ts: float
2426
data: T
2527

26-
def decorator(f: Callable[P, R]) -> Callable[P, R]:
27-
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
28+
def decorator(f: Callable[P, R]) -> Callable[P, tuple[R, float | None]]:
29+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> tuple[R, float | None]:
2830
if ttl == 0:
29-
return f(*args, **kwargs)
31+
return f(*args, **kwargs), None
3032

3133
if any(not isinstance(arg, Hashable) for arg in args):
3234
raise ValueError(f"Unhashable arg values: {args}")
@@ -41,13 +43,13 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
4143
cache = Cache[R](ts=raw_cache["ts"], data=raw_cache["data"])
4244

4345
if 0 < time.time() - cache["ts"] < ttl:
44-
return cache["data"]
46+
return cache["data"], cache["ts"]
4547

4648
new_data = f(*args, **kwargs)
47-
new_cache = Cache[R](ts=time.time(), data=new_data)
49+
new_cache = Cache[R](ts=(ts := time.time()), data=new_data)
4850
store.write(key, json.dumps(new_cache))
4951

50-
return new_data
52+
return new_data, ts
5153

5254
return wrapper
5355

tests/unit/cmk/plugins/gerrit/lib/test_agent.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# conditions defined in the file COPYING, which is part of this source code package.
55

66
import argparse
7+
import re
78
from pathlib import Path
89

910
import pytest
@@ -31,8 +32,8 @@ def test_run_agent(capsys: pytest.CaptureFixture[str]) -> None:
3132
# agent ran without error
3233
assert captured.err == ""
3334

34-
# sections headings were successfully written out.
35-
assert "<<<gerrit_version:sep(0)>>>" in captured.out
35+
# section heading was written out with a cache timestamp.
36+
assert re.search(r"<<<gerrit_version:sep\(0\):cached\([\d.]+,60\)>>>", captured.out)
3637

3738
# cache is being used on second run.
3839
agent.run(ctx)
@@ -52,6 +53,11 @@ def test_run_agent_with_no_cache(capsys: pytest.CaptureFixture[str]) -> None:
5253
agent.run(ctx)
5354
second_run_output = capsys.readouterr().out
5455

56+
# section heading should not have :cached marker
57+
assert "<<<gerrit_version:sep(0)>>>" in first_run_output
58+
assert "<<<gerrit_version:sep(0)>>>" in second_run_output
59+
60+
# data should be different between runs
5561
assert first_run_output != second_run_output
5662

5763

0 commit comments

Comments
 (0)