Skip to content

Commit 2a42037

Browse files
authored
RAV-2958 - Provide a guava-based IP cache solution with TTL and size limit (#8)
Utility module that provide TTL + size limit cache for the udp socket IP cache
1 parent 8149623 commit 2a42037

5 files changed

Lines changed: 332 additions & 2 deletions

File tree

pom.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<modelVersion>4.0.0</modelVersion>
77

88
<groupId>net.airvantage</groupId>
9-
<artifactId>proxysocket-java</artifactId>
9+
<artifactId>proxy-socket-java</artifactId>
1010
<version>1.0.0-SNAPSHOT</version>
1111
<packaging>pom</packaging>
1212

@@ -16,11 +16,13 @@
1616

1717
<!-- Dependency versions -->
1818
<junit.version>5.10.3</junit.version>
19+
<guava.version>33.5.0-jre</guava.version>
1920

2021
</properties>
2122

2223
<modules>
2324
<module>proxy-socket-core</module>
25+
<module>proxy-socket-guava</module>
2426
</modules>
2527

2628
<build>

proxy-socket-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<parent>
77
<groupId>net.airvantage</groupId>
8-
<artifactId>proxysocket-java</artifactId>
8+
<artifactId>proxy-socket-java</artifactId>
99
<version>1.0.0-SNAPSHOT</version>
1010
</parent>
1111
<artifactId>proxy-socket-core</artifactId>

proxy-socket-guava/pom.xml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>net.airvantage</groupId>
8+
<artifactId>proxy-socket-java</artifactId>
9+
<version>1.0.0-SNAPSHOT</version>
10+
</parent>
11+
<artifactId>proxy-socket-guava</artifactId>
12+
<name>Proxy Protocol - Guava Cache</name>
13+
<packaging>jar</packaging>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>net.airvantage</groupId>
18+
<artifactId>proxy-socket-core</artifactId>
19+
<version>${project.version}</version>
20+
</dependency>
21+
<dependency>
22+
<groupId>com.google.guava</groupId>
23+
<artifactId>guava</artifactId>
24+
<version>${guava.version}</version>
25+
</dependency>
26+
<dependency>
27+
<groupId>org.junit.jupiter</groupId>
28+
<artifactId>junit-jupiter-api</artifactId>
29+
<version>${junit.version}</version>
30+
<scope>test</scope>
31+
</dependency>
32+
<dependency>
33+
<groupId>org.junit.jupiter</groupId>
34+
<artifactId>junit-jupiter-engine</artifactId>
35+
<version>${junit.version}</version>
36+
<scope>test</scope>
37+
</dependency>
38+
</dependencies>
39+
40+
</project>
41+
42+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package net.airvantage.proxysocket.guava;
2+
3+
import net.airvantage.proxysocket.core.ProxyAddressCache;
4+
import com.google.common.cache.Cache;
5+
import com.google.common.cache.CacheBuilder;
6+
import java.net.InetSocketAddress;
7+
import java.time.Duration;
8+
9+
public final class GuavaProxyAddressCache implements ProxyAddressCache {
10+
private final Cache<InetSocketAddress, InetSocketAddress> cache;
11+
12+
public GuavaProxyAddressCache(long maxSize, Duration ttl) {
13+
CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder().maximumSize(maxSize);
14+
if (ttl != null && !ttl.isNegative() && !ttl.isZero()) {
15+
builder = builder.expireAfterAccess(ttl);
16+
}
17+
//noinspection unchecked
18+
this.cache = (Cache<InetSocketAddress, InetSocketAddress>) (Cache<?, ?>) builder.build();
19+
}
20+
21+
@Override
22+
public void put(InetSocketAddress clientAddr, InetSocketAddress proxyAddr) {
23+
if (clientAddr == null || proxyAddr == null) return;
24+
cache.put(clientAddr, proxyAddr);
25+
}
26+
27+
@Override
28+
public InetSocketAddress get(InetSocketAddress clientAddr) {
29+
if (clientAddr == null) return null;
30+
return cache.getIfPresent(clientAddr);
31+
}
32+
33+
@Override
34+
public void invalidate(InetSocketAddress clientAddr) {
35+
if (clientAddr == null) return;
36+
cache.invalidate(clientAddr);
37+
}
38+
39+
@Override
40+
public void clear() {
41+
cache.invalidateAll();
42+
}
43+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* MIT License
3+
* Copyright (c) 2025 Semtech
4+
*/
5+
package net.airvantage.proxysocket.guava;
6+
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.net.InetSocketAddress;
11+
import java.time.Duration;
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.concurrent.*;
15+
import java.util.concurrent.atomic.AtomicInteger;
16+
17+
import static org.junit.jupiter.api.Assertions.*;
18+
19+
class GuavaProxyAddressCacheTest {
20+
private GuavaProxyAddressCache cache;
21+
private InetSocketAddress clientAddr1;
22+
private InetSocketAddress proxyAddr1;
23+
private InetSocketAddress proxyAddr2;
24+
25+
@BeforeEach
26+
void setUp() {
27+
cache = new GuavaProxyAddressCache(100, Duration.ofMinutes(10));
28+
clientAddr1 = new InetSocketAddress("192.168.1.100", 12345);
29+
proxyAddr1 = new InetSocketAddress("10.0.0.1", 443);
30+
proxyAddr2 = new InetSocketAddress("10.0.0.2", 443);
31+
}
32+
33+
@Test
34+
void testPutAndGet() {
35+
cache.put(clientAddr1, proxyAddr1);
36+
InetSocketAddress result = cache.get(clientAddr1);
37+
assertEquals(proxyAddr1, result);
38+
}
39+
40+
@Test
41+
void testGetNonExistentAddress() {
42+
InetSocketAddress result = cache.get(clientAddr1);
43+
assertNull(result);
44+
}
45+
46+
@Test
47+
void testPutOverwritesExistingValue() {
48+
cache.put(clientAddr1, proxyAddr1);
49+
cache.put(clientAddr1, proxyAddr2);
50+
51+
InetSocketAddress result = cache.get(clientAddr1);
52+
assertEquals(proxyAddr2, result);
53+
}
54+
55+
@Test
56+
void testMaximumSizeEnforcement() {
57+
GuavaProxyAddressCache smallCache = new GuavaProxyAddressCache(2, Duration.ofMinutes(10));
58+
59+
InetSocketAddress addr1 = new InetSocketAddress("192.168.1.1", 1);
60+
InetSocketAddress addr2 = new InetSocketAddress("192.168.1.2", 2);
61+
InetSocketAddress addr3 = new InetSocketAddress("192.168.1.3", 3);
62+
63+
smallCache.put(addr1, proxyAddr1);
64+
smallCache.put(addr2, proxyAddr1);
65+
smallCache.put(addr3, proxyAddr1);
66+
67+
// One of the first two entries should have been evicted
68+
int presentCount = 0;
69+
if (smallCache.get(addr1) != null) presentCount++;
70+
if (smallCache.get(addr2) != null) presentCount++;
71+
if (smallCache.get(addr3) != null) presentCount++;
72+
73+
assertTrue(presentCount <= 2, "Cache should not exceed maximum size");
74+
}
75+
76+
@Test
77+
void testTTLExpiration() throws InterruptedException {
78+
GuavaProxyAddressCache ttlCache = new GuavaProxyAddressCache(100, Duration.ofMillis(100));
79+
80+
ttlCache.put(clientAddr1, proxyAddr1);
81+
assertNotNull(ttlCache.get(clientAddr1));
82+
83+
// Wait for TTL to expire
84+
Thread.sleep(150);
85+
86+
assertNull(ttlCache.get(clientAddr1), "Entry should have expired");
87+
}
88+
89+
@Test
90+
void testTTLRefreshOnAccess() throws InterruptedException {
91+
GuavaProxyAddressCache ttlCache = new GuavaProxyAddressCache(100, Duration.ofMillis(200));
92+
93+
ttlCache.put(clientAddr1, proxyAddr1);
94+
95+
// Access the entry before expiration to refresh TTL
96+
Thread.sleep(100);
97+
assertNotNull(ttlCache.get(clientAddr1));
98+
99+
// Wait another 100ms (total 200ms since last access)
100+
Thread.sleep(100);
101+
assertNotNull(ttlCache.get(clientAddr1), "Entry should still be present due to access refresh");
102+
103+
// Wait for final expiration
104+
Thread.sleep(210);
105+
assertNull(ttlCache.get(clientAddr1), "Entry should have expired");
106+
}
107+
108+
// ========== CACHE SATURATION TESTS ==========
109+
110+
@Test
111+
void testCacheSaturationBeyondCapacity() {
112+
int maxSize = 10;
113+
GuavaProxyAddressCache smallCache = new GuavaProxyAddressCache(maxSize, Duration.ofMinutes(10));
114+
115+
// Add more entries than max size
116+
int totalEntries = maxSize + 5;
117+
List<InetSocketAddress> addresses = new ArrayList<>();
118+
for (int i = 0; i < totalEntries; i++) {
119+
InetSocketAddress addr = new InetSocketAddress("192.168.1." + i, 1000 + i);
120+
addresses.add(addr);
121+
smallCache.put(addr, proxyAddr1);
122+
}
123+
124+
// Count present entries
125+
int presentCount = 0;
126+
for (InetSocketAddress addr : addresses) {
127+
if (smallCache.get(addr) != null) {
128+
presentCount++;
129+
}
130+
}
131+
132+
// Should not exceed max size
133+
assertTrue(presentCount <= maxSize, "Cache should not exceed max size");
134+
135+
// Most recent entries should still be present
136+
int recentPresentCount = 0;
137+
for (int i = totalEntries - maxSize; i < totalEntries; i++) {
138+
if (smallCache.get(addresses.get(i)) != null) {
139+
recentPresentCount++;
140+
}
141+
}
142+
assertTrue(recentPresentCount > 0, "Recent entries should still be present");
143+
}
144+
145+
// ========== CONCURRENCY TESTS ==========
146+
147+
@Test
148+
void testConcurrentTTLExpirationWithAccessPatterns() throws Exception {
149+
// Cache with 300ms TTL
150+
int ttlMs = 300;
151+
GuavaProxyAddressCache ttlCache = new GuavaProxyAddressCache(1000, Duration.ofMillis(ttlMs));
152+
153+
InetSocketAddress testAddr = new InetSocketAddress("192.168.1.100", 10000);
154+
InetSocketAddress testProxy = new InetSocketAddress("10.0.0.1", 443);
155+
156+
ExecutorService executor = Executors.newFixedThreadPool(2);
157+
AtomicInteger thread1SuccessfulGets = new AtomicInteger(0);
158+
AtomicInteger thread2SuccessfulGets = new AtomicInteger(0);
159+
CountDownLatch startLatch = new CountDownLatch(1);
160+
161+
// Thread 1: Puts value, gets it once with small pause, then long pauses
162+
Future<?> thread1 = executor.submit(() -> {
163+
try {
164+
startLatch.await();
165+
166+
// Put initial value
167+
ttlCache.put(testAddr, testProxy);
168+
169+
// Get it once immediately (should succeed)
170+
Thread.sleep(50); // Small pause (< TTL)
171+
if (ttlCache.get(testAddr) != null) {
172+
thread1SuccessfulGets.incrementAndGet();
173+
}
174+
175+
// Now start long pauses (> TTL) and check if value is still there
176+
// Without Thread 2 refreshing, these should fail
177+
for (int i = 0; i < 3; i++) {
178+
Thread.sleep(ttlMs + 100); // Wait longer than TTL
179+
if (ttlCache.get(testAddr) != null) {
180+
thread1SuccessfulGets.incrementAndGet();
181+
}
182+
}
183+
184+
} catch (InterruptedException e) {
185+
Thread.currentThread().interrupt();
186+
}
187+
});
188+
189+
// Thread 2: Fetches value with increasingly slower frequency
190+
Future<?> thread2 = executor.submit(() -> {
191+
try {
192+
startLatch.await();
193+
Thread.sleep(10); // Let thread1 put the value first
194+
195+
// Fetch with increasing delays
196+
int[] delays = {50, 100, 150, 200, 250}; // All < TTL (300ms)
197+
for (int delay : delays) {
198+
if (ttlCache.get(testAddr) != null) {
199+
thread2SuccessfulGets.incrementAndGet();
200+
}
201+
Thread.sleep(delay);
202+
}
203+
204+
// Now try with delay > TTL (should fail if no other access)
205+
Thread.sleep(ttlMs + 50);
206+
if (ttlCache.get(testAddr) != null) {
207+
thread2SuccessfulGets.incrementAndGet();
208+
}
209+
210+
} catch (InterruptedException e) {
211+
Thread.currentThread().interrupt();
212+
}
213+
});
214+
215+
// Start both threads
216+
startLatch.countDown();
217+
218+
thread1.get(10, TimeUnit.SECONDS);
219+
thread2.get(10, TimeUnit.SECONDS);
220+
executor.shutdown();
221+
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
222+
223+
// Verify expectations:
224+
// Thread 1: Should get 1 successful fetch initially, then expect ~0-1 more
225+
// (depends on timing with Thread 2's accesses)
226+
int t1Gets = thread1SuccessfulGets.get();
227+
assertTrue(t1Gets >= 1 && t1Gets <= 4,
228+
"Thread 1 should have 1-4 successful gets, got: " + t1Gets);
229+
230+
// Thread 2: Should successfully fetch 5 times (all delays < TTL)
231+
// The 6th fetch (after TTL + 50) might fail depending on Thread 1's timing
232+
int t2Gets = thread2SuccessfulGets.get();
233+
assertTrue(t2Gets >= 4 && t2Gets <= 6,
234+
"Thread 2 should have 4-6 successful gets (keeping cache alive), got: " + t2Gets);
235+
236+
// The key insight: Thread 2's regular accesses should keep the entry alive
237+
// for Thread 1's checks, at least for some of them
238+
int totalGets = t1Gets + t2Gets;
239+
assertTrue(totalGets >= 5,
240+
"Total successful gets should be at least 5, got: " + totalGets);
241+
}
242+
}
243+

0 commit comments

Comments
 (0)