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 com .github .benmanes .caffeine .cache .Cache ;
21+ import com .github .benmanes .caffeine .cache .Caffeine ;
22+ import org .apache .kafka .common .metrics .KafkaMetric ;
23+ import org .apache .kafka .common .metrics .Metrics ;
24+ import org .junit .jupiter .api .Test ;
25+
26+ import java .time .Duration ;
27+
28+ import static org .assertj .core .api .Assertions .assertThat ;
29+ import static org .assertj .core .api .InstanceOfAssertFactories .DOUBLE ;
30+ import static org .awaitility .Awaitility .await ;
31+
32+ class CaffeineCacheMetricsTest {
33+
34+ @ Test
35+ void testCacheHitMissMetric () {
36+ // Given
37+ final Metrics metrics = new Metrics ();
38+ final Cache <String , String > cache = Caffeine .newBuilder ()
39+ .recordStats ()
40+ .build ();
41+ new CaffeineCacheMetrics (metrics , cache );
42+ cache .put ("key1" , "value1" );
43+
44+ // When there is a cache hit
45+ var value = cache .getIfPresent ("key1" ); // hit
46+ assertThat (value ).isEqualTo ("value1" );
47+
48+ // Then
49+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_HIT_COUNT ).metricValue ())
50+ .asInstanceOf (DOUBLE )
51+ .isOne ();
52+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_HIT_RATE ).metricValue ())
53+ .asInstanceOf (DOUBLE )
54+ .isOne ();
55+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_MISS_RATE ).metricValue ())
56+ .asInstanceOf (DOUBLE )
57+ .isZero ();
58+
59+ // When there is a cache miss
60+ value = cache .getIfPresent ("key2" ); // miss
61+ assertThat (value ).isNull ();
62+
63+ // Then
64+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_MISS_COUNT ).metricValue ())
65+ .asInstanceOf (DOUBLE )
66+ .isOne ();
67+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_HIT_RATE ).metricValue ())
68+ .asInstanceOf (DOUBLE )
69+ .isEqualTo (0.5 ); // 1 hit, 1 miss
70+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_MISS_RATE ).metricValue ())
71+ .asInstanceOf (DOUBLE )
72+ .isEqualTo (0.5 ); // 1 hit, 1 miss
73+
74+ // overall load is zero
75+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_AVG_LOAD_PENALTY_NANOSECONDS ).metricValue ())
76+ .asInstanceOf (DOUBLE )
77+ .isZero ();
78+ }
79+
80+ @ Test
81+ void testCacheSizeAndEviction () {
82+ // Given
83+ final Metrics metrics = new Metrics ();
84+ final Cache <String , String > cache = Caffeine .newBuilder ()
85+ .maximumSize (1 )
86+ .recordStats ()
87+ .build ();
88+ new CaffeineCacheMetrics (metrics , cache );
89+
90+ // Initially, cache size should be zero
91+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_SIZE ).metricValue ())
92+ .asInstanceOf (DOUBLE )
93+ .isZero ();
94+
95+ // When adding entries to the cache
96+ cache .put ("key1" , "value1" );
97+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_SIZE ).metricValue ())
98+ .asInstanceOf (DOUBLE )
99+ .isOne ();
100+
101+ // Adding another entry should evict the first one due to max size of 1
102+ cache .put ("key2" , "value2" ); // This should evict key1
103+ await ().atMost (Duration .ofSeconds (10 )).until (() ->
104+ // size should eventually come back to 1
105+ (double ) getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_SIZE ).metricValue () == 1.0
106+ );
107+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_EVICTION_COUNT ).metricValue ())
108+ .asInstanceOf (DOUBLE )
109+ .isOne ();
110+ }
111+
112+ @ Test
113+ void testCacheLoadTime () {
114+ final Metrics metrics = new Metrics ();
115+ final Cache <String , String > cache = Caffeine .newBuilder ()
116+ .recordStats ()
117+ .build ();
118+ new CaffeineCacheMetrics (metrics , cache );
119+
120+ // Simulate cache loads with some computation time
121+ cache .get ("key1" , key -> {
122+ try {
123+ Thread .sleep (10 ); // Simulate load time
124+ } catch (InterruptedException e ) {
125+ Thread .currentThread ().interrupt ();
126+ }
127+ return "value1" ;
128+ });
129+
130+ assertThat (getMetric (metrics , CaffeineCacheMetricsRegistry .CACHE_AVG_LOAD_PENALTY_NANOSECONDS ).metricValue ())
131+ .asInstanceOf (DOUBLE )
132+ .isGreaterThan (0.0 );
133+ }
134+
135+ private static KafkaMetric getMetric (Metrics metrics , String metricName ) {
136+ return metrics .metric (metrics .metricName (metricName , CaffeineCacheMetricsRegistry .METRIC_GROUP ));
137+ }
138+ }
0 commit comments