From 1a8847d1774f3462be29e36e6a105c0f38504f74 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Wed, 20 Apr 2016 00:41:45 +0000 Subject: [PATCH] Blockfile: Add method to change serialization schema for a skiplist Fix delIndex() method, broken and previously unused Improve javadocs BlockfileNamingService: New database version 4, allows for multiple destinations per hostname Disallow database version higher than supported --- .../client/naming/BlockfileNamingService.java | 234 ++++++++++++++---- .../net/metanotion/io/block/BlockFile.java | 94 ++++++- .../io/block/index/BSkipLevels.java | 2 +- .../metanotion/io/block/index/BSkipList.java | 1 - 4 files changed, 279 insertions(+), 52 deletions(-) diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java index efa8b3e064..6627a1be41 100644 --- a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java +++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java @@ -54,7 +54,7 @@ import net.metanotion.util.skiplist.SkipList; * * "%%__INFO__%%" is the master database skiplist, containing one entry: * "info": a Properties, serialized with DataHelper functions: - * "version": "2" + * "version": "4" * "created": Java long time (ms) * "upgraded": Java long time (ms) (as of database version 2) * "lists": Comma-separated list of host databases, to be @@ -74,8 +74,12 @@ import net.metanotion.util.skiplist.SkipList; * the hosts for that database. * The keys/values in these skiplists are as follows: * key: a UTF-8 String - * value: a DestEntry, which is a Properties (serialized with DataHelper) - * followed by a Destination (serialized as usual). + * value: a DestEntry, which is: + * a one-byte count of the Properties/Destination pairs to follow + * (as of database version 4, otherwise one) + * that many pairs of: + * Properties (serialized with DataHelper) + * Destination (serialized as usual). * * * The DestEntry Properties typically contains: @@ -98,11 +102,15 @@ public class BlockfileNamingService extends DummyNamingService { private final Map<String, String> _negativeCache; private volatile boolean _isClosed; private final boolean _readOnly; + private String _version = "0"; private boolean _needsUpgrade; private static final Serializer _infoSerializer = new PropertiesSerializer(); private static final Serializer _stringSerializer = new UTF8StringBytes(); - private static final Serializer _destSerializer = new DestEntrySerializer(); + private static final Serializer _destSerializerV1 = new DestEntrySerializer(); + private static final Serializer _destSerializerV4 = new DestEntrySerializerV4(); + // upgrade(), initExisting(), and initNew() will change this to _destSerializerV4 + private volatile Serializer _destSerializer = _destSerializerV1; private static final Serializer _hashIndexSerializer = new IntBytes(); private static final String HOSTS_DB = "hostsdb.blockfile"; @@ -116,7 +124,7 @@ public class BlockfileNamingService extends DummyNamingService { private static final String PROP_LISTS = "lists"; private static final String PROP_CREATED = "created"; private static final String PROP_UPGRADED = "upgraded"; - private static final String VERSION = "3"; + private static final String VERSION = "4"; private static final String PROP_ADDED = "a"; private static final String PROP_SOURCE = "s"; @@ -169,8 +177,9 @@ public class BlockfileNamingService extends DummyNamingService { if (raf != null) { try { raf.close(); } catch (IOException e) {} } - File corrupt = new File(_context.getRouterDir(), HOSTS_DB + ".corrupt"); - _log.log(Log.CRIT, "Corrupt or unreadable database " + f + ", moving to " + corrupt + + File corrupt = new File(_context.getRouterDir(), HOSTS_DB + '.' + System.currentTimeMillis() + ".corrupt"); + _log.log(Log.CRIT, "Corrupt, unsupported version, or unreadable database " + + f + ", moving to " + corrupt + " and creating new database", ioe); boolean success = f.renameTo(corrupt); if (!success) @@ -183,7 +192,7 @@ public class BlockfileNamingService extends DummyNamingService { // so we must create and retain a RAF so we may close it later raf = new RAIFile(f, true, true); SecureFileOutputStream.setPerms(f); - bf = init(raf); + bf = initNew(raf); } catch (IOException ioe) { if (raf != null) { try { raf.close(); } catch (IOException e) {} @@ -206,8 +215,10 @@ public class BlockfileNamingService extends DummyNamingService { * privatehosts.txt, userhosts.txt, and hosts.txt, * creating a skiplist in the database for each. */ - private BlockFile init(RAIFile f) throws IOException { + private BlockFile initNew(RAIFile f) throws IOException { long start = _context.clock().now(); + _version = VERSION; + _destSerializer = _destSerializerV4; try { BlockFile rv = new BlockFile(f, true); SkipList hdr = rv.makeIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer); @@ -301,15 +312,24 @@ public class BlockfileNamingService extends DummyNamingService { } String version = info.getProperty(PROP_VERSION); - _needsUpgrade = needsUpgrade(bf, version); + if (version == null) + throw new IOException("No version"); + if (VersionComparator.comp(version, VERSION) > 0) + throw new IOException("Database version is " + version + + " but this implementation only supports versions 1-" + VERSION + + " Did you downgrade I2P??"); + _version = version; + if (VersionComparator.comp(version, "4") >= 0) + _destSerializer = _destSerializerV4; + _needsUpgrade = needsUpgrade(bf); if (_needsUpgrade) { if (_log.shouldLog(Log.WARN)) - _log.warn("Upgrading from database version " + version + " to " + VERSION + - " created " + (new Date(createdOn)).toString() + + _log.warn("Upgrading database from version " + _version + " to " + VERSION + + ", created " + (new Date(createdOn)).toString() + " containing lists: " + list); } else { if (_log.shouldLog(Log.INFO)) - _log.info("Found database version " + version + + _log.info("Found database version " + _version + " created " + (new Date(createdOn)).toString() + " containing lists: " + list); } @@ -333,12 +353,11 @@ public class BlockfileNamingService extends DummyNamingService { * @throws IOE on bad version * @since 0.8.9 */ - private boolean needsUpgrade(BlockFile bf, String version) throws IOException { - if (version != null && VersionComparator.comp(version, VERSION) >= 0) + private boolean needsUpgrade(BlockFile bf) throws IOException { + if (VersionComparator.comp(_version, VERSION) >= 0) return false; if (!bf.file.canWrite()) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Not upgrading read-only database version " + version); + _log.logAlways(Log.WARN, "Not upgrading read-only database version " + _version); return false; } return true; @@ -350,41 +369,57 @@ public class BlockfileNamingService extends DummyNamingService { * Version 1->2: Add reverse skiplist and populate * Version 2->3: Re-populate reverse skiplist as version 2 didn't keep it updated * after the upgrade. No change to format. + * Version 3->4: Change format to support multiple destinations per hostname * * @return true if upgraded successfully * @since 0.8.9 */ private boolean upgrade() { try { - // wasn't there in version 1, is there in version 2 - SkipList rev = _bf.getIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer); - if (rev == null) { + // version 1 -> version 2 + // Add reverse skiplist + if (VersionComparator.comp(_version, "2") < 0) { + SkipList rev = _bf.getIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer); + if (rev == null) { + rev = _bf.makeIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer); + if (_log.shouldLog(Log.WARN)) + _log.warn("Created reverse index"); + } + setVersion("2"); + } + + // version 2 -> version 3 + // no change in format, just regenerate skiplist + if (VersionComparator.comp(_version, "3") < 0) { + Map<String, Destination> entries = getEntries(); + int i = 0; + for (Map.Entry<String, Destination> entry : entries.entrySet()) { + addReverseEntry(entry.getKey(), entry.getValue()); + i++; + } + // i may be greater than skiplist keys if there are dups if (_log.shouldLog(Log.WARN)) - _log.warn("Created reverse index"); - rev = _bf.makeIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer); + _log.warn("Updated reverse index with " + i + " entries"); + setVersion("3"); } - Map<String, Destination> entries = getEntries(); - long start = System.currentTimeMillis(); - int i = 0; - for (Map.Entry<String, Destination> entry : entries.entrySet()) { - addReverseEntry(entry.getKey(), entry.getValue()); - i++; + + // version 3 -> version 4 + // support multiple destinations per hostname + if (VersionComparator.comp(_version, "4") < 0) { + for (String list : _lists) { + try { + if (_log.shouldWarn()) + _log.warn("Upgrading " + list + " from database version 3 to 4"); + _bf.reformatIndex(list, _stringSerializer, _destSerializerV1, + _stringSerializer, _destSerializerV4); + } catch (IOException ioe) { + _log.error("Failed upgrade of list " + list + " to version 4", ioe); + } + } + _destSerializer = _destSerializerV4; + setVersion("4"); } - // i may be greater than skiplist keys if there are dups - if (_log.shouldLog(Log.WARN)) - _log.warn("Updated reverse index with " + i + " entries"); - SkipList hdr = _bf.getIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer); - if (hdr == null) - throw new IOException("No db header"); - Properties info = (Properties) hdr.get(PROP_INFO); - if (info == null) - throw new IOException("No header info"); - info.setProperty(PROP_VERSION, VERSION); - info.setProperty(PROP_UPGRADED, Long.toString(_context.clock().now())); - hdr.put(PROP_INFO, info); - if (_log.shouldLog(Log.WARN)) - _log.warn("Upgraded to version " + VERSION + " in " + - DataHelper.formatDuration(System.currentTimeMillis() - start)); + return true; } catch (IOException ioe) { _log.error("Error upgrading DB", ioe); @@ -394,6 +429,29 @@ public class BlockfileNamingService extends DummyNamingService { return false; } + /** + * Save new version number in blockfile after upgrade. + * Blockfile must be writable, of course. + * Side effect: sets _version field + * + * Caller must synchronize + * @since 0.9.26 pulled out of upgrade() + */ + private void setVersion(String version) throws IOException { + SkipList hdr = _bf.getIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer); + if (hdr == null) + throw new IOException("No db header"); + Properties info = (Properties) hdr.get(PROP_INFO); + if (info == null) + throw new IOException("No header info"); + info.setProperty(PROP_VERSION, version); + info.setProperty(PROP_UPGRADED, Long.toString(_context.clock().now())); + hdr.put(PROP_INFO, info); + if (_log.shouldLog(Log.WARN)) + _log.warn("Upgraded database from version " + _version + " to version " + version); + _version = version; + } + /** * Caller must synchronize * @return entry or null, or throws ioe @@ -1325,6 +1383,10 @@ public class BlockfileNamingService extends DummyNamingService { public Properties props; /** may not be null */ public Destination dest; + /** may be null - v4 only - same size as destList - may contain null entries */ + public List<Properties> propsList; + /** may be null - v4 only - same size as propsList */ + public List<Destination> destList; @Override public String toString() { @@ -1356,7 +1418,7 @@ public class BlockfileNamingService extends DummyNamingService { logError("DB Write Fail - properties too big?", dfe); // null properties is a two-byte length of 0. baos.write(new byte[2]); - } + } de.dest.writeBytes(baos); } catch (IOException ioe) { logError("DB Write Fail", ioe); @@ -1386,6 +1448,75 @@ public class BlockfileNamingService extends DummyNamingService { } } + /** + * For multiple destinations per hostname + * @since 0.9.26 + */ + private static class DestEntrySerializerV4 implements Serializer { + + public byte[] getBytes(Object o) { + DestEntry de = (DestEntry) o; + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + int sz = de.destList != null ? de.destList.size() : 1; + try { + baos.write((byte) sz); + for (int i = 0; i < sz; i++) { + Properties p; + Destination d; + if (i == 0) { + p = de.props; + d = de.dest; + } else { + p = de.propsList.get(i); + d = de.destList.get(i); + } + try { + DataHelper.writeProperties(baos, p, true, false); + } catch (DataFormatException dfe) { + logError("DB Write Fail - properties too big?", dfe); + baos.write(new byte[2]); + } + d.writeBytes(baos); + } + } catch (IOException ioe) { + logError("DB Write Fail", ioe); + } catch (DataFormatException dfe) { + logError("DB Write Fail", dfe); + } + return baos.toByteArray(); + } + + /** returns null on error */ + public Object construct(byte[] b) { + DestEntry rv = new DestEntry(); + ByteArrayInputStream bais = new ByteArrayInputStream(b); + try { + int sz = bais.read() & 0xff; + if (sz <= 0) + throw new DataFormatException("bad dest count " + sz); + rv.props = DataHelper.readProperties(bais); + rv.dest = Destination.create(bais); + if (sz > 1) { + rv.propsList = new ArrayList<Properties>(sz); + rv.destList = new ArrayList<Destination>(sz); + rv.propsList.add(rv.props); + rv.destList.add(rv.dest); + for (int i = 1; i < sz; i++) { + rv.propsList.add(DataHelper.readProperties(bais)); + rv.destList.add(Destination.create(bais)); + } + } + } catch (IOException ioe) { + logError("DB Read Fail", ioe); + return null; + } catch (DataFormatException dfe) { + logError("DB Read Fail", dfe); + return null; + } + return rv; + } + } + /** * Used to store entries that need deleting */ @@ -1410,6 +1541,17 @@ public class BlockfileNamingService extends DummyNamingService { I2PAppContext ctx = new I2PAppContext(ctxProps); BlockfileNamingService bns = new BlockfileNamingService(ctx); /**** + Properties sprops = new Properties(); + String lname = "privatehosts.txt"; + sprops.setProperty("list", lname); + System.out.println("List " + lname + " contains " + bns.size(sprops)); + lname = "userhosts.txt"; + sprops.setProperty("list", lname); + System.out.println("List " + lname + " contains " + bns.size(sprops)); + lname = "hosts.txt"; + sprops.setProperty("list", lname); + System.out.println("List " + lname + " contains " + bns.size(sprops)); + List<String> names = null; Properties props = new Properties(); try { @@ -1479,12 +1621,14 @@ public class BlockfileNamingService extends DummyNamingService { } System.out.println("BFNS took " + DataHelper.formatDuration(System.currentTimeMillis() - start)); System.out.println("Added " + found + " not added " + notfound); - + System.out.println("size() reports " + bns.size()); //bns.dumpDB(); ****/ bns.close(); + ctx.logManager().flush(); + System.out.flush(); /**** if (true) return; diff --git a/core/java/src/net/metanotion/io/block/BlockFile.java b/core/java/src/net/metanotion/io/block/BlockFile.java index 122f30796e..5e1b433995 100644 --- a/core/java/src/net/metanotion/io/block/BlockFile.java +++ b/core/java/src/net/metanotion/io/block/BlockFile.java @@ -32,8 +32,10 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Set; import net.metanotion.io.RAIFile; @@ -431,8 +433,14 @@ public class BlockFile implements Closeable { } /** + * Open a skiplist if it exists. + * Returns null if the skiplist does not exist. + * Empty skiplists are not preserved after close. + * * If the file is writable, this runs an integrity check and repair * on first open. + * + * @return null if not found */ public BSkipList getIndex(String name, Serializer key, Serializer val) throws IOException { // added I2P @@ -454,6 +462,12 @@ public class BlockFile implements Closeable { return bsl; } + /** + * Create and open a new skiplist if it does not exist. + * Throws IOException if it already exists. + * + * @throws IOException if already exists or other errors + */ public BSkipList makeIndex(String name, Serializer key, Serializer val) throws IOException { if(metaIndex.get(name) != null) { throw new IOException("Index already exists"); } int page = allocPage(); @@ -464,15 +478,27 @@ public class BlockFile implements Closeable { return bsl; } + /** + * Delete a skiplist if it exists. + * Must be open. Throws IOException if exists but is closed. + * Broken before 0.9.26. + * + * @throws IOException if it is closed. + */ public void delIndex(String name) throws IOException { - Integer page = (Integer) metaIndex.remove(name); - if (page == null) { return; } - Serializer nb = new IdentityBytes(); - BSkipList bsl = new BSkipList(spanSize, this, page.intValue(), nb, nb, true); + if (metaIndex.get(name) == null) + return; + BSkipList bsl = openIndices.get(name); + if (bsl == null) + throw new IOException("Cannot delete closed skiplist, open it first: " + name); bsl.delete(); + openIndices.remove(name); + metaIndex.remove(name); } /** + * Close a skiplist if it is open. + * * Added I2P */ public void closeIndex(String name) { @@ -482,8 +508,66 @@ public class BlockFile implements Closeable { } /** + * Reformat a skiplist with new Serializers if it exists. + * The skiplist must be closed. + * Throws IOException if the skiplist is open. + * The skiplist will remain closed after completion. + * + * @throws IOException if it is open or on errors + * @since 0.9.26 + */ + public void reformatIndex(String name, Serializer oldKey, Serializer oldVal, + Serializer newKey, Serializer newVal) throws IOException { + if (openIndices.containsKey(name)) + throw new IOException("Cannot reformat open skiplist " + name); + BSkipList old = getIndex(name, oldKey, oldVal); + if (old == null) + return; + long start = System.currentTimeMillis(); + String tmpName = "---tmp---" + name + "---tmp---"; + BSkipList tmp = makeIndex(tmpName, newKey, newVal); + + // It could be much more efficient to do this at the + // SkipSpan layer but that's way too hard. + final int loop = 32; + List<Comparable> keys = new ArrayList<Comparable>(loop); + List<Object> vals = new ArrayList<Object>(loop); + while (true) { + SkipIterator iter = old.iterator(); + for (int i = 0; iter.hasNext() && i < loop; i++) { + keys.add(iter.nextKey()); + vals.add(iter.next()); + } + // save state, as deleting corrupts the iterator + boolean done = !iter.hasNext(); + for (int i = 0; i < keys.size(); i++) { + tmp.put(keys.get(i), vals.get(i)); + } + for (int i = keys.size() - 1; i >= 0; i--) { + old.remove(keys.get(i)); + } + if (done) + break; + keys.clear(); + vals.clear(); + } + + delIndex(name); + closeIndex(name); + closeIndex(tmpName); + Integer page = (Integer) metaIndex.get(tmpName); + metaIndex.put(name, page); + metaIndex.remove(tmpName); + if (log.shouldWarn()) + log.warn("reformatted list: " + name + " in " + + (System.currentTimeMillis() - start) + "ms"); + } + + /** + * Closes all open skiplists and then the blockfile itself. + * * Note (I2P) - * Does NOT close the RAF / RAI. + * Does NOT close the RAF / RAI. */ public void close() throws IOException { // added I2P diff --git a/core/java/src/net/metanotion/io/block/index/BSkipLevels.java b/core/java/src/net/metanotion/io/block/index/BSkipLevels.java index 0a707c9d56..dcf5f38dd0 100644 --- a/core/java/src/net/metanotion/io/block/index/BSkipLevels.java +++ b/core/java/src/net/metanotion/io/block/index/BSkipLevels.java @@ -229,7 +229,7 @@ public class BSkipLevels extends SkipLevels { return; } if (bf.log.shouldLog(Log.DEBUG)) - bf.log.debug("Killing " + this + ' ' + print(), new Exception()); + bf.log.debug("Killing " + this + ' ' + print() /* , new Exception() */ ); isKilled = true; bsl.levelHash.remove(Integer.valueOf(levelPage)); bf.freePage(levelPage); diff --git a/core/java/src/net/metanotion/io/block/index/BSkipList.java b/core/java/src/net/metanotion/io/block/index/BSkipList.java index 3a2d8b599c..71bc805353 100644 --- a/core/java/src/net/metanotion/io/block/index/BSkipList.java +++ b/core/java/src/net/metanotion/io/block/index/BSkipList.java @@ -151,7 +151,6 @@ public class BSkipList extends SkipList implements Closeable { curLevel.killInstance(); curLevel = nextLevel; } - stack.killInstance(); SkipSpan curSpan = first; while(curSpan != null) { -- GitLab