1 /* 2 * Copyright (C) 2017 The Guava Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.common.cache; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static org.junit.Assert.assertThrows; 21 22 import com.google.common.util.concurrent.UncheckedExecutionException; 23 import java.util.ArrayList; 24 import java.util.List; 25 import java.util.Queue; 26 import java.util.concurrent.ConcurrentLinkedQueue; 27 import java.util.concurrent.ExecutionException; 28 import java.util.concurrent.TimeUnit; 29 import java.util.function.IntConsumer; 30 import java.util.stream.IntStream; 31 import junit.framework.TestCase; 32 33 /** Test Java8 map.compute in concurrent cache context. */ 34 public class LocalCacheMapComputeTest extends TestCase { 35 final int count = 10000; 36 final String delimiter = "-"; 37 final String key = "key"; 38 Cache<String, String> cache; 39 40 // helper doParallelCacheOp(int count, IntConsumer consumer)41 private static void doParallelCacheOp(int count, IntConsumer consumer) { 42 IntStream.range(0, count).parallel().forEach(consumer); 43 } 44 45 @Override setUp()46 public void setUp() throws Exception { 47 super.setUp(); 48 this.cache = 49 CacheBuilder.newBuilder() 50 .expireAfterAccess(500000, TimeUnit.MILLISECONDS) 51 .maximumSize(count) 52 .build(); 53 } 54 testComputeIfAbsent()55 public void testComputeIfAbsent() { 56 // simultaneous insertion for same key, expect 1 winner 57 doParallelCacheOp( 58 count, 59 n -> { 60 cache.asMap().computeIfAbsent(key, k -> "value" + n); 61 }); 62 assertEquals(1, cache.size()); 63 } 64 testComputeIfAbsentEviction()65 public void testComputeIfAbsentEviction() { 66 // b/80241237 67 68 Cache<String, String> c = CacheBuilder.newBuilder().maximumSize(1).build(); 69 70 assertThat(c.asMap().computeIfAbsent("hash-1", k -> "")).isEqualTo(""); 71 assertThat(c.asMap().computeIfAbsent("hash-1", k -> "")).isEqualTo(""); 72 assertThat(c.asMap().computeIfAbsent("hash-1", k -> "")).isEqualTo(""); 73 assertThat(c.size()).isEqualTo(1); 74 assertThat(c.asMap().computeIfAbsent("hash-2", k -> "")).isEqualTo(""); 75 } 76 testComputeEviction()77 public void testComputeEviction() { 78 // b/80241237 79 80 Cache<String, String> c = CacheBuilder.newBuilder().maximumSize(1).build(); 81 82 assertThat(c.asMap().compute("hash-1", (k, v) -> "a")).isEqualTo("a"); 83 assertThat(c.asMap().compute("hash-1", (k, v) -> "b")).isEqualTo("b"); 84 assertThat(c.asMap().compute("hash-1", (k, v) -> "c")).isEqualTo("c"); 85 assertThat(c.size()).isEqualTo(1); 86 assertThat(c.asMap().computeIfAbsent("hash-2", k -> "")).isEqualTo(""); 87 } 88 testComputeIfPresent()89 public void testComputeIfPresent() { 90 cache.put(key, "1"); 91 // simultaneous update for same key, expect count successful updates 92 doParallelCacheOp( 93 count, 94 n -> { 95 cache.asMap().computeIfPresent(key, (k, v) -> v + delimiter + n); 96 }); 97 assertEquals(1, cache.size()); 98 assertThat(cache.getIfPresent(key).split(delimiter)).hasLength(count + 1); 99 } 100 testComputeIfPresentRemove()101 public void testComputeIfPresentRemove() { 102 List<RemovalNotification<Integer, Integer>> notifications = new ArrayList<>(); 103 Cache<Integer, Integer> cache = 104 CacheBuilder.newBuilder() 105 .removalListener( 106 new RemovalListener<Integer, Integer>() { 107 @Override 108 public void onRemoval(RemovalNotification<Integer, Integer> notification) { 109 notifications.add(notification); 110 } 111 }) 112 .build(); 113 cache.put(1, 2); 114 115 // explicitly remove the existing value 116 cache.asMap().computeIfPresent(1, (key, value) -> null); 117 assertThat(notifications).hasSize(1); 118 CacheTesting.checkEmpty(cache); 119 120 // ensure no zombie entry remains 121 cache.asMap().computeIfPresent(1, (key, value) -> null); 122 assertThat(notifications).hasSize(1); 123 CacheTesting.checkEmpty(cache); 124 } 125 testUpdates()126 public void testUpdates() { 127 cache.put(key, "1"); 128 // simultaneous update for same key, some null, some non-null 129 doParallelCacheOp( 130 count, 131 n -> { 132 cache.asMap().compute(key, (k, v) -> n % 2 == 0 ? v + delimiter + n : null); 133 }); 134 assertTrue(1 >= cache.size()); 135 } 136 testCompute()137 public void testCompute() { 138 cache.put(key, "1"); 139 // simultaneous deletion 140 doParallelCacheOp( 141 count, 142 n -> { 143 cache.asMap().compute(key, (k, v) -> null); 144 }); 145 assertEquals(0, cache.size()); 146 } 147 testComputeWithLoad()148 public void testComputeWithLoad() { 149 Queue<RemovalNotification<String, String>> notifications = new ConcurrentLinkedQueue<>(); 150 cache = 151 CacheBuilder.newBuilder() 152 .removalListener( 153 new RemovalListener<String, String>() { 154 @Override 155 public void onRemoval(RemovalNotification<String, String> notification) { 156 notifications.add(notification); 157 } 158 }) 159 .expireAfterAccess(500000, TimeUnit.MILLISECONDS) 160 .maximumSize(count) 161 .build(); 162 163 cache.put(key, "1"); 164 // simultaneous load and deletion 165 doParallelCacheOp( 166 count, 167 n -> { 168 try { 169 String unused = cache.get(key, () -> key); 170 cache.asMap().compute(key, (k, v) -> null); 171 } catch (ExecutionException e) { 172 throw new UncheckedExecutionException(e); 173 } 174 }); 175 176 CacheTesting.checkEmpty(cache); 177 for (RemovalNotification<String, String> entry : notifications) { 178 assertThat(entry.getKey()).isNotNull(); 179 assertThat(entry.getValue()).isNotNull(); 180 } 181 } 182 testComputeExceptionally()183 public void testComputeExceptionally() { 184 assertThrows( 185 RuntimeException.class, 186 () -> 187 doParallelCacheOp( 188 count, 189 n -> { 190 cache 191 .asMap() 192 .compute( 193 key, 194 (k, v) -> { 195 throw new RuntimeException(); 196 }); 197 })); 198 } 199 } 200