Skip to content

Commit 1141a7b

Browse files
cache: concurrency friendly
1 parent b5ac98e commit 1141a7b

6 files changed

Lines changed: 120 additions & 13 deletions

File tree

webtau-cache/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@
6363
<artifactId>groovy-nio</artifactId>
6464
<scope>test</scope>
6565
</dependency>
66+
67+
<dependency>
68+
<groupId>org.testingisdocumenting.webtau</groupId>
69+
<artifactId>webtau-core</artifactId>
70+
<version>${project.version}</version>
71+
<scope>test</scope>
72+
<type>test-jar</type>
73+
</dependency>
6674
</dependencies>
6775

6876
<build>

webtau-cache/src/main/java/org/testingisdocumenting/webtau/cache/FileBasedCache.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/*
2+
* Copyright 2021 webtau maintainers
23
* Copyright 2019 TWO SIGMA OPEN SOURCE, LLC
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,17 +25,29 @@
2425
import java.nio.file.Path;
2526
import java.util.LinkedHashMap;
2627
import java.util.Map;
28+
import java.util.concurrent.ConcurrentHashMap;
29+
import java.util.concurrent.atomic.AtomicBoolean;
30+
import java.util.concurrent.atomic.AtomicLong;
2731
import java.util.function.Supplier;
2832

2933
public class FileBasedCache {
3034
private static final String VALUE_KEY = "value";
3135
private static final String EXPIRATION_TIME_KEY = "expirationTime";
3236

37+
private final AtomicBoolean isCacheLoaded;
38+
3339
private final Supplier<Path> cachePathSupplier;
34-
private Map<String, Object> loadedCache;
40+
private final AtomicLong lastPutTime;
41+
42+
private final Map<String, Object> loadedCache;
3543

3644
public FileBasedCache(Supplier<Path> cachePathSupplier) {
3745
this.cachePathSupplier = cachePathSupplier;
46+
this.isCacheLoaded = new AtomicBoolean(false);
47+
this.lastPutTime = new AtomicLong(Time.currentTimeMillis());
48+
this.loadedCache = new ConcurrentHashMap<>();
49+
50+
registerFlushCacheOnExit();
3851
}
3952

4053
@SuppressWarnings("unchecked")
@@ -47,22 +60,28 @@ public <E> E get(String key) {
4760

4861
Number expirationTime = (Number) valueWithMeta.get(EXPIRATION_TIME_KEY);
4962
if (Time.currentTimeMillis() >= expirationTime.longValue()) {
63+
loadedCache.remove(key);
64+
flushCacheToDiskIfRequired();
5065
return null;
5166
}
5267

53-
return (E) valueWithMeta.get(VALUE_KEY);
68+
E result = (E) valueWithMeta.get(VALUE_KEY);
69+
flushCacheToDiskIfRequired();
70+
71+
return result;
5472
}
5573

5674
public void put(String key, Object value, long expirationTime) {
5775
loadCacheIfRequired();
5876
Map<String, Object> valueWithMeta = createValueWithMeta(value, expirationTime);
5977
loadedCache.put(key, valueWithMeta);
6078

61-
FileUtils.writeTextContent(cachePathSupplier.get(), JsonUtils.serializePrettyPrint(loadedCache));
79+
flushCacheToDiskIfRequired();
6280
}
6381

6482
public void put(String key, Object value) {
6583
put(key, value, Long.MAX_VALUE);
84+
flushCacheToDiskIfRequired();
6685
}
6786

6887
private Map<String, Object> createValueWithMeta(Object value, long expirationTime) {
@@ -75,16 +94,36 @@ private Map<String, Object> createValueWithMeta(Object value, long expirationTim
7594

7695
@SuppressWarnings("unchecked")
7796
private void loadCacheIfRequired() {
78-
if (loadedCache != null) {
97+
if (isCacheLoaded.get()) {
7998
return;
8099
}
81100

82101
Path cachePath = cachePathSupplier.get();
83102
if (!Files.exists(cachePath)) {
84-
loadedCache = new LinkedHashMap<>();
103+
isCacheLoaded.set(true);
85104
return;
86105
}
87106

88-
loadedCache = (Map<String, Object>) JsonUtils.deserialize(FileUtils.fileTextContent(cachePath));
107+
loadedCache.putAll((Map<String, ?>) JsonUtils.deserialize(FileUtils.fileTextContent(cachePath)));
108+
isCacheLoaded.set(true);
109+
}
110+
111+
private void flushCacheToDiskIfRequired() {
112+
long lastTime = lastPutTime.get();
113+
long current = Time.currentTimeMillis();
114+
115+
if (current - lastTime > 10_000) {
116+
flushCacheToDisk();
117+
}
118+
119+
lastPutTime.set(current);
120+
}
121+
122+
private synchronized void flushCacheToDisk() {
123+
FileUtils.writeTextContent(cachePathSupplier.get(), JsonUtils.serializePrettyPrint(loadedCache));
124+
}
125+
126+
private void registerFlushCacheOnExit() {
127+
Runtime.getRuntime().addShutdownHook(new Thread(this::flushCacheToDisk));
89128
}
90129
}

webtau-cache/src/test/groovy/org/testingisdocumenting/webtau/cache/FileBasedCacheTest.groovy

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,24 @@
1616

1717
package org.testingisdocumenting.webtau.cache
1818

19-
import org.testingisdocumenting.webtau.utils.JsonUtils
19+
import org.junit.After
2020
import org.junit.Assert
2121
import org.junit.Test
22+
import org.testingisdocumenting.webtau.time.DummyTimeProvider
23+
import org.testingisdocumenting.webtau.time.Time
24+
import org.testingisdocumenting.webtau.utils.JsonUtils
2225

2326
import java.nio.file.Files
2427
import java.nio.file.Path
2528
import java.nio.file.Paths
29+
import java.util.concurrent.Executors
2630

2731
class FileBasedCacheTest {
32+
@After
33+
void cleanUp() {
34+
Time.setTimeProvider(null)
35+
}
36+
2837
@Test
2938
void "should load cached values from a provided file"() {
3039
def cache = [
@@ -51,8 +60,11 @@ class FileBasedCacheTest {
5160
}
5261

5362
@Test
54-
void "should persist values in the file as pretty print json"() {
63+
void "should persist values in the file as pretty print json if time passed since last put"() {
5564
def cacheFile = createTempCacheFile([:])
65+
66+
Time.timeProvider = new DummyTimeProvider([0, 11_000])
67+
5668
def fileBasedCache = new FileBasedCache({ -> cacheFile })
5769
fileBasedCache.put('accessToken', 'abc', 400)
5870

@@ -64,10 +76,24 @@ class FileBasedCacheTest {
6476
'}'), cacheFile.text)
6577
}
6678

79+
@Test
80+
void "should not persist values in the file right away"() {
81+
def cacheFile = createTempCacheFile([:])
82+
83+
Time.timeProvider = new DummyTimeProvider([0, 100])
84+
85+
def fileBasedCache = new FileBasedCache({ -> cacheFile })
86+
fileBasedCache.put('accessToken', 'abc', 400)
87+
88+
Assert.assertEquals("{ }", cacheFile.text)
89+
}
90+
6791
@Test
6892
void "should create non expiring values if no expiration time is provided"() {
6993
def cacheFile = createTempCacheFile([:])
94+
7095
def fileBasedCache = new FileBasedCache({ -> cacheFile })
96+
Time.timeProvider = new DummyTimeProvider([0, 11_000])
7197
fileBasedCache.put('accessToken', 'abc')
7298

7399
Assert.assertEquals(String.format('{%n' +
@@ -78,6 +104,31 @@ class FileBasedCacheTest {
78104
'}'), cacheFile.text)
79105
}
80106

107+
@Test
108+
void "should handle put from multiple threads"() {
109+
def cacheFile = createTempCacheFile([:])
110+
Time.timeProvider = new DummyTimeProvider(0)
111+
def fileBasedCache = new FileBasedCache({ -> cacheFile })
112+
113+
def executor = Executors.newFixedThreadPool(300)
114+
115+
def futures = []
116+
300.times {idx ->
117+
fileBasedCache.put("value" + idx, 0)
118+
119+
futures << executor.submit {
120+
150.times {
121+
fileBasedCache.put("value" + idx, fileBasedCache.get("value" + idx) + 1)
122+
}
123+
}
124+
}
125+
126+
futures*.get()
127+
assert fileBasedCache.get("value0") == 150
128+
assert fileBasedCache.get("value1") == 150
129+
assert fileBasedCache.get("value9") == 150
130+
}
131+
81132
@Test
82133
void "should handle non existing file"() {
83134
def cachePath = Paths.get('nonExisting.json')

webtau-core/src/main/java/org/testingisdocumenting/webtau/time/Time.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/*
2+
* Copyright 2021 webtau maintainers
23
* Copyright 2019 TWO SIGMA OPEN SOURCE, LLC
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,7 +21,7 @@
2021

2122
public class Time {
2223
private static final TimeProvider systemTimeProvider = new SystemTimeProvider();
23-
private static AtomicReference<TimeProvider> timeProvider = new AtomicReference<>(systemTimeProvider);
24+
private static final AtomicReference<TimeProvider> timeProvider = new AtomicReference<>(systemTimeProvider);
2425

2526
public static long currentTimeMillis() {
2627
return Time.timeProvider.get().currentTimeMillis();

webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/ConsoleStepReporterTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class ConsoleStepReporterTest implements ConsoleOutput {
4242
ConsoleOutputs.add(this)
4343
ConsoleOutputs.add(ansiConsoleOutput)
4444

45-
Time.setTimeProvider(new DummyTimeProvider([0, 0, 0, 0, 0, 0]))
45+
Time.setTimeProvider(new DummyTimeProvider(0))
4646
}
4747

4848
@After

webtau-core/src/test/groovy/org/testingisdocumenting/webtau/time/DummyTimeProvider.groovy

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/*
2+
* Copyright 2021 webtau maintainers
23
* Copyright 2019 TWO SIGMA OPEN SOURCE, LLC
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,18 +17,25 @@
1617

1718
package org.testingisdocumenting.webtau.time
1819

19-
import org.testingisdocumenting.webtau.time.TimeProvider
20-
2120
class DummyTimeProvider implements TimeProvider {
22-
private List<Integer> timeSnapshots = []
21+
private List<Long> timeSnapshots = []
22+
private Long constantTime = null
2323
private int currentSnapshotIdx = 0
2424

2525
DummyTimeProvider(List<Integer> timeSnapshots) {
2626
this.timeSnapshots.addAll(timeSnapshots)
2727
}
2828

29+
DummyTimeProvider(Long constantTime) {
30+
this.constantTime = constantTime
31+
}
32+
2933
@Override
3034
long currentTimeMillis() {
35+
if (constantTime != null) {
36+
return constantTime
37+
}
38+
3139
if (currentSnapshotIdx >= timeSnapshots.size()) {
3240
throw new RuntimeException("$currentSnapshotIdx idx is out of the provided time snapshots $timeSnapshots")
3341
}

0 commit comments

Comments
 (0)