Skip to content

Commit bb48abc

Browse files
authored
Merge pull request #65 from urbanairship/cache_misses
adds an option to cache missing values in CachingIdService
2 parents 0f9edfb + 078695e commit bb48abc

File tree

3 files changed

+110
-21
lines changed

3 files changed

+110
-21
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>com.urbanairship</groupId>
55
<artifactId>datacube</artifactId>
6-
<version>1.5.0-SNAPSHOT</version>
6+
<version>1.5.1-SNAPSHOT</version>
77
<packaging>jar</packaging>
88

99
<name>datacube</name>

src/main/java/com/urbanairship/datacube/idservices/CachingIdService.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
import com.urbanairship.datacube.IdService;
1212
import com.yammer.metrics.Metrics;
1313
import com.yammer.metrics.core.Gauge;
14+
import com.yammer.metrics.core.Meter;
15+
import com.yammer.metrics.core.Timer;
16+
import com.yammer.metrics.core.TimerContext;
1417

1518
import java.io.IOException;
19+
import java.util.concurrent.TimeUnit;
1620

1721
/**
1822
* An IdService that wraps around another IdService and caches its results. Calls to getOrCreateId() are
@@ -25,7 +29,16 @@ public class CachingIdService implements IdService {
2529
private final Cache<Key, byte[]> cache;
2630
private final IdService wrappedIdService;
2731

32+
private final Timer idGetTime;
33+
private final boolean cacheMisses;
34+
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
35+
private final Meter cachedNullResult;
36+
2837
public CachingIdService(int numCached, final IdService wrappedIdService, final String cacheName) {
38+
this(numCached, wrappedIdService, cacheName, false);
39+
}
40+
41+
public CachingIdService(int numCached, final IdService wrappedIdService, final String cacheName, boolean cacheMisses) {
2942
this.wrappedIdService = wrappedIdService;
3043
this.cache = CacheBuilder.newBuilder()
3144
.maximumSize(numCached)
@@ -46,6 +59,11 @@ public Double value() {
4659
return cache.stats().hitRate();
4760
}
4861
});
62+
63+
this.idGetTime = Metrics.newTimer(CachingIdService.class,
64+
"id_get", cacheName, TimeUnit.MILLISECONDS, TimeUnit.SECONDS);
65+
cachedNullResult = Metrics.newMeter(CachingIdService.class, cacheName, "cache null", TimeUnit.SECONDS);
66+
this.cacheMisses = cacheMisses;
4967
}
5068

5169

@@ -64,17 +82,27 @@ public byte[] getOrCreateId(int dimensionNum, byte[] bytes, int numIdBytes) thro
6482

6583
@Override
6684
public Optional<byte[]> getId(int dimensionNum, byte[] bytes, int numIdBytes) throws IOException, InterruptedException {
67-
final Key key = new Key(dimensionNum, new BoxedByteArray(bytes), numIdBytes);
68-
final byte[] cachedVal = cache.getIfPresent(key);
69-
70-
if (cachedVal == null) {
71-
final Optional<byte[]> id = wrappedIdService.getId(dimensionNum, bytes, numIdBytes);
72-
if (id.isPresent()) {
73-
cache.put(key, id.get());
85+
final TimerContext time = idGetTime.time();
86+
try {
87+
final Key key = new Key(dimensionNum, new BoxedByteArray(bytes), numIdBytes);
88+
final byte[] cachedVal = cache.getIfPresent(key);
89+
90+
if (cachedVal == null) {
91+
final Optional<byte[]> id = wrappedIdService.getId(dimensionNum, bytes, numIdBytes);
92+
if (id.isPresent()) {
93+
cache.put(key, id.get());
94+
} else if (cacheMisses) {
95+
cachedNullResult.mark();
96+
cache.put(key, EMPTY_BYTE_ARRAY);
97+
}
98+
return id;
99+
} else if (cachedVal == EMPTY_BYTE_ARRAY) {
100+
return Optional.absent();
101+
} else {
102+
return Optional.of(cachedVal);
74103
}
75-
return id;
76-
} else {
77-
return Optional.of(cachedVal);
104+
} finally {
105+
time.stop();
78106
}
79107
}
80108

src/test/java/com/urbanairship/datacube/IdServiceTests.java

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44

55
package com.urbanairship.datacube;
66

7+
import com.google.common.base.Optional;
78
import com.google.common.math.LongMath;
9+
import com.urbanairship.datacube.idservices.CachingIdService;
810
import org.junit.Assert;
11+
import org.junit.Test;
912

13+
import java.io.IOException;
1014
import java.util.HashSet;
1115
import java.util.Set;
1216
import java.util.concurrent.TimeUnit;
17+
import java.util.concurrent.atomic.AtomicInteger;
1318

1419
public class IdServiceTests {
1520
public static void basicTest(IdService idService) throws Exception {
1621
final int numFieldBytes = 5;
17-
22+
1823
// Different inputs should always produce different outputs (non-repeating ids)
1924
Set<BoxedByteArray> idsSeen = new HashSet<BoxedByteArray>();
2025
for(int i=0; i<500; i++) {
@@ -23,46 +28,102 @@ public static void basicTest(IdService idService) throws Exception {
2328
BoxedByteArray newBox = new BoxedByteArray(newId);
2429
Assert.assertTrue("ID was repeated: " + newBox, idsSeen.add(newBox));
2530
}
26-
31+
2732
// The same input should produce the same output
2833
byte[] id1 = idService.getOrCreateId(1, Util.longToBytes(10), numFieldBytes);
2934
byte[] id2 = idService.getOrCreateId(1, Util.longToBytes(10), numFieldBytes);
3035
Assert.assertEquals(numFieldBytes, id1.length);
3136
Assert.assertArrayEquals(id1, id2);
3237
}
33-
38+
3439
/**
3540
* Generating 2^(fieldbits) unique IDs should work, then generating one more should raise an
3641
* exception because no more IDs were available.
37-
*
42+
*
3843
*/
39-
public static void testExhaustion(IdService idService, int numFieldBytes, int dimensionNum)
44+
public static void testExhaustion(IdService idService, int numFieldBytes, int dimensionNum)
4045
throws Exception {
4146
int numFieldBits = numFieldBytes * 8;
42-
47+
4348
long numToGenerate = LongMath.pow(2, numFieldBits);
4449
long i=0;
4550
for(; i<numToGenerate; i++) {
4651
byte[] id = idService.getOrCreateId(dimensionNum, Util.longToBytes(i), numFieldBytes);
4752
Assert.assertEquals(numFieldBytes, id.length);
4853
}
49-
54+
5055
try {
5156
idService.getOrCreateId(dimensionNum, Util.longToBytes(i), numFieldBytes);
5257
Assert.fail("getOrCreateId call should have thrown an exception");
5358
} catch (RuntimeException e) {
5459
// Happy success
5560
}
56-
61+
5762
// Subsequent calls for the same input should fail quickly (and not block for long)
5863
long startTimeNanos = System.nanoTime();
5964
try {
6065
idService.getOrCreateId(dimensionNum, Util.longToBytes(i), numFieldBytes);
6166
Assert.fail("ID allocation should have failed");
62-
} catch(RuntimeException e) {
63-
if(System.nanoTime() - startTimeNanos > TimeUnit.SECONDS.toNanos(5)) {
67+
} catch (RuntimeException e) {
68+
if (System.nanoTime() - startTimeNanos > TimeUnit.SECONDS.toNanos(5)) {
6469
Assert.fail("Took too long to fail");
6570
}
6671
}
6772
}
73+
74+
@Test
75+
public void testCacheMissingOff() throws IOException, InterruptedException {
76+
final CountingIdService wrappedIdService = new CountingIdService();
77+
final CachingIdService lol = new CachingIdService(2, wrappedIdService, "lol", false);
78+
lol.getId(-1, CountingIdService.unknown, 0);
79+
lol.getId(-1, CountingIdService.unknown, 0);
80+
lol.getId(-1, CountingIdService.known, 0);
81+
lol.getId(-1, CountingIdService.known, 0);
82+
// should not have been cached, so accessed backing store twice
83+
Assert.assertEquals(2, wrappedIdService.unKnownCount.get());
84+
// should have been cached, so accessed backing store once
85+
Assert.assertEquals(1, wrappedIdService.knownCount.get());
86+
}
87+
88+
@Test
89+
public void testCacheMissingOn() throws IOException, InterruptedException {
90+
final CountingIdService wrappedIdService = new CountingIdService();
91+
final CachingIdService lol = new CachingIdService(2, wrappedIdService, "lol", true);
92+
lol.getId(-1, CountingIdService.unknown, 0);
93+
lol.getId(-1, CountingIdService.unknown, 0);
94+
lol.getId(-1, CountingIdService.known, 0);
95+
lol.getId(-1, CountingIdService.known, 0);
96+
// should have been cached, so accessed backing store once
97+
Assert.assertEquals(1, wrappedIdService.unKnownCount.get());
98+
// should have been cached, so accessed backing store once
99+
Assert.assertEquals(1, wrappedIdService.knownCount.get());
100+
}
101+
102+
private static final class CountingIdService implements IdService {
103+
104+
public static final byte[] known = "known".getBytes();
105+
public static final byte[] unknown = "unknown".getBytes();
106+
107+
public final AtomicInteger knownCount = new AtomicInteger(0);
108+
public final AtomicInteger unKnownCount = new AtomicInteger(0);
109+
110+
@Override
111+
public byte[] getOrCreateId(int dimensionNum, byte[] input, int numIdBytes) throws IOException, InterruptedException {
112+
throw new RuntimeException("the feature this class tests doesn't make sense with getOrCreateId");
113+
}
114+
115+
@Override
116+
public Optional<byte[]> getId(int dimensionNum, byte[] input, int numIdBytes) throws IOException, InterruptedException {
117+
if (input == known) {
118+
knownCount.incrementAndGet();
119+
return Optional.of(known);
120+
} else if (input == unknown) {
121+
unKnownCount.incrementAndGet();
122+
return Optional.absent();
123+
} else {
124+
throw new RuntimeException("only use the two values for testing plz");
125+
}
126+
}
127+
128+
}
68129
}

0 commit comments

Comments
 (0)