View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.commons.collections;
18  
19  import static org.junit.jupiter.api.Assertions.*;
20  import static org.apache.juneau.commons.collections.CacheMode.*;
21  import static org.apache.juneau.junit.bct.BctAssertions.*;
22  
23  import java.util.*;
24  import java.util.concurrent.*;
25  import java.util.concurrent.atomic.*;
26  
27  import org.apache.juneau.*;
28  import org.junit.jupiter.api.*;
29  
30  class Cache2_Test extends TestBase {
31  
32  	//====================================================================================================
33  	// a - Basic cache operations with default supplier
34  	//====================================================================================================
35  
36  	@Test
37  	void a01_defaultSupplier_cacheMiss() {
38  		var callCount = new AtomicInteger();
39  		var x = Cache2.of(String.class, Integer.class, String.class)
40  			.supplier((k1, k2) -> {
41  				callCount.incrementAndGet();
42  				return k1 + ":" + k2;
43  			})
44  			.build();
45  
46  		var result = x.get("user", 123);
47  
48  		assertEquals("user:123", result);
49  		assertEquals(1, callCount.get());
50  		assertSize(1, x);
51  		assertEquals(0, x.getCacheHits());
52  	}
53  
54  	@Test
55  	void a02_defaultSupplier_cacheHit() {
56  		var callCount = new AtomicInteger();
57  		var x = Cache2.of(String.class, Integer.class, String.class)
58  			.supplier((k1, k2) -> {
59  				callCount.incrementAndGet();
60  				return k1 + ":" + k2;
61  			})
62  			.build();
63  
64  		// First call - cache miss
65  		var result1 = x.get("user", 123);
66  
67  		// Second call - cache hit
68  		var result2 = x.get("user", 123);
69  
70  		assertEquals("user:123", result1);
71  		assertEquals("user:123", result2);
72  		assertSame(result1, result2);
73  		assertEquals(1, callCount.get()); // Supplier only called once
74  		assertSize(1, x);
75  		assertEquals(1, x.getCacheHits());
76  	}
77  
78  	@Test
79  	void a03_multipleKeys() {
80  		var callCount = new AtomicInteger();
81  		var x = Cache2.of(String.class, Integer.class, String.class)
82  			.supplier((k1, k2) -> {
83  				callCount.incrementAndGet();
84  				return k1 + ":" + k2;
85  			})
86  			.build();
87  
88  		var v1 = x.get("user", 123);
89  		var v2 = x.get("admin", 456);
90  		var v3 = x.get("guest", 789);
91  
92  		assertEquals("user:123", v1);
93  		assertEquals("admin:456", v2);
94  		assertEquals("guest:789", v3);
95  		assertEquals(3, callCount.get());
96  		assertSize(3, x);
97  		assertEquals(0, x.getCacheHits());
98  	}
99  
100 	//====================================================================================================
101 	// b - Override supplier
102 	//====================================================================================================
103 
104 	@Test
105 	void b01_overrideSupplier() {
106 		var x = Cache2.of(String.class, Integer.class, String.class)
107 			.supplier((k1, k2) -> k1 + ":" + k2) // Default
108 			.build();
109 
110 		// Use override supplier
111 		var result = x.get("user", 123, () -> "OVERRIDE");
112 
113 		assertEquals("OVERRIDE", result);
114 		assertSize(1, x);
115 	}
116 
117 	@Test
118 	void b02_overrideSupplier_cachesResult() {
119 		var defaultCalls = new AtomicInteger();
120 		var overrideCalls = new AtomicInteger();
121 		var x = Cache2.of(String.class, Integer.class, String.class)
122 			.supplier((k1, k2) -> {
123 				defaultCalls.incrementAndGet();
124 				return "DEFAULT";
125 			})
126 			.build();
127 
128 		// First call with override
129 		var result1 = x.get("user", 123, () -> {
130 			overrideCalls.incrementAndGet();
131 			return "OVERRIDE";
132 		});
133 
134 		// Second call with default supplier - should use cached value
135 		var result2 = x.get("user", 123);
136 
137 		assertEquals("OVERRIDE", result1);
138 		assertEquals("OVERRIDE", result2); // Cached value, not default
139 		assertEquals(0, defaultCalls.get());
140 		assertEquals(1, overrideCalls.get());
141 		assertEquals(1, x.getCacheHits());
142 	}
143 
144 	//====================================================================================================
145 	// c - Null key validation
146 	//====================================================================================================
147 
148 	@Test
149 	void c01_nullKeys_defaultSupplier() {
150 		var x = Cache2.of(String.class, Integer.class, String.class)
151 			.supplier((k1, k2) -> "value-" + k1 + "-" + k2)
152 			.build();
153 
154 		// Null keys are now allowed
155 		assertEquals("value-null-123", x.get(null, 123));
156 		assertEquals("value-user-null", x.get("user", null));
157 		assertEquals("value-null-null", x.get(null, null));
158 
159 		// Cached values should be returned on subsequent calls
160 		assertEquals("value-null-123", x.get(null, 123));
161 		assertEquals("value-user-null", x.get("user", null));
162 	}
163 
164 	@Test
165 	void c02_nullKeys_overrideSupplier() {
166 		var x = Cache2.of(String.class, Integer.class, String.class).build();
167 
168 		// Null keys are now allowed
169 		assertEquals("value", x.get(null, 123, () -> "value"));
170 		assertEquals("value", x.get("user", null, () -> "value"));
171 		assertEquals("value", x.get(null, null, () -> "value"));
172 
173 		// Cached values should be returned on subsequent calls
174 		assertEquals("value", x.get(null, 123, () -> "should-not-be-called"));
175 		assertEquals("value", x.get("user", null, () -> "should-not-be-called"));
176 	}
177 
178 	//====================================================================================================
179 	// d - Disabled caching
180 	//====================================================================================================
181 
182 	@Test
183 	void d01_disableCaching_alwaysCallsSupplier() {
184 		var defaultCallCount = new AtomicInteger();
185 		var overrideCallCount = new AtomicInteger();
186 		var x = Cache2.of(String.class, Integer.class, String.class)
187 			.cacheMode(NONE)
188 			.supplier((k1, k2) -> {
189 				defaultCallCount.incrementAndGet();
190 				return k1 + ":" + k2;
191 			})
192 			.build();
193 
194 		// First call with override supplier
195 		assertEquals("user:123", x.get("user", 123, () -> {
196 			overrideCallCount.incrementAndGet();
197 			return "user:123";
198 		}));
199 		assertEquals(0, defaultCallCount.get());
200 		assertEquals(1, overrideCallCount.get());
201 
202 		// Second call with override - supplier called again (no caching)
203 		assertEquals("user:123", x.get("user", 123, () -> {
204 			overrideCallCount.incrementAndGet();
205 			return "user:123";
206 		}));
207 		assertEquals(0, defaultCallCount.get());
208 		assertEquals(2, overrideCallCount.get());
209 
210 		// Using default supplier
211 		assertEquals("user:456", x.get("user", 456));
212 		assertEquals(1, defaultCallCount.get());
213 
214 		assertEquals("user:456", x.get("user", 456));
215 		assertEquals(2, defaultCallCount.get()); // Called again
216 
217 		assertEmpty(x); // Nothing cached
218 	}
219 
220 	//====================================================================================================
221 	// d2 - Weak cache mode
222 	//====================================================================================================
223 
224 	@Test
225 	void d02_weakMode_basicCaching() {
226 		var callCount = new AtomicInteger();
227 		var x = Cache2.of(String.class, Integer.class, String.class)
228 			.cacheMode(WEAK)
229 			.supplier((k1, k2) -> {
230 				callCount.incrementAndGet();
231 				return k1 + ":" + k2;
232 			})
233 			.build();
234 
235 		// First call - cache miss
236 		var result1 = x.get("user", 123);
237 
238 		// Second call - cache hit
239 		var result2 = x.get("user", 123);
240 
241 		assertEquals("user:123", result1);
242 		assertEquals("user:123", result2);
243 		assertSame(result1, result2);
244 		assertEquals(1, callCount.get()); // Supplier only called once
245 		assertSize(1, x);
246 		assertEquals(1, x.getCacheHits());
247 	}
248 
249 	@Test
250 	void d03_weakMode_multipleKeys() {
251 		var x = Cache2.of(String.class, Integer.class, String.class)
252 			.cacheMode(WEAK)
253 			.supplier((k1, k2) -> k1 + ":" + k2)
254 			.build();
255 
256 		x.get("user", 123);
257 		x.get("admin", 456);
258 		x.get("guest", 789);
259 
260 		assertSize(3, x);
261 		assertEquals(0, x.getCacheHits());
262 
263 		// Verify all cached
264 		assertEquals("user:123", x.get("user", 123));
265 		assertEquals("admin:456", x.get("admin", 456));
266 		assertEquals("guest:789", x.get("guest", 789));
267 		assertEquals(3, x.getCacheHits());
268 	}
269 
270 	@Test
271 	void d04_weakMode_clear() {
272 		var x = Cache2.of(String.class, Integer.class, String.class)
273 			.cacheMode(WEAK)
274 			.supplier((k1, k2) -> k1 + ":" + k2)
275 			.build();
276 
277 		x.get("user", 123);
278 		x.get("admin", 456);
279 		assertSize(2, x);
280 
281 		x.clear();
282 		assertEmpty(x);
283 	}
284 
285 	@Test
286 	void d05_weakMode_maxSize() {
287 		var x = Cache2.of(String.class, Integer.class, String.class)
288 			.cacheMode(WEAK)
289 			.maxSize(2)
290 			.supplier((k1, k2) -> k1 + ":" + k2)
291 			.build();
292 
293 		x.get("k1", 1);
294 		x.get("k2", 2);
295 		assertSize(2, x);
296 
297 		// 3rd item doesn't trigger eviction yet
298 		x.get("k3", 3);
299 		assertSize(3, x);
300 
301 		// 4th item triggers eviction
302 		x.get("k4", 4);
303 		assertSize(1, x);
304 	}
305 
306 	@Test
307 	void d06_weakMethod_basicCaching() {
308 		// Test the weak() convenience method
309 		var callCount = new AtomicInteger();
310 		var x = Cache2.of(String.class, Integer.class, String.class)
311 			.weak()
312 			.supplier((k1, k2) -> {
313 				callCount.incrementAndGet();
314 				return k1 + ":" + k2;
315 			})
316 			.build();
317 
318 		// First call - cache miss
319 		var result1 = x.get("user", 123);
320 
321 		// Second call - cache hit
322 		var result2 = x.get("user", 123);
323 
324 		assertEquals("user:123", result1);
325 		assertEquals("user:123", result2);
326 		assertSame(result1, result2);
327 		assertEquals(1, callCount.get()); // Supplier only called once
328 		assertSize(1, x);
329 		assertEquals(1, x.getCacheHits());
330 	}
331 
332 	@Test
333 	void d07_weakMethod_chaining() {
334 		// Test that weak() can be chained with other builder methods
335 		var x = Cache2.of(String.class, Integer.class, String.class)
336 			.weak()
337 			.maxSize(100)
338 			.supplier((k1, k2) -> k1 + ":" + k2)
339 			.build();
340 
341 		var result = x.get("user", 123);
342 		assertEquals("user:123", result);
343 		assertSize(1, x);
344 	}
345 
346 	//====================================================================================================
347 	// e - Max size and eviction
348 	//====================================================================================================
349 
350 	@Test
351 	void e01_maxSize_clearsWhenExceeded() {
352 		var x = Cache2.of(String.class, Integer.class, String.class)
353 			.maxSize(2)
354 			.supplier((k1, k2) -> k1 + ":" + k2)
355 			.build();
356 
357 		x.get("k1", 1);
358 		x.get("k2", 2);
359 		assertSize(2, x);
360 
361 		// Adding a third entry doesn't exceed maxSize (2 > 2 is false)
362 		x.get("k3", 3);
363 		assertSize(3, x);
364 
365 		// Fourth entry triggers clear (3 > 2 is true)
366 		x.get("k4", 4);
367 		assertSize(1, x); // Only the new entry
368 	}
369 
370 	//====================================================================================================
371 	// f - Cache hits tracking
372 	//====================================================================================================
373 
374 	@Test
375 	void f01_cacheHitsTracking() {
376 		var x = Cache2.of(String.class, Integer.class, String.class)
377 			.supplier((k1, k2) -> k1 + ":" + k2)
378 			.build();
379 
380 		assertEquals(0, x.getCacheHits());
381 
382 		x.get("user", 123); // Miss
383 		assertEquals(0, x.getCacheHits());
384 
385 		x.get("user", 123); // Hit
386 		assertEquals(1, x.getCacheHits());
387 
388 		x.get("admin", 456); // Miss
389 		assertEquals(1, x.getCacheHits());
390 
391 		x.get("user", 123); // Hit
392 		x.get("admin", 456); // Hit
393 		assertEquals(3, x.getCacheHits());
394 	}
395 
396 	//====================================================================================================
397 	// g - Clear operation
398 	//====================================================================================================
399 
400 	@Test
401 	void g01_clear() {
402 		var x = Cache2.of(String.class, Integer.class, String.class)
403 			.supplier((k1, k2) -> k1 + ":" + k2)
404 			.build();
405 
406 		x.get("user", 123);
407 		x.get("admin", 456);
408 		assertSize(2, x);
409 
410 		x.clear();
411 
412 		assertEmpty(x);
413 	}
414 
415 	//====================================================================================================
416 	// h - No default supplier
417 	//====================================================================================================
418 
419 	@Test
420 	void h01_noDefaultSupplier_requiresOverride() {
421 		var x = Cache2.of(String.class, Integer.class, String.class).build();
422 
423 		assertThrows(NullPointerException.class, () -> x.get("user", 123));
424 	}
425 
426 	@Test
427 	void h02_noDefaultSupplier_worksWithOverride() {
428 		var x = Cache2.of(String.class, Integer.class, String.class).build();
429 
430 		var result = x.get("user", 123, () -> "CUSTOM");
431 
432 		assertEquals("CUSTOM", result);
433 		assertSize(1, x);
434 	}
435 
436 	//====================================================================================================
437 	// i - put() method
438 	//====================================================================================================
439 
440 	@Test
441 	void i01_put_directInsertion() {
442 		var x = Cache2.of(String.class, Integer.class, String.class).build();
443 		var previous = x.put("user", 123, "value1");
444 		assertNull(previous);
445 		assertEquals("value1", x.get("user", 123, () -> "should not be called"));
446 		assertSize(1, x);
447 	}
448 
449 	@Test
450 	void i02_put_overwritesExisting() {
451 		var x = Cache2.of(String.class, Integer.class, String.class).build();
452 		x.put("user", 123, "value1");
453 		var previous = x.put("user", 123, "value2");
454 		assertEquals("value1", previous);
455 		assertEquals("value2", x.get("user", 123, () -> "should not be called"));
456 	}
457 
458 	@Test
459 	void i03_put_withNullValue() {
460 		var x = Cache2.of(String.class, Integer.class, String.class).build();
461 		x.put("user", 123, "value1");
462 		var previous = x.put("user", 123, null);
463 		assertEquals("value1", previous);
464 		assertFalse(x.containsKey("user", 123));
465 	}
466 
467 	@Test
468 	void i04_put_withNullValue_newKey() {
469 		var x = Cache2.of(String.class, Integer.class, String.class).build();
470 		var previous = x.put("user", 123, null);
471 		assertNull(previous);
472 		assertFalse(x.containsKey("user", 123));
473 		assertTrue(x.isEmpty());
474 	}
475 
476 	//====================================================================================================
477 	// j - isEmpty() method
478 	//====================================================================================================
479 
480 	@Test
481 	void j01_isEmpty_newCache() {
482 		var x = Cache2.of(String.class, Integer.class, String.class).build();
483 		assertTrue(x.isEmpty());
484 	}
485 
486 	@Test
487 	void j02_isEmpty_afterPut() {
488 		var x = Cache2.of(String.class, Integer.class, String.class).build();
489 		x.put("user", 123, "value");
490 		assertFalse(x.isEmpty());
491 	}
492 
493 	@Test
494 	void j03_isEmpty_afterClear() {
495 		var x = Cache2.of(String.class, Integer.class, String.class).build();
496 		x.put("user", 123, "value");
497 		x.clear();
498 		assertTrue(x.isEmpty());
499 	}
500 
501 	//====================================================================================================
502 	// k - containsKey() method
503 	//====================================================================================================
504 
505 	@Test
506 	void k01_containsKey_notPresent() {
507 		var x = Cache2.of(String.class, Integer.class, String.class).build();
508 		assertFalse(x.containsKey("user", 123));
509 	}
510 
511 	@Test
512 	void k02_containsKey_afterPut() {
513 		var x = Cache2.of(String.class, Integer.class, String.class).build();
514 		x.put("user", 123, "value");
515 		assertTrue(x.containsKey("user", 123));
516 		assertFalse(x.containsKey("user", 456));
517 	}
518 
519 	@Test
520 	void k03_containsKey_afterGet() {
521 		var x = Cache2.of(String.class, Integer.class, String.class)
522 			.supplier((k1, k2) -> k1 + ":" + k2)
523 			.build();
524 		x.get("user", 123);
525 		assertTrue(x.containsKey("user", 123));
526 	}
527 
528 	//====================================================================================================
529 	// l - remove() method
530 	//====================================================================================================
531 
532 	@Test
533 	void l02_remove_existingKey() {
534 		var x = Cache2.of(String.class, Integer.class, String.class).build();
535 		x.put("user", 123, "value1");
536 		var removed = x.remove("user", 123);
537 		assertEquals("value1", removed);
538 		assertFalse(x.containsKey("user", 123));
539 	}
540 
541 	@Test
542 	void l03_remove_nonExistentKey() {
543 		var x = Cache2.of(String.class, Integer.class, String.class).build();
544 		var removed = x.remove("user", 123);
545 		assertNull(removed);
546 	}
547 
548 	//====================================================================================================
549 	// m - containsValue() method
550 	//====================================================================================================
551 
552 	@Test
553 	void m02_containsValue_present() {
554 		var x = Cache2.of(String.class, Integer.class, String.class).build();
555 		x.put("user", 123, "value1");
556 		x.put("admin", 456, "value2");
557 		assertTrue(x.containsValue("value1"));
558 		assertTrue(x.containsValue("value2"));
559 		assertFalse(x.containsValue("value3"));
560 	}
561 
562 	@Test
563 	void m03_containsValue_notPresent() {
564 		var x = Cache2.of(String.class, Integer.class, String.class).build();
565 		assertFalse(x.containsValue("value1"));
566 	}
567 
568 	@Test
569 	void m04_containsValue_nullValue() {
570 		var x = Cache2.of(String.class, Integer.class, String.class).build();
571 		// Null values can't be cached, so containsValue(null) should return false
572 		x.get("user", 123, () -> null);
573 		assertFalse(x.containsValue(null));
574 		// Also test with empty cache
575 		var x2 = Cache2.of(String.class, Integer.class, String.class).build();
576 		assertFalse(x2.containsValue(null));
577 	}
578 
579 	//====================================================================================================
580 	// n - logOnExit() builder methods
581 	//====================================================================================================
582 
583 	@Test
584 	void n02_logOnExit_withStringId() {
585 		var x = Cache2.of(String.class, Integer.class, String.class)
586 			.logOnExit("TestCache2")
587 			.supplier((k1, k2) -> k1 + ":" + k2)
588 			.build();
589 		x.get("user", 123);
590 		assertSize(1, x);
591 	}
592 
593 	@Test
594 	void n03_logOnExit_withBooleanTrue() {
595 		var x = Cache2.of(String.class, Integer.class, String.class)
596 			.logOnExit(true, "MyCache2")
597 			.supplier((k1, k2) -> k1 + ":" + k2)
598 			.build();
599 		x.get("user", 123);
600 		assertSize(1, x);
601 	}
602 
603 	@Test
604 	void n04_logOnExit_withBooleanFalse() {
605 		var x = Cache2.of(String.class, Integer.class, String.class)
606 			.logOnExit(false, "DisabledCache2")
607 			.supplier((k1, k2) -> k1 + ":" + k2)
608 			.build();
609 		x.get("user", 123);
610 		assertSize(1, x);
611 	}
612 
613 	//====================================================================================================
614 	// l - create() static method
615 	//====================================================================================================
616 
617 	@Test
618 	void l01_create_basic() {
619 		var x = Cache2.<String, Integer, String>create()
620 			.supplier((k1, k2) -> k1 + ":" + k2)
621 			.build();
622 
623 		var result = x.get("user", 123);
624 		assertEquals("user:123", result);
625 		assertSize(1, x);
626 	}
627 
628 	//====================================================================================================
629 	// m - disableCaching() builder method
630 	//====================================================================================================
631 
632 	@Test
633 	void m01_disableCaching() {
634 		var callCount = new AtomicInteger();
635 		var x = Cache2.of(String.class, Integer.class, String.class)
636 			.cacheMode(NONE)
637 			.supplier((k1, k2) -> {
638 				callCount.incrementAndGet();
639 				return "value";
640 			})
641 			.build();
642 
643 		x.get("user", 123);
644 		x.get("user", 123);
645 		assertEquals(2, callCount.get());
646 		assertTrue(x.isEmpty());
647 	}
648 
649 	//====================================================================================================
650 	// n - Null value handling
651 	//====================================================================================================
652 
653 	@Test
654 	void n01_nullValue_notCached() {
655 		var callCount = new AtomicInteger();
656 		var x = Cache2.of(String.class, Integer.class, String.class).build();
657 
658 		var result1 = x.get("user", 123, () -> {
659 			callCount.incrementAndGet();
660 			return null;
661 		});
662 		assertNull(result1);
663 
664 		var result2 = x.get("user", 123, () -> {
665 			callCount.incrementAndGet();
666 			return null;
667 		});
668 		assertNull(result2);
669 		assertEquals(2, callCount.get());
670 		assertTrue(x.isEmpty());
671 	}
672 
673 	//====================================================================================================
674 	// o - Thread-local cache mode
675 	//====================================================================================================
676 
677 	@Test
678 	void o01_threadLocal_basicCaching() {
679 		var callCount = new AtomicInteger();
680 		var x = Cache2.of(String.class, Integer.class, String.class)
681 			.threadLocal()
682 			.supplier((k1, k2) -> {
683 				callCount.incrementAndGet();
684 				return k1 + ":" + k2;
685 			})
686 			.build();
687 
688 		// First call - cache miss
689 		var result1 = x.get("user", 123);
690 
691 		// Second call - cache hit
692 		var result2 = x.get("user", 123);
693 
694 		assertEquals("user:123", result1);
695 		assertEquals("user:123", result2);
696 		assertSame(result1, result2);
697 		assertEquals(1, callCount.get()); // Supplier only called once
698 		assertSize(1, x);
699 		assertEquals(1, x.getCacheHits());
700 	}
701 
702 	@Test
703 	void o02_threadLocal_eachThreadHasOwnCache() throws Exception {
704 		var x = Cache2.of(String.class, Integer.class, String.class)
705 			.threadLocal()
706 			.build();
707 		var executor = java.util.concurrent.Executors.newFixedThreadPool(2);
708 		var threadValues = new ConcurrentHashMap<Thread, String>();
709 
710 		// Each thread caches ("user", 123) with its own value
711 		var future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
712 			var value = x.get("user", 123, () -> "thread1-value");
713 			threadValues.put(Thread.currentThread(), value);
714 		}, executor);
715 
716 		var future2 = java.util.concurrent.CompletableFuture.runAsync(() -> {
717 			var value = x.get("user", 123, () -> "thread2-value");
718 			threadValues.put(Thread.currentThread(), value);
719 		}, executor);
720 
721 		java.util.concurrent.CompletableFuture.allOf(future1, future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
722 
723 		// Verify both threads cached their own values
724 		assertEquals(2, threadValues.size());
725 		assertTrue(threadValues.containsValue("thread1-value"));
726 		assertTrue(threadValues.containsValue("thread2-value"));
727 
728 		// Verify each thread's cache is independent - same thread should get same cached value
729 		var threadValues2 = new ConcurrentHashMap<Thread, String>();
730 		var threads = new ArrayList<>(threadValues.keySet());
731 
732 		future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
733 			var value = x.get("user", 123, () -> "should-not-be-called");
734 			threadValues2.put(Thread.currentThread(), value);
735 		}, executor);
736 
737 		future2 = java.util.concurrent.CompletableFuture.runAsync(() -> {
738 			var value = x.get("user", 123, () -> "should-not-be-called");
739 			threadValues2.put(Thread.currentThread(), value);
740 		}, executor);
741 
742 		java.util.concurrent.CompletableFuture.allOf(future1, future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
743 
744 		// Each thread should get its own cached value (same as what it cached before)
745 		for (var thread : threads) {
746 			if (threadValues2.containsKey(thread)) {
747 				assertEquals(threadValues.get(thread), threadValues2.get(thread),
748 					"Thread " + thread + " should get its own cached value");
749 			}
750 		}
751 
752 		executor.shutdown();
753 	}
754 
755 	@Test
756 	void o03_threadLocal_multipleKeys() {
757 		var x = Cache2.of(String.class, Integer.class, String.class)
758 			.threadLocal()
759 			.supplier((k1, k2) -> k1 + ":" + k2)
760 			.build();
761 
762 		x.get("user", 123);
763 		x.get("admin", 456);
764 		x.get("guest", 789);
765 
766 		assertSize(3, x);
767 		assertEquals(0, x.getCacheHits());
768 
769 		// Verify all cached
770 		assertEquals("user:123", x.get("user", 123));
771 		assertEquals("admin:456", x.get("admin", 456));
772 		assertEquals("guest:789", x.get("guest", 789));
773 		assertEquals(3, x.getCacheHits());
774 	}
775 
776 	@Test
777 	void o04_threadLocal_clear() {
778 		var x = Cache2.of(String.class, Integer.class, String.class)
779 			.threadLocal()
780 			.supplier((k1, k2) -> k1 + ":" + k2)
781 			.build();
782 
783 		x.get("user", 123);
784 		x.get("admin", 456);
785 		assertSize(2, x);
786 
787 		x.clear();
788 		assertEmpty(x);
789 	}
790 
791 	@Test
792 	void o05_threadLocal_maxSize() {
793 		var x = Cache2.of(String.class, Integer.class, String.class)
794 			.threadLocal()
795 			.maxSize(2)
796 			.supplier((k1, k2) -> k1 + ":" + k2)
797 			.build();
798 
799 		x.get("k1", 1);
800 		x.get("k2", 2);
801 		assertSize(2, x);
802 
803 		// 3rd item doesn't trigger eviction yet
804 		x.get("k3", 3);
805 		assertSize(3, x);
806 
807 		// 4th item triggers eviction
808 		x.get("k4", 4);
809 		assertSize(1, x);
810 	}
811 
812 	//====================================================================================================
813 	// p - Thread-local + weak mode combination
814 	//====================================================================================================
815 
816 	@Test
817 	void p01_threadLocal_weakMode_basicCaching() {
818 		var callCount = new AtomicInteger();
819 		var x = Cache2.of(String.class, Integer.class, String.class)
820 			.threadLocal()
821 			.cacheMode(WEAK)
822 			.supplier((k1, k2) -> {
823 				callCount.incrementAndGet();
824 				return k1 + ":" + k2;
825 			})
826 			.build();
827 
828 		// First call - cache miss
829 		var result1 = x.get("user", 123);
830 
831 		// Second call - cache hit
832 		var result2 = x.get("user", 123);
833 
834 		assertEquals("user:123", result1);
835 		assertEquals("user:123", result2);
836 		assertSame(result1, result2);
837 		assertEquals(1, callCount.get()); // Supplier only called once
838 		assertSize(1, x);
839 		assertEquals(1, x.getCacheHits());
840 	}
841 
842 	@Test
843 	void p02_threadLocal_weakMode_eachThreadHasOwnCache() throws Exception {
844 		var x = Cache2.of(String.class, Integer.class, String.class)
845 			.threadLocal()
846 			.cacheMode(WEAK)
847 			.build();
848 		var executor = java.util.concurrent.Executors.newFixedThreadPool(2);
849 		var threadValues = new ConcurrentHashMap<Thread, String>();
850 
851 		// Each thread caches ("user", 123) with its own value
852 		var future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
853 			var value = x.get("user", 123, () -> "thread1-value");
854 			threadValues.put(Thread.currentThread(), value);
855 		}, executor);
856 
857 		var future2 = java.util.concurrent.CompletableFuture.runAsync(() -> {
858 			var value = x.get("user", 123, () -> "thread2-value");
859 			threadValues.put(Thread.currentThread(), value);
860 		}, executor);
861 
862 		java.util.concurrent.CompletableFuture.allOf(future1, future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
863 
864 		// Verify both threads cached their own values
865 		assertEquals(2, threadValues.size());
866 		assertTrue(threadValues.containsValue("thread1-value"));
867 		assertTrue(threadValues.containsValue("thread2-value"));
868 
869 		executor.shutdown();
870 	}
871 
872 	@Test
873 	void p03_threadLocal_weakMode_clear() {
874 		var x = Cache2.of(String.class, Integer.class, String.class)
875 			.threadLocal()
876 			.cacheMode(WEAK)
877 			.supplier((k1, k2) -> k1 + ":" + k2)
878 			.build();
879 
880 		x.get("user", 123);
881 		x.get("admin", 456);
882 		assertSize(2, x);
883 
884 		x.clear();
885 		assertEmpty(x);
886 	}
887 
888 	@Test
889 	void p04_threadLocal_weakMode_maxSize() {
890 		var x = Cache2.of(String.class, Integer.class, String.class)
891 			.threadLocal()
892 			.cacheMode(WEAK)
893 			.maxSize(2)
894 			.supplier((k1, k2) -> k1 + ":" + k2)
895 			.build();
896 
897 		x.get("k1", 1);
898 		x.get("k2", 2);
899 		assertSize(2, x);
900 
901 		// 3rd item doesn't trigger eviction yet
902 		x.get("k3", 3);
903 		assertSize(3, x);
904 
905 		// 4th item triggers eviction
906 		x.get("k4", 4);
907 		assertSize(1, x);
908 	}
909 }
910