-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix async expiration race for create + read (fixes #298)
When the future is in-flight, the expiration is dsabled by using an infinite timestamp. When finished, a callback updates it through a no-op write and called expireAfterCreate. If a read comes in after the future is done but before the timestamp is reset, it was allowed to set the value. This could cause an ABA race where the infinite timestamp was read, replaced, and written back. This fix now avoids that by using guards to skip the read and CAS when updating.
- Loading branch information
Showing
4 changed files
with
201 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
143 changes: 143 additions & 0 deletions
143
caffeine/src/test/java/com/github/benmanes/caffeine/cache/issues/Issue298Test.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
/* | ||
* Copyright 2019 Ben Manes. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package com.github.benmanes.caffeine.cache.issues; | ||
|
||
import static com.github.benmanes.caffeine.testing.Awaits.await; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.Matchers.is; | ||
import static org.hamcrest.Matchers.lessThanOrEqualTo; | ||
|
||
import java.time.Duration; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
|
||
import javax.annotation.Nonnull; | ||
|
||
import org.testng.annotations.AfterMethod; | ||
import org.testng.annotations.BeforeMethod; | ||
import org.testng.annotations.Test; | ||
|
||
import com.github.benmanes.caffeine.cache.AsyncLoadingCache; | ||
import com.github.benmanes.caffeine.cache.Caffeine; | ||
import com.github.benmanes.caffeine.cache.Expiry; | ||
import com.github.benmanes.caffeine.cache.Policy.VarExpiration; | ||
|
||
/** | ||
* Issue #298: Stale data when using Expiry | ||
* <p> | ||
* When a future value in an AsyncCache is in-flight, the entry has an infinite expiration time to | ||
* disable eviction. When it completes, a callback performs a no-op write into the cache to | ||
* update its metadata (expiration, weight, etc). This may race with a reader who obtains a | ||
* completed future, reads the current duration as infinite, and tries to set the expiration time | ||
* accordingly (to indicate no change). If the writer completes before the reader updates, then we | ||
* encounter an ABA problem where the entry is set to never expire. | ||
* | ||
* @author [email protected] (Ben Manes) | ||
*/ | ||
@Test(groups = "isolated") | ||
public final class Issue298Test { | ||
static final long EXPIRE_NS = Duration.ofDays(1).toNanos(); | ||
|
||
AtomicBoolean startedLoad; | ||
AtomicBoolean doLoad; | ||
|
||
AtomicBoolean startedCreate; | ||
AtomicBoolean doCreate; | ||
|
||
AtomicBoolean startedRead; | ||
AtomicBoolean doRead; | ||
AtomicBoolean endRead; | ||
|
||
AsyncLoadingCache<String, String> cache; | ||
VarExpiration<String, String> policy; | ||
String key; | ||
|
||
@BeforeMethod | ||
public void before() { | ||
startedCreate = new AtomicBoolean(); | ||
startedLoad = new AtomicBoolean(); | ||
startedRead = new AtomicBoolean(); | ||
doCreate = new AtomicBoolean(); | ||
endRead = new AtomicBoolean(); | ||
doLoad = new AtomicBoolean(); | ||
doRead = new AtomicBoolean(); | ||
|
||
key = "key"; | ||
cache = makeAsyncCache(); | ||
policy = cache.synchronous().policy().expireVariably().get(); | ||
} | ||
|
||
@AfterMethod | ||
public void after() { | ||
endRead.set(true); | ||
} | ||
|
||
@Test | ||
public void readDuringCreate() { | ||
// Loaded value and waiting at expireAfterCreate (expire: infinite) | ||
cache.get(key); | ||
await().untilTrue(startedLoad); | ||
doLoad.set(true); | ||
await().untilTrue(startedCreate); | ||
|
||
// Async read trying to wait at expireAfterRead | ||
CompletableFuture<Void> reader = CompletableFuture.runAsync(() -> { | ||
do { | ||
cache.get(key); | ||
} while (!endRead.get()); | ||
}); | ||
|
||
// Ran expireAfterCreate (expire: infinite -> create) | ||
doCreate.set(true); | ||
await().until(() -> policy.getExpiresAfter(key).get().toNanos() <= EXPIRE_NS); | ||
await().untilTrue(startedRead); | ||
|
||
// Ran reader (expire: create -> ?) | ||
doRead.set(true); | ||
endRead.set(true); | ||
reader.join(); | ||
|
||
// Ensure expire is [expireAfterCreate], not [infinite] | ||
assertThat(policy.getExpiresAfter(key).get().toNanos(), is(lessThanOrEqualTo(EXPIRE_NS))); | ||
} | ||
|
||
private AsyncLoadingCache<String, String> makeAsyncCache() { | ||
return Caffeine.newBuilder() | ||
.expireAfter(new Expiry<String, String>() { | ||
@Override public long expireAfterCreate(@Nonnull String key, | ||
@Nonnull String value, long currentTime) { | ||
startedCreate.set(true); | ||
await().untilTrue(doCreate); | ||
return EXPIRE_NS; | ||
} | ||
@Override public long expireAfterUpdate(@Nonnull String key, | ||
@Nonnull String value, long currentTime, long currentDuration) { | ||
return currentDuration; | ||
} | ||
@Override public long expireAfterRead(@Nonnull String key, | ||
@Nonnull String value, long currentTime, long currentDuration) { | ||
startedRead.set(true); | ||
await().untilTrue(doRead); | ||
return currentDuration; | ||
} | ||
}) | ||
.buildAsync(key -> { | ||
startedLoad.set(true); | ||
await().untilTrue(doLoad); | ||
return key + "'s value"; | ||
}); | ||
} | ||
} |