Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local caching of address/ISD/AS info #54

Merged
merged 1 commit into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}