Skip to content

Commit 8b6aac1

Browse files
authored
Merge pull request #134 from splitio/development
Development
2 parents af66ac7 + 6a825a6 commit 8b6aac1

File tree

13 files changed

+100462
-3
lines changed

13 files changed

+100462
-3
lines changed

client/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>io.split.client</groupId>
77
<artifactId>java-client-parent</artifactId>
8-
<version>3.3.3</version>
8+
<version>3.3.4</version>
99
</parent>
1010
<artifactId>java-client</artifactId>
1111
<packaging>jar</packaging>

client/src/main/java/io/split/client/dtos/KeyImpression.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class KeyImpression {
99
public String label;
1010
public long time;
1111
public Long changeNumber; // can be null if there is no changeNumber
12+
public Long pt;
1213

1314
@Override
1415
public boolean equals(Object o) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.split.client.impressions;
2+
3+
import io.split.client.dtos.KeyImpression;
4+
import io.split.client.utils.MurmurHash3;
5+
6+
public class ImpressionHasher {
7+
8+
private static final String HASHABLE_FORMAT = "%s:%s:%s:%s:%d";
9+
private static final String UNKNOWN = "UNKNOWN";
10+
11+
private static String unknownIfNull(String s) {
12+
return (s == null) ? UNKNOWN : s;
13+
}
14+
15+
private static Long zeroIfNull(Long l) {
16+
return (l == null) ? 0 : l;
17+
}
18+
19+
public static Long process(KeyImpression impression) {
20+
if (null == impression) {
21+
return null;
22+
}
23+
return MurmurHash3.hash128x64(String.format(HASHABLE_FORMAT,
24+
unknownIfNull(impression.keyName),
25+
unknownIfNull(impression.feature),
26+
unknownIfNull(impression.treatment),
27+
unknownIfNull(impression.label),
28+
zeroIfNull(impression.changeNumber)).getBytes())[0];
29+
}
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.split.client.impressions;
2+
3+
import com.google.common.cache.Cache;
4+
import com.google.common.cache.CacheBuilder;
5+
import io.split.client.dtos.KeyImpression;
6+
import org.apache.http.annotation.NotThreadSafe;
7+
8+
/*
9+
According to guava's docs (https://guava.dev/releases/18.0/api/docs/com/google/common/annotations/Beta.html),
10+
the @Beta decorator only means that the api is not frozen, and has nothing to do with behaviour stability, but
11+
rather to a non-frozen API which may introduce breaking changes at any time in future versions.
12+
Since the library is shaded and should not be exposed to users of the SDK, it's safe to use it here.
13+
*/
14+
15+
@SuppressWarnings("UnstableApiUsage")
16+
@NotThreadSafe
17+
public class ImpressionObserver {
18+
19+
private final Cache<Long, Long> _cache;
20+
21+
public ImpressionObserver(long size) {
22+
_cache = CacheBuilder.newBuilder()
23+
.maximumSize(size)
24+
.concurrencyLevel(4) // Just setting the default value explicitly
25+
.build();
26+
}
27+
28+
public Long testAndSet(KeyImpression impression) {
29+
if (null == impression) {
30+
return null;
31+
}
32+
33+
Long hash = ImpressionHasher.process(impression);
34+
Long previous = _cache.getIfPresent(hash);
35+
_cache.put(hash, impression.time);
36+
return previous;
37+
}
38+
}

client/src/main/java/io/split/client/impressions/ImpressionsManager.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@
2828
public class ImpressionsManager implements ImpressionListener, Runnable {
2929

3030
private static final Logger _log = LoggerFactory.getLogger(ImpressionsManager.class);
31+
private static final long LAST_SEEN_CACHE_SIZE = 500000; // cache up to 500k impression hashes
3132

3233
private final SplitClientConfig _config;
3334
private final CloseableHttpClient _client;
3435
private final BlockingQueue<KeyImpression> _queue;
3536
private final ScheduledExecutorService _scheduler;
3637
private final ImpressionsSender _impressionsSender;
38+
private final ImpressionObserver _impressionObserver;
3739

3840
public static ImpressionsManager instance(CloseableHttpClient client,
3941
SplitClientConfig config) throws URISyntaxException {
@@ -51,6 +53,7 @@ private ImpressionsManager(CloseableHttpClient client, SplitClientConfig config,
5153
_config = config;
5254
_client = client;
5355
_queue = new ArrayBlockingQueue<KeyImpression>(config.impressionsQueueSize());
56+
_impressionObserver = new ImpressionObserver(LAST_SEEN_CACHE_SIZE);
5457

5558
ThreadFactory threadFactory = new ThreadFactoryBuilder()
5659
.setDaemon(true)
@@ -129,6 +132,7 @@ private void sendImpressions() {
129132
impressionsForTest = new ArrayList<>();
130133
tests.put(ki.feature, impressionsForTest);
131134
}
135+
ki.pt = _impressionObserver.testAndSet(ki);
132136
impressionsForTest.add(ki);
133137
}
134138

client/src/main/java/io/split/client/utils/MurmurHash3.java

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,142 @@ public static long murmurhash3_x86_32(CharSequence data, int offset, int len, in
161161
return h1 & 0xFFFFFFFFL;
162162
}
163163

164+
// The following set of methods and constants are borrowed from:
165+
// `This method is borrowed from `org.apache.commons.codec.digest.MurmurHash3`
164166

167+
// Constants for 128-bit variant
168+
private static final long C1 = 0x87c37b91114253d5L;
169+
private static final long C2 = 0x4cf5ad432745937fL;
170+
private static final int R1 = 31;
171+
private static final int R2 = 27;
172+
private static final int R3 = 33;
173+
private static final int M = 5;
174+
private static final int N1 = 0x52dce729;
175+
private static final int N2 = 0x38495ab5;
176+
177+
/**
178+
* Gets the little-endian long from 8 bytes starting at the specified index.
179+
*
180+
* @param data The data
181+
* @param index The index
182+
* @return The little-endian long
183+
*/
184+
private static long getLittleEndianLong(final byte[] data, final int index) {
185+
return (((long) data[index ] & 0xff) ) |
186+
(((long) data[index + 1] & 0xff) << 8) |
187+
(((long) data[index + 2] & 0xff) << 16) |
188+
(((long) data[index + 3] & 0xff) << 24) |
189+
(((long) data[index + 4] & 0xff) << 32) |
190+
(((long) data[index + 5] & 0xff) << 40) |
191+
(((long) data[index + 6] & 0xff) << 48) |
192+
(((long) data[index + 7] & 0xff) << 56);
193+
}
194+
195+
public static long[] hash128x64(final byte[] data) {
196+
return hash128x64(data, 0, data.length, 0);
197+
}
198+
199+
/**
200+
* Generates 128-bit hash from the byte array with the given offset, length and seed.
201+
*
202+
* <p>This is an implementation of the 128-bit hash function {@code MurmurHash3_x64_128}
203+
* from from Austin Applyby's original MurmurHash3 {@code c++} code in SMHasher.</p>
204+
*
205+
* @param data The input byte array
206+
* @param offset The first element of array
207+
* @param length The length of array
208+
* @param seed The initial seed value
209+
* @return The 128-bit hash (2 longs)
210+
*/
211+
public static long[] hash128x64(final byte[] data, final int offset, final int length, final long seed) {
212+
long h1 = seed;
213+
long h2 = seed;
214+
final int nblocks = length >> 4;
215+
216+
// body
217+
for (int i = 0; i < nblocks; i++) {
218+
final int index = offset + (i << 4);
219+
long k1 = getLittleEndianLong(data, index);
220+
long k2 = getLittleEndianLong(data, index + 8);
221+
222+
// mix functions for k1
223+
k1 *= C1;
224+
k1 = Long.rotateLeft(k1, R1);
225+
k1 *= C2;
226+
h1 ^= k1;
227+
h1 = Long.rotateLeft(h1, R2);
228+
h1 += h2;
229+
h1 = h1 * M + N1;
230+
231+
// mix functions for k2
232+
k2 *= C2;
233+
k2 = Long.rotateLeft(k2, R3);
234+
k2 *= C1;
235+
h2 ^= k2;
236+
h2 = Long.rotateLeft(h2, R1);
237+
h2 += h1;
238+
h2 = h2 * M + N2;
239+
}
240+
241+
// tail
242+
long k1 = 0;
243+
long k2 = 0;
244+
final int index = offset + (nblocks << 4);
245+
switch (offset + length - index) {
246+
case 15:
247+
k2 ^= ((long) data[index + 14] & 0xff) << 48;
248+
case 14:
249+
k2 ^= ((long) data[index + 13] & 0xff) << 40;
250+
case 13:
251+
k2 ^= ((long) data[index + 12] & 0xff) << 32;
252+
case 12:
253+
k2 ^= ((long) data[index + 11] & 0xff) << 24;
254+
case 11:
255+
k2 ^= ((long) data[index + 10] & 0xff) << 16;
256+
case 10:
257+
k2 ^= ((long) data[index + 9] & 0xff) << 8;
258+
case 9:
259+
k2 ^= data[index + 8] & 0xff;
260+
k2 *= C2;
261+
k2 = Long.rotateLeft(k2, R3);
262+
k2 *= C1;
263+
h2 ^= k2;
264+
265+
case 8:
266+
k1 ^= ((long) data[index + 7] & 0xff) << 56;
267+
case 7:
268+
k1 ^= ((long) data[index + 6] & 0xff) << 48;
269+
case 6:
270+
k1 ^= ((long) data[index + 5] & 0xff) << 40;
271+
case 5:
272+
k1 ^= ((long) data[index + 4] & 0xff) << 32;
273+
case 4:
274+
k1 ^= ((long) data[index + 3] & 0xff) << 24;
275+
case 3:
276+
k1 ^= ((long) data[index + 2] & 0xff) << 16;
277+
case 2:
278+
k1 ^= ((long) data[index + 1] & 0xff) << 8;
279+
case 1:
280+
k1 ^= data[index] & 0xff;
281+
k1 *= C1;
282+
k1 = Long.rotateLeft(k1, R1);
283+
k1 *= C2;
284+
h1 ^= k1;
285+
}
286+
287+
// finalization
288+
h1 ^= length;
289+
h2 ^= length;
290+
291+
h1 += h2;
292+
h2 += h1;
293+
294+
h1 = fmix64(h1);
295+
h2 = fmix64(h2);
296+
297+
h1 += h2;
298+
h2 += h1;
299+
300+
return new long[] { h1, h2 };
301+
}
165302
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.split.client.impressions;
2+
3+
import io.split.client.dtos.KeyImpression;
4+
import org.junit.Test;
5+
6+
import static org.hamcrest.Matchers.*;
7+
import static org.junit.Assert.*;
8+
9+
10+
public class ImpressionHasherTest {
11+
12+
@Test
13+
public void works() {
14+
KeyImpression imp1 = new KeyImpression();
15+
imp1.feature = "someFeature";
16+
imp1.keyName = "someKey";
17+
imp1.changeNumber = 123L;
18+
imp1.label = "someLabel";
19+
imp1.treatment = "someTreatment";
20+
21+
// Different feature
22+
KeyImpression imp2 = new KeyImpression();
23+
imp2.feature = "someOtherFeature";
24+
imp2.keyName = "someKey";
25+
imp2.changeNumber = 123L;
26+
imp2.label = "someLabel";
27+
assertThat(ImpressionHasher.process(imp1), not(equalTo(ImpressionHasher.process(imp2))));
28+
29+
// different key
30+
imp2.feature = imp1.feature;
31+
imp2.keyName = "someOtherKey";
32+
assertThat(ImpressionHasher.process(imp1), not(equalTo(ImpressionHasher.process(imp2))));
33+
34+
// different changeNumber
35+
imp2.keyName = imp1.keyName;
36+
imp2.changeNumber = 456L;
37+
assertThat(ImpressionHasher.process(imp1), not(equalTo(ImpressionHasher.process(imp2))));
38+
39+
// different label
40+
imp2.changeNumber = imp1.changeNumber;
41+
imp2.label = "someOtherLabel";
42+
assertThat(ImpressionHasher.process(imp1), not(equalTo(ImpressionHasher.process(imp2))));
43+
44+
// different treatment
45+
imp2.label = imp1.label;
46+
imp2.treatment = "someOtherTreatment";
47+
assertThat(ImpressionHasher.process(imp1), not(equalTo(ImpressionHasher.process(imp2))));
48+
}
49+
50+
@Test
51+
public void doesNotCrash() {
52+
KeyImpression imp1 = new KeyImpression();
53+
imp1.feature = null;
54+
imp1.keyName = "someKey";
55+
imp1.changeNumber = 123L;
56+
imp1.label = "someLabel";
57+
assertNotNull(ImpressionHasher.process(imp1));
58+
59+
imp1.keyName = null;
60+
assertNotNull(ImpressionHasher.process(imp1));
61+
62+
imp1.changeNumber = null;
63+
assertNotNull(ImpressionHasher.process(imp1));
64+
65+
imp1.label = null;
66+
assertNotNull(ImpressionHasher.process(imp1));
67+
68+
imp1.treatment = null;
69+
assertNotNull(ImpressionHasher.process(imp1));
70+
71+
assertNull(ImpressionHasher.process(null));
72+
}
73+
}

0 commit comments

Comments
 (0)