Skip to content

Commit

Permalink
Address caching (#54)
Browse files Browse the repository at this point in the history
Co-authored-by: Tilmann Zäschke <tilmann.zaeschke@inf.ethz.ch>
  • Loading branch information
tzaeschke and Tilmann Zäschke authored Apr 26, 2024
1 parent 24e4657 commit 49fc193
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Demo cleanup an new `ScmpShowpathsDemo`.
[#49](https://github.com/netsec-ethz/scion-java-client/pull/49)
- Channel demo cleanup. [#52](https://github.com/netsec-ethz/scion-java-client/pull/52)
- Address/ISD/AS caching. [#54](https://github.com/netsec-ethz/scion-java-client/pull/54)

### Changed
- BREAKING CHANGE: Changed maven artifactId to "client"
Expand Down
34 changes: 27 additions & 7 deletions src/main/java/org/scion/jpan/ScionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.scion.internal.SimpleCache;
import org.scion.jpan.internal.DNSHelper;
import org.scion.jpan.internal.HostsFileParser;
import org.scion.jpan.internal.ScionBootstrapper;
Expand Down Expand Up @@ -81,6 +82,7 @@ public class ScionService {
private Thread shutdownHook;
private final java.nio.channels.DatagramChannel[] ifDiscoveryChannel = {null};
private final HostsFileParser hostsFile = new HostsFileParser();
private final SimpleCache<String, ScionAddress> scionAddressCache = new SimpleCache<>(100);

protected enum Mode {
DAEMON,
Expand Down Expand Up @@ -294,24 +296,26 @@ List<Daemon.Path> getPathListDaemon(long srcIsdAs, long dstIsdAs) {
/**
* Requests paths from the local ISD/AS to the destination.
*
* @param dstAddress Destination IP address
* @param dstAddress Destination IP address. It will try to perform a DNS look up to map the
* hostName to SCION address.
* @return All paths returned by the path service.
* @throws IOException if an errors occurs while querying paths.
*/
public List<RequestPath> getPaths(InetSocketAddress dstAddress) throws IOException {
long dstIsdAs = getIsdAs(dstAddress.getHostString());
return getPaths(dstIsdAs, dstAddress);
ScionAddress sa = getScionAddress(dstAddress.getHostName());
return getPaths(sa.getIsdAs(), sa.getInetAddress(), dstAddress.getPort());
}

/**
* Request paths from the local ISD/AS to the destination.
*
* @param dstIsdAs Destination ISD/AS
* @param dstAddress Destination IP address
* @param dstScionAddress Destination IP address. Must belong to a SCION enabled end host.
* @return All paths returned by the path service.
*/
public List<RequestPath> getPaths(long dstIsdAs, InetSocketAddress dstAddress) {
return getPaths(dstIsdAs, dstAddress.getAddress(), dstAddress.getPort());
public List<RequestPath> getPaths(long dstIsdAs, InetSocketAddress dstScionAddress) {
// TODO change method API name to make clear that this requires a SCION IP.
return getPaths(dstIsdAs, dstScionAddress.getAddress(), dstScionAddress.getPort());
}

/**
Expand Down Expand Up @@ -372,6 +376,11 @@ public long getLocalIsdAs() {
* @throws ScionException if the DNS/TXT lookup did not return a (valid) SCION address.
*/
public long getIsdAs(String hostName) throws ScionException {
ScionAddress scionAddress = scionAddressCache.get(hostName);
if (scionAddress != null) {
return scionAddress.getIsdAs();
}

// Look for TXT in application properties
String txtFromProperties = findTxtRecordInProperties(hostName);
if (txtFromProperties != null) {
Expand Down Expand Up @@ -408,6 +417,11 @@ public long getIsdAs(String hostName) throws ScionException {
* @throws ScionException if the DNS/TXT lookup did not return a (valid) SCION address.
*/
public ScionAddress getScionAddress(String hostName) throws ScionException {
ScionAddress scionAddress = scionAddressCache.get(hostName);
if (scionAddress != null) {
return scionAddress;
}

// Look for TXT in application properties
String txtFromProperties = findTxtRecordInProperties(hostName);
if (txtFromProperties != null) {
Expand All @@ -433,12 +447,18 @@ public ScionAddress getScionAddress(String hostName) throws ScionException {
ScionAddress fromDNS =
DNSHelper.queryTXT(hostName, DNS_TXT_KEY, x -> parseTxtRecord(x, hostName));
if (fromDNS != null) {
return fromDNS;
return addToCache(fromDNS);
}

throw new ScionException("No DNS TXT entry \"scion\" found for host: " + hostName);
}

private ScionAddress addToCache(ScionAddress address) {
scionAddressCache.put(address.getHostName(), address);
scionAddressCache.put(address.getInetAddress().getHostAddress(), address);
return address;
}

private boolean isLocalhost(String hostName) {
return hostName.startsWith("127.0.0.")
|| "::1".equals(hostName)
Expand Down
103 changes: 103 additions & 0 deletions src/main/java/org/scion/jpan/internal/SimpleCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2024 ETH Zurich
//
// 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 org.scion.internal;

import java.util.HashMap;
import java.util.TreeMap;

/**
* A simple cache that min/max watermark. Once max watermark is reached, the cache removes entries
* until min watermark is reached. Least recently accessed entries are removed first.
*
* @param <K> The key
* @param <V> The value
*/
public class SimpleCache<K, V> {
private final TreeMap<Long, Entry> ageMap = new TreeMap<>();
private final HashMap<K, Entry> lookupMap = new HashMap<>();

private int capacity;
private long opCount = 0;

public SimpleCache(int capacity) {
if (capacity < 1) {
throw new IllegalArgumentException();
}
this.capacity = capacity;
}

public void put(K key, V value) {
Entry e = lookupMap.get(key);
if (e == null) {
e = new Entry(opCount++, key, value);
checkCapacity(1);
lookupMap.put(key, e);
} else {
ageMap.remove(e.age);
e.age = opCount++;
e.value = value;
}
ageMap.put(e.age, e);
}

public V get(K key) {
Entry e = lookupMap.get(key);
if (e == null) {
return null;
}

ageMap.remove(e.age);
e.age = opCount++;
ageMap.put(e.age, e);

return e.value;
}

public void clear() {
lookupMap.clear();
ageMap.clear();
}

public void setCapacity(int capacity) {
if (capacity < 1) {
throw new IllegalArgumentException();
}
this.capacity = capacity;
checkCapacity(0);
}

public int getCapacity() {
return capacity;
}

private void checkCapacity(int spare) {
while (lookupMap.size() + spare > capacity) {
Entry e = ageMap.pollFirstEntry().getValue();
lookupMap.remove(e.key);
}
}

private class Entry {
private long age;
private final K key;
private V value;

public Entry(long l, K key, V value) {
this.age = l;
this.key = key;
this.value = value;
}
}
}
117 changes: 117 additions & 0 deletions src/test/java/org/scion/jpan/internal/SimpleCacheTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2024 ETH Zurich
//
// 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 org.scion.internal;

import static org.junit.jupiter.api.Assertions.*;

import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;

class SimpleCacheTest {

private static class Pair {
int i;
String s;

Pair(int i, String s) {
this.i = i;
this.s = s;
}
}

@Test
void testInsertOnly() {
SimpleCache<Integer, Pair> cache = new SimpleCache<>(10);
List<Pair> list = new ArrayList<>();

for (int i = 0; i < 30; i++) {
Pair p = new Pair(i, Integer.toString(i));
cache.put(i, p);
list.add(p);
}

// Test latest 10 remain
for (int i = 0; i < list.size(); i++) {
if (i < 20) {
assertNull(cache.get(i));
} else {
assertEquals(list.get(i), cache.get(i));
}
}
}

@Test
void testMultiGet() {
SimpleCache<Integer, Pair> cache = new SimpleCache<>(10);
List<Pair> list = new ArrayList<>();

for (int i = 0; i < 30; i++) {
Pair p = new Pair(i, Integer.toString(i));
cache.put(i, p);
list.add(p);
// keep using the first 5 entries
assertNotNull(cache.get(i % 5));
}

// Test latest 5 remain and most used 5 remain
for (int i = 0; i < list.size(); i++) {
if (i >= 5 && i < 25) {
assertNull(cache.get(i));
} else {
assertEquals(list.get(i), cache.get(i));
}
}
}

@Test
void testReduceCapacity() {
SimpleCache<Integer, Pair> cache = new SimpleCache<>(10);
List<Pair> list = new ArrayList<>();

for (int i = 0; i < 30; i++) {
Pair p = new Pair(i, Integer.toString(i));
cache.put(i, p);
list.add(p);
// keep using the first 2 entries
assertNotNull(cache.get(i % 2));
}

cache.setCapacity(5);
assertEquals(5, cache.getCapacity());

// Test latest 3 remain and most used 2 remain
for (int i = 0; i < list.size(); i++) {
if (i >= 2 && i < 27) {
assertNull(cache.get(i));
} else {
assertEquals(list.get(i), cache.get(i));
}
}
}

@Test
void testSetCapacity() {
SimpleCache<Integer, Pair> cache = new SimpleCache<>(42);
assertEquals(42, cache.getCapacity());

cache.setCapacity(100);
assertEquals(100, cache.getCapacity());

assertThrows(IllegalArgumentException.class, () -> cache.setCapacity(0));

assertThrows(IllegalArgumentException.class, () -> new SimpleCache<>(0));
}
}
35 changes: 34 additions & 1 deletion src/test/java/org/scion/jpan/testutil/DNSUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@

public class DNSUtil {

public static InetAddress install(String asHost, byte[] addrBytes) {
try {
Name name = Name.fromString(asHost + ".");
Cache c = Lookup.getDefaultCache(DClass.IN);
if (addrBytes.length == 4) {
ARecord a = new ARecord(name, DClass.IN, 5000, InetAddress.getByAddress(addrBytes));
c.addRecord(a, 10);
// remove trailing top-level `.`
return InetAddress.getByAddress(asHost, a.getAddress().getAddress());
} else if (addrBytes.length == 16) {
AAAARecord a = new AAAARecord(name, DClass.IN, 5000, InetAddress.getByAddress(addrBytes));
c.addRecord(a, 10);
// remove trailing top-level `.`
return InetAddress.getByAddress(asHost, a.getAddress().getAddress());
} else {
throw new IllegalArgumentException();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void installNAPTR(String asHost, byte[] topoAddr, int topoPort) {
installNAPTR(asHost, topoAddr, "x-sciondiscovery=" + topoPort, "x-sciondiscovery:tcp");
}
Expand Down Expand Up @@ -60,7 +82,7 @@ public static void installScionTXT(String asHost, String isdAs, String ipAddress

public static void installTXT(String asHost, String entry) {
try {
Name name = Name.fromString(asHost + "."); // "inf.ethz.ch.";
Name name = Name.fromString(asHost + ".");
Cache c = Lookup.getDefaultCache(DClass.IN);
TXTRecord txt = new TXTRecord(name, DClass.IN, 5000, entry);
c.addRecord(txt, 10);
Expand All @@ -72,4 +94,15 @@ public static void installTXT(String asHost, String entry) {
public static void clear() {
Lookup.setDefaultCache(new Cache(), DClass.IN);
}

private static Name toName(String hostName) {
try {
if (hostName.endsWith(".")) {
return Name.fromString(hostName);
}
return Name.fromString(hostName + ".");
} catch (TextParseException e) {
throw new RuntimeException(e);
}
}
}

0 comments on commit 49fc193

Please sign in to comment.