- Iterate through eepgetted subscription file instead of loading the whole thing into memory

This commit is contained in:
zzz
2011-03-22 18:20:44 +00:00
parent 311bb7a4b6
commit 5dc9214296
3 changed files with 269 additions and 41 deletions

View File

@@ -23,11 +23,14 @@ package net.i2p.addressbook;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.util.EepGet; import net.i2p.util.EepGet;
import net.i2p.util.SecureFile;
/** /**
* An address book for storing human readable names mapped to base64 i2p * An address book for storing human readable names mapped to base64 i2p
@@ -39,11 +42,12 @@ import net.i2p.util.EepGet;
*/ */
class AddressBook { class AddressBook {
private String location; private final String location;
/** either addresses or subFile will be non-null, but not both */
private Map<String, String> addresses; private final Map<String, String> addresses;
private final File subFile;
private boolean modified; private boolean modified;
private static final boolean DEBUG = true;
/** /**
* Construct an AddressBook from the contents of the Map addresses. * Construct an AddressBook from the contents of the Map addresses.
@@ -54,6 +58,8 @@ class AddressBook {
*/ */
public AddressBook(Map<String, String> addresses) { public AddressBook(Map<String, String> addresses) {
this.addresses = addresses; this.addresses = addresses;
this.subFile = null;
this.location = null;
} }
/* /*
@@ -81,6 +87,7 @@ class AddressBook {
} }
*/ */
static final long MAX_SUB_SIZE = 3 * 1024 * 1024l; //about 5,000 hosts static final long MAX_SUB_SIZE = 3 * 1024 * 1024l; //about 5,000 hosts
/** /**
* Construct an AddressBook from the Subscription subscription. If the * Construct an AddressBook from the Subscription subscription. If the
* address book at subscription has not changed since the last time it was * address book at subscription has not changed since the last time it was
@@ -89,33 +96,46 @@ class AddressBook {
* *
* Yes, the EepGet fetch() is done in this constructor. * Yes, the EepGet fetch() is done in this constructor.
* *
* This stores the subscription in a temporary file and does not read the whole thing into memory.
* An AddressBook created with this constructor may not be modified or written using write().
* It may be a merge source (an parameter for another AddressBook's merge())
* but may not be a merge target (this.merge() will throw an exception).
*
* @param subscription * @param subscription
* A Subscription instance pointing at a remote address book. * A Subscription instance pointing at a remote address book.
* @param proxyHost hostname of proxy * @param proxyHost hostname of proxy
* @param proxyPort port number of proxy * @param proxyPort port number of proxy
*/ */
public AddressBook(Subscription subscription, String proxyHost, int proxyPort) { public AddressBook(Subscription subscription, String proxyHost, int proxyPort) {
File tmp = new File(I2PAppContext.getGlobalContext().getTempDir(), "addressbook.tmp"); Map<String, String> a = null;
File subf = null;
try {
File tmp = SecureFile.createTempFile("addressbook", null, I2PAppContext.getGlobalContext().getTempDir());
EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true,
proxyHost, proxyPort, 0, -1l, MAX_SUB_SIZE, tmp.getAbsolutePath(), null,
subscription.getLocation(), true, subscription.getEtag(), subscription.getLastModified(), null);
if (get.fetch()) {
subscription.setEtag(get.getETag());
subscription.setLastModified(get.getLastModified());
subscription.setLastFetched(I2PAppContext.getGlobalContext().clock().now());
subf = tmp;
} else {
a = Collections.EMPTY_MAP;
tmp.delete();
}
} catch (IOException ioe) {
a = Collections.EMPTY_MAP;
}
this.addresses = a;
this.subFile = subf;
this.location = subscription.getLocation(); this.location = subscription.getLocation();
EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true,
proxyHost, proxyPort, 0, -1l, MAX_SUB_SIZE, tmp.getAbsolutePath(), null,
subscription.getLocation(), true, subscription.getEtag(), subscription.getLastModified(), null);
if (get.fetch()) {
subscription.setEtag(get.getETag());
subscription.setLastModified(get.getLastModified());
subscription.setLastFetched(I2PAppContext.getGlobalContext().clock().now());
}
try {
this.addresses = ConfigParser.parse(tmp);
} catch (IOException exp) {
this.addresses = new HashMap();
}
tmp.delete();
} }
/** /**
* Construct an AddressBook from the contents of the file at file. If the * Construct an AddressBook from the contents of the file at file. If the
* file cannot be read, construct an empty AddressBook * file cannot be read, construct an empty AddressBook.
* This reads the entire file into memory.
* The resulting map is modifiable and may be a merge target.
* *
* @param file * @param file
* A File pointing at a file with lines in the format * A File pointing at a file with lines in the format
@@ -124,22 +144,38 @@ class AddressBook {
*/ */
public AddressBook(File file) { public AddressBook(File file) {
this.location = file.toString(); this.location = file.toString();
Map<String, String> a;
try { try {
this.addresses = ConfigParser.parse(file); a = ConfigParser.parse(file);
} catch (IOException exp) { } catch (IOException exp) {
this.addresses = new HashMap(); a = new HashMap();
} }
this.addresses = a;
this.subFile = null;
} }
/** /**
* Return a Map containing the addresses in the AddressBook. * Return an iterator over the addresses in the AddressBook.
* * @since 0.8.6
* @return A Map containing the addresses in the AddressBook, where the key
* is a human readable name, and the value is a base64 i2p
* destination.
*/ */
public Map<String, String> getAddresses() { public Iterator<Map.Entry<String, String>> iterator() {
return this.addresses; if (this.subFile != null)
return new ConfigIterator(this.subFile);
return this.addresses.entrySet().iterator();
}
/**
* Delete the temp file or clear the map.
* @since 0.8.6
*/
public void delete() {
if (this.subFile != null) {
this.subFile.delete();
} else if (this.addresses != null) {
try {
this.addresses.clear();
} catch (UnsupportedOperationException uoe) {}
}
} }
/** /**
@@ -147,19 +183,22 @@ class AddressBook {
* *
* @return A String representing either an abstract path, or a url, * @return A String representing either an abstract path, or a url,
* depending on how the instance was constructed. * depending on how the instance was constructed.
* Will be null if created with the Map constructor.
*/ */
public String getLocation() { public String getLocation() {
return this.location; return this.location;
} }
/** /**
* Return a string representation of the contents of the AddressBook. * Return a string representation of the origin of the AddressBook.
* *
* @return A String representing the contents of the AddressBook. * @return A String representing the origin of the AddressBook.
*/ */
@Override @Override
public String toString() { public String toString() {
return this.addresses.toString(); if (this.location != null)
return "Book from " + this.location;
return "Map containing " + this.addresses.size() + " entries";
} }
private static final int MIN_DEST_LENGTH = 516; private static final int MIN_DEST_LENGTH = 516;
@@ -222,16 +261,21 @@ class AddressBook {
* @param overwrite True to overwrite * @param overwrite True to overwrite
* @param log * @param log
* The log to write messages about new addresses or conflicts to. * The log to write messages about new addresses or conflicts to.
*
* @throws IllegalStateException if this was created with the Subscription constructor.
*/ */
public void merge(AddressBook other, boolean overwrite, Log log) { public void merge(AddressBook other, boolean overwrite, Log log) {
for (Map.Entry<String, String> entry : other.addresses.entrySet()) { if (this.addresses == null)
throw new IllegalStateException();
for (Iterator<Map.Entry<String, String>> iter = other.iterator(); iter.hasNext(); ) {
Map.Entry<String, String> entry = iter.next();
String otherKey = entry.getKey(); String otherKey = entry.getKey();
String otherValue = entry.getValue(); String otherValue = entry.getValue();
if (isValidKey(otherKey) && isValidDest(otherValue)) { if (isValidKey(otherKey) && isValidDest(otherValue)) {
if (this.addresses.containsKey(otherKey) && !overwrite) { if (this.addresses.containsKey(otherKey) && !overwrite) {
if (!this.addresses.get(otherKey).equals(otherValue) if (DEBUG && log != null &&
&& log != null) { !this.addresses.get(otherKey).equals(otherValue)) {
log.append("Conflict for " + otherKey + " from " log.append("Conflict for " + otherKey + " from "
+ other.location + other.location
+ ". Destination in remote address book is " + ". Destination in remote address book is "
@@ -256,8 +300,12 @@ class AddressBook {
* *
* @param file * @param file
* The file to write the contents of this AddressBook too. * The file to write the contents of this AddressBook too.
*
* @throws IllegalStateException if this was created with the Subscription constructor.
*/ */
public void write(File file) { public void write(File file) {
if (this.addresses == null)
throw new IllegalStateException();
if (this.modified) { if (this.modified) {
try { try {
ConfigParser.write(this.addresses, file); ConfigParser.write(this.addresses, file);
@@ -271,8 +319,17 @@ class AddressBook {
* Write this AddressBook out to the file it was read from. Requires that * Write this AddressBook out to the file it was read from. Requires that
* AddressBook was constructed from a file on the local filesystem. If the * AddressBook was constructed from a file on the local filesystem. If the
* file cannot be writen to, this method will silently fail. * file cannot be writen to, this method will silently fail.
*
* @throws IllegalStateException if this was not created with the File constructor.
*/ */
public void write() { public void write() {
if (this.location == null || this.location.startsWith("http://"))
throw new IllegalStateException();
this.write(new File(this.location)); this.write(new File(this.location));
} }
@Override
protected void finalize() {
delete();
}
} }

View File

@@ -0,0 +1,144 @@
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
/**
* A class to iterate through a hosts.txt or config file without
* reading the whole thing into memory.
* Keys are always converted to lower case.
*
* Callers should iterate all the way through or call close()
* to ensure the underlying stream is closed.
*
* @since 0.8.6
*/
class ConfigIterator implements Iterator<Map.Entry<String, String>> {
private BufferedReader input;
private ConfigEntry next;
/**
* A dummy iterator in which hasNext() is always false.
*/
public ConfigIterator() {}
/**
* An iterator over the key/value pairs in the file.
*/
public ConfigIterator(File file) {
try {
FileInputStream fileStream = new FileInputStream(file);
input = new BufferedReader(new InputStreamReader(fileStream));
} catch (IOException ioe) {}
}
public boolean hasNext() {
if (input == null)
return false;
if (next != null)
return true;
try {
String inputLine = input.readLine();
while (inputLine != null) {
inputLine = ConfigParser.stripComments(inputLine);
String[] splitLine = inputLine.split("=");
if (splitLine.length == 2) {
next = new ConfigEntry(splitLine[0].trim().toLowerCase(), splitLine[1].trim());
return true;
}
inputLine = input.readLine();
}
} catch (IOException ioe) {}
try { input.close(); } catch (IOException ioe) {}
input = null;
next = null;
return false;
}
public Map.Entry<String, String> next() {
if (!hasNext())
throw new NoSuchElementException();
Map.Entry<String, String> rv = next;
next = null;
return rv;
}
public void remove() {
throw new UnsupportedOperationException();
}
public void close() {
if (input != null) {
try { input.close(); } catch (IOException ioe) {}
}
}
@Override
protected void finalize() {
close();
}
/**
* The object returned by the iterator.
*/
private static class ConfigEntry implements Map.Entry<String, String> {
private final String key;
private final String value;
public ConfigEntry(String k, String v) {
key = k;
value = v;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
public String setValue(String v) {
throw new UnsupportedOperationException();
}
public int hashCode() {
return key.hashCode() ^ value.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry) o;
return key.equals(e.getKey()) && value.equals(e.getValue());
}
}
}

View File

@@ -45,6 +45,7 @@ public class Daemon {
public static final String VERSION = "2.0.4"; public static final String VERSION = "2.0.4";
private static final Daemon _instance = new Daemon(); private static final Daemon _instance = new Daemon();
private boolean _running; private boolean _running;
private static final boolean DEBUG = true;
/** /**
* Update the router and published address books using remote data from the * Update the router and published address books using remote data from the
@@ -103,8 +104,15 @@ public class Daemon {
Iterator<AddressBook> iter = subscriptions.iterator(); Iterator<AddressBook> iter = subscriptions.iterator();
while (iter.hasNext()) { while (iter.hasNext()) {
// yes, the EepGet fetch() is done in next() // yes, the EepGet fetch() is done in next()
long start = System.currentTimeMillis();
AddressBook sub = iter.next(); AddressBook sub = iter.next();
for (Map.Entry<String, String> entry : sub.getAddresses().entrySet()) { long end = System.currentTimeMillis();
if (DEBUG && log != null)
log.append("Fetch of " + sub.getLocation() + " took " + (end - start));
start = end;
int old = 0, nnew = 0, invalid = 0, conflict = 0;
for (Iterator<Map.Entry<String, String>> eIter = sub.iterator(); eIter.hasNext(); ) {
Map.Entry<String, String> entry = eIter.next();
String key = entry.getKey(); String key = entry.getKey();
Destination oldDest = router.lookup(key); Destination oldDest = router.lookup(key);
try { try {
@@ -127,21 +135,40 @@ public class Daemon {
if (!success) if (!success)
log.append("Save to published addressbook " + published.getAbsolutePath() + " failed for new key " + key); log.append("Save to published addressbook " + published.getAbsolutePath() + " failed for new key " + key);
} }
nnew++;
} else if (log != null) { } else if (log != null) {
log.append("Bad hostname " + key + " from " log.append("Bad hostname " + key + " from "
+ sub.getLocation()); + sub.getLocation());
invalid++;
} }
} else if (!oldDest.toBase64().equals(entry.getValue()) && log != null) { } else if (DEBUG && log != null) {
log.append("Conflict for " + key + " from " if (!oldDest.toBase64().equals(entry.getValue())) {
+ sub.getLocation() log.append("Conflict for " + key + " from "
+ ". Destination in remote address book is " + sub.getLocation()
+ entry.getValue()); + ". Destination in remote address book is "
+ entry.getValue());
conflict++;
} else {
old++;
}
} else {
old++;
} }
} catch (DataFormatException dfe) { } catch (DataFormatException dfe) {
if (log != null) if (log != null)
log.append("Invalid b64 for" + key + " From: " + sub.getLocation()); log.append("Invalid b64 for" + key + " From: " + sub.getLocation());
invalid++;
} }
} }
if (DEBUG && log != null) {
log.append("Merge of " + sub.getLocation() + " into " + router +
" took " + (System.currentTimeMillis() - start) + " ms with " +
nnew + " new, " +
old + " old, " +
invalid + " invalid, " +
conflict + " conflicts");
}
sub.delete();
} }
} }