ConcurrentHashMultisetTest.java revision 1d580d0f6ee4f21eb309ba7b509d2c6d671c4044
1/*
2 * Copyright (C) 2007 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
17package com.google.common.collect;
18
19import static com.google.common.collect.MapMakerInternalMap.Strength.SOFT;
20import static com.google.common.collect.MapMakerInternalMap.Strength.STRONG;
21import static com.google.common.collect.MapMakerInternalMap.Strength.WEAK;
22import static com.google.common.collect.testing.IteratorFeature.SUPPORTS_REMOVE;
23import static com.google.common.testing.SerializableTester.reserializeAndAssert;
24import static java.util.Arrays.asList;
25import static org.easymock.EasyMock.eq;
26import static org.easymock.EasyMock.expect;
27import static org.easymock.EasyMock.isA;
28
29import com.google.common.base.Equivalences;
30import com.google.common.collect.MapMaker.RemovalListener;
31import com.google.common.collect.MapMaker.RemovalNotification;
32import com.google.common.collect.Multiset.Entry;
33import com.google.common.collect.testing.IteratorTester;
34
35import junit.framework.TestCase;
36
37import org.easymock.EasyMock;
38
39import java.util.Iterator;
40import java.util.List;
41import java.util.concurrent.ConcurrentMap;
42import java.util.concurrent.TimeUnit;
43import java.util.concurrent.atomic.AtomicInteger;
44
45/**
46 * Test case for {@link ConcurrentHashMultiset}.
47 *
48 * @author Cliff L. Biffle
49 * @author mike nonemacher
50 */
51public class ConcurrentHashMultisetTest extends TestCase {
52  private static final String KEY = "puppies";
53
54  ConcurrentMap<String, AtomicInteger> backingMap;
55  ConcurrentHashMultiset<String> multiset;
56
57  @SuppressWarnings("unchecked")
58  @Override protected void setUp() {
59    backingMap = EasyMock.createMock(ConcurrentMap.class);
60    expect(backingMap.isEmpty()).andReturn(true);
61    replay();
62
63    multiset = new ConcurrentHashMultiset<String>(backingMap);
64    verify();
65    reset();
66  }
67
68  public void testCount_elementPresent() {
69    final int COUNT = 12;
70    expect(backingMap.get(KEY)).andReturn(new AtomicInteger(COUNT));
71    replay();
72
73    assertEquals(COUNT, multiset.count(KEY));
74    verify();
75  }
76
77  public void testCount_elementAbsent() {
78    expect(backingMap.get(KEY)).andReturn(null);
79    replay();
80
81    assertEquals(0, multiset.count(KEY));
82    verify();
83  }
84
85  public void testAdd_zero() {
86    final int INITIAL_COUNT = 32;
87
88    expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT));
89    replay();
90    assertEquals(INITIAL_COUNT, multiset.add(KEY, 0));
91    verify();
92  }
93
94  public void testAdd_firstFewWithSuccess() {
95    final int COUNT = 400;
96
97    expect(backingMap.get(KEY)).andReturn(null);
98    expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(null);
99    replay();
100
101    assertEquals(0, multiset.add(KEY, COUNT));
102    verify();
103  }
104
105  public void testAdd_laterFewWithSuccess() {
106    int INITIAL_COUNT = 32;
107    int COUNT_TO_ADD = 400;
108
109    AtomicInteger initial = new AtomicInteger(INITIAL_COUNT);
110    expect(backingMap.get(KEY)).andReturn(initial);
111    replay();
112
113    assertEquals(INITIAL_COUNT, multiset.add(KEY, COUNT_TO_ADD));
114    assertEquals(INITIAL_COUNT + COUNT_TO_ADD, initial.get());
115    verify();
116  }
117
118  public void testAdd_laterFewWithOverflow() {
119    final int INITIAL_COUNT = 92384930;
120    final int COUNT_TO_ADD = Integer.MAX_VALUE - INITIAL_COUNT + 1;
121
122    expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT));
123    replay();
124
125    try {
126      multiset.add(KEY, COUNT_TO_ADD);
127      fail("Must reject arguments that would cause counter overflow.");
128    } catch (IllegalArgumentException e) {
129      // Expected.
130    }
131    verify();
132  }
133
134  /**
135   * Simulate some of the races that can happen on add. We can't easily simulate the race that
136   * happens when an {@link AtomicInteger#compareAndSet} fails, but we can simulate the case where
137   * the putIfAbsent returns a non-null value, and the case where the replace() of an observed
138   * zero fails.
139   */
140  public void testAdd_withFailures() {
141    AtomicInteger existing = new AtomicInteger(12);
142    AtomicInteger existingZero = new AtomicInteger(0);
143
144    // initial map.get()
145    expect(backingMap.get(KEY)).andReturn(null);
146    // since get returned null, try a putIfAbsent; that fails due to a simulated race
147    expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existingZero);
148    // since the putIfAbsent returned a zero, we'll try to replace...
149    expect(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class)))
150        .andReturn(false);
151    // ...and then putIfAbsent. Simulate failure on both
152    expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existing);
153
154    // next map.get()
155    expect(backingMap.get(KEY)).andReturn(existingZero);
156    // since get returned zero, try a replace; that fails due to a simulated race
157    expect(backingMap.replace(eq(KEY), eq(existingZero), isA(AtomicInteger.class)))
158        .andReturn(false);
159    expect(backingMap.putIfAbsent(eq(KEY), isA(AtomicInteger.class))).andReturn(existing);
160
161    // another map.get()
162    expect(backingMap.get(KEY)).andReturn(existing);
163    // we shouldn't see any more map operations; CHM will now just update the AtomicInteger
164
165    replay();
166
167    assertEquals(multiset.add(KEY, 3), 12);
168    assertEquals(15, existing.get());
169
170    verify();
171  }
172
173  public void testRemove_zeroFromSome() {
174    final int INITIAL_COUNT = 14;
175    expect(backingMap.get(KEY)).andReturn(new AtomicInteger(INITIAL_COUNT));
176    replay();
177
178    assertEquals(INITIAL_COUNT, multiset.remove(KEY, 0));
179    verify();
180  }
181
182  public void testRemove_zeroFromNone() {
183    expect(backingMap.get(KEY)).andReturn(null);
184    replay();
185
186    assertEquals(0, multiset.remove(KEY, 0));
187    verify();
188  }
189
190  public void testRemove_nonePresent() {
191    expect(backingMap.get(KEY)).andReturn(null);
192    replay();
193
194    assertEquals(0, multiset.remove(KEY, 400));
195    verify();
196  }
197
198  public void testRemove_someRemaining() {
199    int countToRemove = 30;
200    int countRemaining = 1;
201    AtomicInteger current = new AtomicInteger(countToRemove + countRemaining);
202
203    expect(backingMap.get(KEY)).andReturn(current);
204    replay();
205
206    assertEquals(countToRemove + countRemaining, multiset.remove(KEY, countToRemove));
207    assertEquals(countRemaining, current.get());
208    verify();
209  }
210
211  public void testRemove_noneRemaining() {
212    int countToRemove = 30;
213    AtomicInteger current = new AtomicInteger(countToRemove);
214
215    expect(backingMap.get(KEY)).andReturn(current);
216    // it's ok if removal fails: another thread may have done the remove
217    expect(backingMap.remove(KEY, current)).andReturn(false);
218    replay();
219
220    assertEquals(countToRemove, multiset.remove(KEY, countToRemove));
221    assertEquals(0, current.get());
222    verify();
223  }
224
225  public void testIteratorRemove_actualMap() {
226    // Override to avoid using mocks.
227    multiset = ConcurrentHashMultiset.create();
228
229    multiset.add(KEY);
230    multiset.add(KEY + "_2");
231    multiset.add(KEY);
232
233    int mutations = 0;
234    for (Iterator<String> it = multiset.iterator(); it.hasNext(); ) {
235      it.next();
236      it.remove();
237      mutations++;
238    }
239    assertTrue(multiset.isEmpty());
240    assertEquals(3, mutations);
241  }
242
243  public void testIterator() {
244    // multiset.iterator
245    List<String> expected = asList("a", "a", "b", "b", "b");
246    new IteratorTester<String>(
247        5, asList(SUPPORTS_REMOVE), expected, IteratorTester.KnownOrder.UNKNOWN_ORDER) {
248
249      ConcurrentHashMultiset<String> multiset;
250
251      @Override protected Iterator<String> newTargetIterator() {
252        multiset = ConcurrentHashMultiset.create();
253        multiset.add("a", 2);
254        multiset.add("b", 3);
255        return multiset.iterator();
256      }
257
258      @Override protected void verify(List<String> elements) {
259        super.verify(elements);
260        assertEquals(ImmutableMultiset.copyOf(elements), multiset);
261      }
262    }.test();
263  }
264
265  public void testEntryIterator() {
266    // multiset.entryIterator
267    List<Entry<String>> expected = asList(
268        Multisets.immutableEntry("a", 1),
269        Multisets.immutableEntry("b", 2),
270        Multisets.immutableEntry("c", 3),
271        Multisets.immutableEntry("d", 4),
272        Multisets.immutableEntry("e", 5));
273    new IteratorTester<Entry<String>>(
274        5, asList(SUPPORTS_REMOVE), expected, IteratorTester.KnownOrder.UNKNOWN_ORDER) {
275
276      ConcurrentHashMultiset<String> multiset;
277
278      @Override protected Iterator<Entry<String>> newTargetIterator() {
279        multiset = ConcurrentHashMultiset.create();
280        multiset.add("a", 1);
281        multiset.add("b", 2);
282        multiset.add("c", 3);
283        multiset.add("d", 4);
284        multiset.add("e", 5);
285        return multiset.entryIterator();
286      }
287
288      @Override protected void verify(List<Entry<String>> elements) {
289        super.verify(elements);
290        assertEquals(ImmutableSet.copyOf(elements), ImmutableSet.copyOf(multiset.entryIterator()));
291      }
292    }.test();
293  }
294
295  public void testSetCount_basic() {
296    int initialCount = 20;
297    int countToSet = 40;
298    AtomicInteger current = new AtomicInteger(initialCount);
299
300    expect(backingMap.get(KEY)).andReturn(current);
301    replay();
302
303    assertEquals(initialCount, multiset.setCount(KEY, countToSet));
304    assertEquals(countToSet, current.get());
305    verify();
306  }
307
308  public void testSetCount_asRemove() {
309    int countToRemove = 40;
310    AtomicInteger current = new AtomicInteger(countToRemove);
311
312    expect(backingMap.get(KEY)).andReturn(current);
313    expect(backingMap.remove(KEY, current)).andReturn(true);
314    replay();
315
316    assertEquals(countToRemove, multiset.setCount(KEY, 0));
317    assertEquals(0, current.get());
318    verify();
319  }
320
321  public void testSetCount_0_nonePresent() {
322    expect(backingMap.get(KEY)).andReturn(null);
323    replay();
324
325    assertEquals(0, multiset.setCount(KEY, 0));
326    verify();
327  }
328
329  public void testCreate() {
330    ConcurrentHashMultiset<Integer> multiset = ConcurrentHashMultiset.create();
331    assertTrue(multiset.isEmpty());
332    reserializeAndAssert(multiset);
333  }
334
335  public void testCreateFromIterable() {
336    Iterable<Integer> iterable = asList(1, 2, 2, 3, 4);
337    ConcurrentHashMultiset<Integer> multiset
338        = ConcurrentHashMultiset.create(iterable);
339    assertEquals(2, multiset.count(2));
340    reserializeAndAssert(multiset);
341  }
342
343  public void testIdentityKeyEquality_strongKeys() {
344    testIdentityKeyEquality(STRONG);
345  }
346
347  public void testIdentityKeyEquality_softKeys() {
348    testIdentityKeyEquality(SOFT);
349  }
350
351  public void testIdentityKeyEquality_weakKeys() {
352    testIdentityKeyEquality(WEAK);
353  }
354
355  private void testIdentityKeyEquality(
356      MapMakerInternalMap.Strength keyStrength) {
357
358    MapMaker mapMaker = new MapMaker()
359        .setKeyStrength(keyStrength)
360        .keyEquivalence(Equivalences.identity());
361
362    ConcurrentHashMultiset<String> multiset =
363        ConcurrentHashMultiset.create(mapMaker);
364
365    String s1 = new String("a");
366    String s2 = new String("a");
367    assertEquals(s1, s2); // Stating the obvious.
368    assertTrue(s1 != s2); // Stating the obvious.
369
370    multiset.add(s1);
371    assertTrue(multiset.contains(s1));
372    assertFalse(multiset.contains(s2));
373    assertEquals(1, multiset.count(s1));
374    assertEquals(0, multiset.count(s2));
375
376    multiset.add(s1);
377    multiset.add(s2, 3);
378    assertEquals(2, multiset.count(s1));
379    assertEquals(3, multiset.count(s2));
380
381    multiset.remove(s1);
382    assertEquals(1, multiset.count(s1));
383    assertEquals(3, multiset.count(s2));
384  }
385
386  public void testLogicalKeyEquality_strongKeys() {
387    testLogicalKeyEquality(STRONG);
388  }
389
390  public void testLogicalKeyEquality_softKeys() {
391    testLogicalKeyEquality(SOFT);
392  }
393
394  public void testLogicalKeyEquality_weakKeys() {
395    testLogicalKeyEquality(WEAK);
396  }
397
398  private void testLogicalKeyEquality(
399      MapMakerInternalMap.Strength keyStrength) {
400
401    MapMaker mapMaker = new MapMaker()
402        .setKeyStrength(keyStrength)
403        .keyEquivalence(Equivalences.equals());
404
405    ConcurrentHashMultiset<String> multiset =
406        ConcurrentHashMultiset.create(mapMaker);
407
408    String s1 = new String("a");
409    String s2 = new String("a");
410    assertEquals(s1, s2); // Stating the obvious.
411
412    multiset.add(s1);
413    assertTrue(multiset.contains(s1));
414    assertTrue(multiset.contains(s2));
415    assertEquals(1, multiset.count(s1));
416    assertEquals(1, multiset.count(s2));
417
418    multiset.add(s2, 3);
419    assertEquals(4, multiset.count(s1));
420    assertEquals(4, multiset.count(s2));
421
422    multiset.remove(s1);
423    assertEquals(3, multiset.count(s1));
424    assertEquals(3, multiset.count(s2));
425  }
426
427  public void testSerializationWithMapMaker1() {
428    MapMaker mapMaker = new MapMaker();
429    multiset = ConcurrentHashMultiset.create(mapMaker);
430    reserializeAndAssert(multiset);
431  }
432
433  public void testSerializationWithMapMaker2() {
434    MapMaker mapMaker = new MapMaker();
435    multiset = ConcurrentHashMultiset.create(mapMaker);
436    multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b"));
437    reserializeAndAssert(multiset);
438  }
439
440  public void testSerializationWithMapMaker3() {
441    MapMaker mapMaker = new MapMaker().expireAfterWrite(1, TimeUnit.SECONDS);
442    multiset = ConcurrentHashMultiset.create(mapMaker);
443    multiset.addAll(ImmutableList.of("a", "a", "b", "c", "d", "b"));
444    reserializeAndAssert(multiset);
445  }
446
447  public void testSerializationWithMapMaker_preservesIdentityKeyEquivalence() {
448    MapMaker mapMaker = new MapMaker()
449        .keyEquivalence(Equivalences.identity());
450
451    ConcurrentHashMultiset<String> multiset =
452        ConcurrentHashMultiset.create(mapMaker);
453    multiset = reserializeAndAssert(multiset);
454
455    String s1 = new String("a");
456    String s2 = new String("a");
457    assertEquals(s1, s2); // Stating the obvious.
458    assertTrue(s1 != s2); // Stating the obvious.
459
460    multiset.add(s1);
461    assertTrue(multiset.contains(s1));
462    assertFalse(multiset.contains(s2));
463    assertEquals(1, multiset.count(s1));
464    assertEquals(0, multiset.count(s2));
465  }
466
467//  @Suppress(owner = "bmanes", detail = "Does not call the eviction listener")
468//  public void testWithMapMakerEvictionListener_BROKEN1()
469//      throws InterruptedException {
470//    MapEvictionListener<String, Number> evictionListener =
471//        mockEvictionListener();
472//    evictionListener.onEviction("a", 5);
473//    EasyMock.replay(evictionListener);
474//
475//    GenericMapMaker<String, Number> mapMaker = new MapMaker()
476//        .expireAfterWrite(100, TimeUnit.MILLISECONDS)
477//        .evictionListener(evictionListener);
478//
479//    ConcurrentHashMultiset<String> multiset =
480//        ConcurrentHashMultiset.create(mapMaker);
481//
482//    multiset.add("a", 5);
483//
484//    assertTrue(multiset.contains("a"));
485//    assertEquals(5, multiset.count("a"));
486//
487//    Thread.sleep(2000);
488//
489//    EasyMock.verify(evictionListener);
490//  }
491
492//  @Suppress(owner = "bmanes", detail = "Does not call the eviction listener")
493//  public void testWithMapMakerEvictionListener_BROKEN2()
494//      throws InterruptedException {
495//    MapEvictionListener<String, Number> evictionListener =
496//        mockEvictionListener();
497//    evictionListener.onEviction("a", 5);
498//    EasyMock.replay(evictionListener);
499//
500//    GenericMapMaker<String, Number> mapMaker = new MapMaker()
501//        .expireAfterWrite(100, TimeUnit.MILLISECONDS)
502//        .evictionListener(evictionListener);
503//
504//    ConcurrentHashMultiset<String> multiset =
505//        ConcurrentHashMultiset.create(mapMaker);
506//
507//    multiset.add("a", 5);
508//
509//    assertTrue(multiset.contains("a"));
510//    assertEquals(5, multiset.count("a"));
511//
512//    Thread.sleep(2000);
513//
514//    // This call should have the side-effect of calling the
515//    // eviction listener, but it does not.
516//    assertFalse(multiset.contains("a"));
517//
518//    EasyMock.verify(evictionListener);
519//  }
520
521  public void testWithMapMakerEvictionListener() {
522    final List<RemovalNotification<String, Number>> notificationQueue = Lists.newArrayList();
523    RemovalListener<String, Number> removalListener =
524        new RemovalListener<String, Number>() {
525          @Override public void onRemoval(RemovalNotification<String, Number> notification) {
526            notificationQueue.add(notification);
527          }
528        };
529
530    @SuppressWarnings("deprecation") // TODO(kevinb): what to do?
531    GenericMapMaker<String, Number> mapMaker = new MapMaker()
532        .concurrencyLevel(1)
533        .maximumSize(1)
534        .removalListener(removalListener);
535
536    ConcurrentHashMultiset<String> multiset = ConcurrentHashMultiset.create(mapMaker);
537
538    multiset.add("a", 5);
539    assertTrue(multiset.contains("a"));
540    assertEquals(5, multiset.count("a"));
541
542    multiset.add("b", 3);
543
544    assertFalse(multiset.contains("a"));
545    assertTrue(multiset.contains("b"));
546    assertEquals(3, multiset.count("b"));
547    RemovalNotification<String, Number> notification = Iterables.getOnlyElement(notificationQueue);
548    assertEquals("a", notification.getKey());
549    // The map evicted this entry, so CHM didn't have a chance to zero it.
550    assertEquals(5, notification.getValue().intValue());
551  }
552
553  private void replay() {
554    EasyMock.replay(backingMap);
555  }
556
557  private void verify() {
558    EasyMock.verify(backingMap);
559  }
560
561  private void reset() {
562    EasyMock.reset(backingMap);
563  }
564}
565