Skip to content

ClassCastException when concurrently computeIfAbsent on WiredCache #155

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
dawnwords opened this issue Oct 1, 2020 · 4 comments
Closed
Assignees
Labels
Milestone

Comments

@dawnwords
Copy link

Version of Cache2k:1.4.0.Final

A ClassCastException occurs in the following simple unit test.

  @Test
  public void testCache2kComputeIfAbsentConcurrently() throws InterruptedException {
    final Cache<String, State> cache = Cache2kBuilder.of(String.class, State.class)
        .expireAfterWrite(1, TimeUnit.HOURS)
        .addListener((CacheEntryExpiredListener<String, State>) (cache1, entry) ->
            logger.info("entry expired: {}", entry.getValue()))
        .build();
    logger.info("which cache: {}", cache.getClass().getName());
    final int threadCount = 16;
    final String key = "key";

    final CountDownLatch startLatch = new CountDownLatch(threadCount);
    final CountDownLatch stopLatch = new CountDownLatch(threadCount);
    final AtomicInteger exceptionCount = new AtomicInteger();
    for (int i = 0; i < threadCount; i++) {
      new Thread(() -> {
        try {
          startLatch.await();
          final State r = cache.computeIfAbsent(key, State::new);
          logger.info("got State: {}", r);
        } catch (Exception e) {
          logger.error("got Exception", e);
          exceptionCount.incrementAndGet();
        } finally {
          stopLatch.countDown();
        }
      }).start();
      startLatch.countDown();
    }
    stopLatch.await();
    assertThat(exceptionCount.get(), Matchers.is(0));
  }

  private static class State {
  }

If I addListener on Cache2kBuilder and a WiredCache instance is built, the assertion at the end of the unit test on Exception count may fail with the following logs:

[2020-10-01T20:08:38.919+08:00] [main           ] -> [INFO ] which cache: org.cache2k.core.WiredCache
[2020-10-01T20:08:38.943+08:00] [Thread-5       ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.943+08:00] [Thread-7       ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.944+08:00] [Thread-6       ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.944+08:00] [Thread-16      ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.943+08:00] [Thread-14      ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.944+08:00] [Thread-17      ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.945+08:00] [Thread-15      ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.945+08:00] [Thread-10      ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.945+08:00] [Thread-13      ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.945+08:00] [Thread-2       ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.946+08:00] [Thread-8       ] -> [INFO ] got State: test.Cache2kTest$State@6fddf965
[2020-10-01T20:08:38.954+08:00] [Thread-11      ] -> [ERROR] got Exception
java.lang.ClassCastException: org.cache2k.core.CompactEntry$InitialValueInEntryNeverReturned cannot be cast to test.Cache2kTest$State
	at test.Cache2kTest.lambda$testCache2k$7(Cache2kTest.java:158)
	at java.lang.Thread.run(Thread.java:748)
[2020-10-01T20:08:38.956+08:00] [Thread-3       ] -> [ERROR] got Exception
java.lang.ClassCastException: org.cache2k.core.CompactEntry$InitialValueInEntryNeverReturned cannot be cast to test.Cache2kTest$State
	at test.Cache2kTest.lambda$testCache2k$7(Cache2kTest.java:158)
	at java.lang.Thread.run(Thread.java:748)
[2020-10-01T20:08:38.956+08:00] [Thread-12      ] -> [ERROR] got Exception
java.lang.ClassCastException: org.cache2k.core.CompactEntry$InitialValueInEntryNeverReturned cannot be cast to test.Cache2kTest$State
	at test.Cache2kTest.lambda$testCache2k$7(Cache2kTest.java:158)
	at java.lang.Thread.run(Thread.java:748)
[2020-10-01T20:08:38.956+08:00] [Thread-4       ] -> [ERROR] got Exception
java.lang.ClassCastException: org.cache2k.core.CompactEntry$InitialValueInEntryNeverReturned cannot be cast to test.Cache2kTest$State
	at test.Cache2kTest.lambda$testCache2k$7(Cache2kTest.java:158)
	at java.lang.Thread.run(Thread.java:748)
[2020-10-01T20:08:38.956+08:00] [Thread-9       ] -> [ERROR] got Exception
java.lang.ClassCastException: org.cache2k.core.CompactEntry$InitialValueInEntryNeverReturned cannot be cast to test.Cache2kTest$State
	at test.Cache2kTest.lambda$testCache2k$7(Cache2kTest.java:158)
	at java.lang.Thread.run(Thread.java:748)

If I comment the addListener part and the Cache instance falls back to HeapCache, the unit test will pass and everything seems to be fine.

Is there any other required configuration to be included when using addListener on Cache2kBuilder?

@cruftex
Copy link
Member

cruftex commented Oct 1, 2020

Jingxiao, many thanks for the detailed bug report. Sorry for the bad experience. That looks like a bug.

Is there any other required configuration to be included when using addListener on Cache2kBuilder

No. Because of the attached listener the cache uses a more complex processing scheme, which seems
to have a flaw here.

I take a look into it.

@cruftex
Copy link
Member

cruftex commented Oct 2, 2020

This is a rather serious bug and also affects Cache.get with a loader. The bug was introduced in 1.3.x / 1.4. Previous versions are not affected. The fix looks straight forward. I will commit it, backport to 1.4 and release a bug fix.

@cruftex cruftex self-assigned this Oct 2, 2020
@cruftex cruftex added the bug label Oct 2, 2020
@cruftex cruftex added this to the v1.4.1 milestone Oct 2, 2020
@cruftex cruftex closed this as completed in 8d5be45 Oct 2, 2020
@cruftex
Copy link
Member

cruftex commented Oct 2, 2020

If curious, here some background information:

Ironically, I introduced a testing harness for concurrency issues in 1.4 but only tested a few methods that I felt might be troublesome. The test for computeIfAbsent looks like:

  static class ComputeIfAbsent1 extends CacheKeyActorPair<Integer, Integer, Integer> {
    final AtomicInteger count = new AtomicInteger(0);
    public void setup() {
      count.set(0);
      cache.remove(key);
    }
    public Integer actor1() {
      return cache.computeIfAbsent(key, new Callable<Integer>() {
        @Override
        public Integer call() {
          count.getAndIncrement();
          return 1;
        }
      });
    }
    public Integer actor2() {
      return cache.computeIfAbsent(key, new Callable<Integer>() {
        @Override
        public Integer call() {
          count.getAndIncrement();
          return 2;
        }
      });
    }
    public void check(Integer r1, Integer r2) {
      assertEquals("compute called once", 1, count.get());
      assertEquals("actor sees computed value of the other", r1, r2);
      assertEquals(r1, value());
    }
  }

Running the actor methods simultaneously is done by generic code. This should cover all aspects. It is able to reproduce the discovered bug.

@cruftex
Copy link
Member

cruftex commented Oct 2, 2020

Fix released as 1.4.1.Final

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants