Activating a HashMap to depth 1 e.g. by activating the object containing it to depth 2, makes the HashMap empty, and *prevents it from being filled* when activating it to depth 2. This is with more or less default configuration, so translators are being used; from scratch, activation to depth 1 gives a valid but empty HashMap, whereas activating to depth 2 fills in the elements.
So far, not a really nasty bug. But if the HashMap is stored while in this half-activated state, we lose the contents. And thanks to
COR-1436, this can happen implicitly, as I have discovered over the last few days: The parent object gets activated to depth 2, hence the HashMap is active to depth 1, i.e. it is empty and cannot be refilled except by deactivating and reactivating. Both the parent object and the HashMap stay in memory for a while, and in a later transaction, we remove the parent object from one arraylist and add it to another, and store() the two arraylists. The empty HashMap then gets stored to disk, leaking its contents. :(
I have a test case, apologies for its length:
HashmapProblemsTest.java:
import java.util.*;
import com.db4o.Db4o;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.config.Configuration;
import freenet.client.async.ManifestElement;
import freenet.support.io.NullBucket;
class HashmapProblemsTest { // aka FCPClient
HashmapProblemsTest() {
byIdentifier = new HashMap<String, ClientPutDir>();
listA = new ArrayList<ClientPutDir>();
listB = new ArrayList<ClientPutDir>();
}
private final Map<String, ClientPutDir> byIdentifier;
private final List<ClientPutDir> listA;
private final List<ClientPutDir> listB;
protected void register(String name, ClientPutDir dir, ObjectContainer container) {
container.activate(byIdentifier, 2);
container.activate(listA, 2);
listA.add(dir);
dir.storeTo(container);
container.ext().store(listA, 2);
byIdentifier.put(name, dir);
container.ext().store(byIdentifier, 2);
}
public static void main(String[] args) {
Configuration dbConfig = Db4o.newConfiguration();
dbConfig.freespace().useBTreeSystem();
dbConfig.messageLevel(1);
dbConfig.activationDepth(1);
dbConfig.blockSize(8);
ObjectContainer db = Db4o.openFile(dbConfig, "hashmap.problems.test.db4o");
ObjectSet<HashmapProblemsTest> results = db.query(HashmapProblemsTest.class);
HashmapProblemsTest test = results.hasNext() ? results.next() : null;
ClientPutDir dir = null;
if(test != null) {
System.err.println("Found HashmapProblemsTest");
db.activate(test.byIdentifier, 2);
Iterator<String> dirs = test.byIdentifier.keySet().iterator();
if(dirs.hasNext()) {
String name = dirs.next();
System.err.println("Name of ClientPutDir: "+name);
dir = test.byIdentifier.get(name);
}
}
if(dir == null) {
System.err.println("No ClientPutDir found. Creating one...");
test = new HashmapProblemsTest();
db.store(test);
db.commit();
System.err.println("Creating ClientPutDir...");
dir = new ClientPutDir();
test.register("test14", dir, db);
db.store(dir);
db.deactivate(dir, 1);
db.commit();
} else {
db.activate(dir, 1);
dir.part1(db); // demonstrate the bug
// now demonstrate why the bug really kills you
test.upgrade(dir, db);
// now lets see what's happened to us...
dir.part2(db);
}
}
public void upgrade(ClientPutDir dir, ObjectContainer db) {
// move dir from list A to list B, e.g. because it has completed some work
listA.remove(dir);
listB.add(dir);
db.store(listA);
db.store(listB);
db.commit();
}
}
ClientPutDir.java:
import java.util.*;
import com.db4o.Db4o;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.config.Configuration;
import freenet.client.async.ManifestElement;
import freenet.support.io.NullBucket;
class ClientPutDir {
protected HashMap<String, Object> map;
private boolean something;
ClientPutDir() {
map = new HashMap<String, Object>();
map.put("name", new ManifestElement("name", "fullname", new NullBucket(), "text/plain", -1));
}
void storeTo(ObjectContainer container) {
container.store(map);
container.store(this);
}
void part1(ObjectContainer container) {
// First demonstrate the basic bug
container.activate(this, 2);
container.activate(map, 2);
System.err.println("Map size after accidentally activating to depth 1 and then deliberately activating to depth 2: "+map.size());
}
void part2(ObjectContainer container) {
container.activate(map, 2);
System.err.println("Map size after activating: "+map.size());
container.deactivate(map, 1);
container.activate(map, 2);
System.err.println("Map size after deactivating and activating (real map size): "+map.size());
}
}
In db4o 7.9 everything works correctly as expected.