1+ /*
2+ * Inkless
3+ * Copyright (C) 2024 - 2025 Aiven OY
4+ *
5+ * This program is free software: you can redistribute it and/or modify
6+ * it under the terms of the GNU Affero General Public License as published by
7+ * the Free Software Foundation, either version 3 of the License, or
8+ * (at your option) any later version.
9+ *
10+ * This program is distributed in the hope that it will be useful,
11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+ * GNU Affero General Public License for more details.
14+ *
15+ * You should have received a copy of the GNU Affero General Public License
16+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
17+ */
18+ package io .aiven .inkless .cache ;
19+
20+ import org .apache .kafka .common .metrics .KafkaMetric ;
21+ import org .apache .kafka .common .metrics .Metrics ;
22+
23+ import com .github .benmanes .caffeine .cache .Cache ;
24+ import com .github .benmanes .caffeine .cache .Caffeine ;
25+
26+ import org .junit .jupiter .api .Test ;
27+
28+ import java .time .Duration ;
29+
30+ import static org .assertj .core .api .Assertions .assertThat ;
31+ import static org .assertj .core .api .InstanceOfAssertFactories .DOUBLE ;
32+ import static org .awaitility .Awaitility .await ;
33+
34+ class CaffeineCacheMetricsTest {
35+
36+ @ Test
37+ void testCacheHitMissMetric () {
38+ // Given
39+ final Metrics metrics = new Metrics ();
40+ final Cache <String , String > cache = Caffeine .newBuilder ()
41+ .recordStats ()
42+ .build ();
43+ new CaffeineCacheMetrics (metrics , cache );
44+ cache .put ("key1" , "value1" );
45+
46+ // When there is a cache hit
47+ var value = cache .getIfPresent ("key1" ); // hit
48+ assertThat (value ).isEqualTo ("value1" );
49+
50+ // Then
51+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_HIT_COUNT ).metricValue ())
52+ .asInstanceOf (DOUBLE )
53+ .isOne ();
54+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_HIT_RATE ).metricValue ())
55+ .asInstanceOf (DOUBLE )
56+ .isOne ();
57+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_MISS_RATE ).metricValue ())
58+ .asInstanceOf (DOUBLE )
59+ .isZero ();
60+
61+ // When there is a cache miss
62+ value = cache .getIfPresent ("key2" ); // miss
63+ assertThat (value ).isNull ();
64+
65+ // Then
66+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_MISS_COUNT ).metricValue ())
67+ .asInstanceOf (DOUBLE )
68+ .isOne ();
69+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_HIT_RATE ).metricValue ())
70+ .asInstanceOf (DOUBLE )
71+ .isEqualTo (0.5 ); // 1 hit, 1 miss
72+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_MISS_RATE ).metricValue ())
73+ .asInstanceOf (DOUBLE )
74+ .isEqualTo (0.5 ); // 1 hit, 1 miss
75+
76+ // overall load is zero
77+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_AVG_LOAD_PENALTY_NANOSECONDS ).metricValue ())
78+ .asInstanceOf (DOUBLE )
79+ .isZero ();
80+ }
81+
82+ @ Test
83+ void testCacheSizeAndEviction () {
84+ // Given
85+ final Metrics metrics = new Metrics ();
86+ final Cache <String , String > cache = Caffeine .newBuilder ()
87+ .maximumSize (1 )
88+ .recordStats ()
89+ .build ();
90+ new CaffeineCacheMetrics (metrics , cache );
91+
92+ // Initially, cache size should be zero
93+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_SIZE ).metricValue ())
94+ .asInstanceOf (DOUBLE )
95+ .isZero ();
96+
97+ // When adding entries to the cache
98+ cache .put ("key1" , "value1" );
99+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_SIZE ).metricValue ())
100+ .asInstanceOf (DOUBLE )
101+ .isOne ();
102+
103+ // Adding another entry should evict the first one due to max size of 1
104+ cache .put ("key2" , "value2" ); // This should evict key1
105+ await ().atMost (Duration .ofSeconds (10 )).until (() ->
106+ // size should eventually come back to 1
107+ (double ) getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_SIZE ).metricValue () == 1.0
108+ );
109+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_EVICTION_COUNT ).metricValue ())
110+ .asInstanceOf (DOUBLE )
111+ .isOne ();
112+ }
113+
114+ @ Test
115+ void testCacheLoadTime () {
116+ final Metrics metrics = new Metrics ();
117+ final Cache <String , String > cache = Caffeine .newBuilder ()
118+ .recordStats ()
119+ .build ();
120+ new CaffeineCacheMetrics (metrics , cache );
121+
122+ // Simulate cache loads with some computation time
123+ cache .get ("key1" , key -> {
124+ try {
125+ Thread .sleep (10 ); // Simulate load time
126+ } catch (InterruptedException e ) {
127+ Thread .currentThread ().interrupt ();
128+ }
129+ return "value1" ;
130+ });
131+
132+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_AVG_LOAD_PENALTY_NANOSECONDS ).metricValue ())
133+ .asInstanceOf (DOUBLE )
134+ .isGreaterThan (0.0 );
135+ }
136+
137+ private static KafkaMetric getMetric (Metrics metrics , String metricName ) {
138+ return metrics .metric (metrics .metricName (metricName , CaffeineCacheMetricsRegistry .METRIC_GROUP ));
139+ }
140+ }
0 commit comments