You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by th...@apache.org on 2013/04/10 10:05:17 UTC

svn commit: r1466363 - in /jackrabbit/oak/trunk/oak-mongomk/src: main/java/org/apache/jackrabbit/mongomk/prototype/ test/java/org/apache/jackrabbit/mongomk/prototype/

Author: thomasm
Date: Wed Apr 10 08:05:17 2013
New Revision: 1466363

URL: http://svn.apache.org/r1466363
Log:
OAK-731 The cluster id should be automatically generated (WIP)

Added:
    jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/ClusterNodeInfo.java
Modified:
    jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/DocumentStore.java
    jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MemoryDocumentStore.java
    jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoDocumentStore.java
    jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoMK.java
    jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/prototype/ClusterTest.java

Added: jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/ClusterNodeInfo.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/ClusterNodeInfo.java?rev=1466363&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/ClusterNodeInfo.java (added)
+++ jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/ClusterNodeInfo.java Wed Apr 10 08:05:17 2013
@@ -0,0 +1,291 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.jackrabbit.mongomk.prototype;
+
+import java.lang.management.ManagementFactory;
+import java.net.NetworkInterface;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.jackrabbit.mk.api.MicroKernelException;
+import org.apache.jackrabbit.mk.util.StringUtils;
+import org.apache.jackrabbit.mongomk.prototype.DocumentStore.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Information about a cluster node.
+ */
+public class ClusterNodeInfo {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MongoDocumentStore.class);
+
+    /**
+     * The prefix for random (non-reusable) keys.
+     */
+    private static final String RANDOM_PREFIX = "random:";
+    
+    /**
+     * The cluster node id.
+     */
+    private static final String ID_KEY = "_id";
+    
+    /**
+     * The machine id.
+     */
+    private static final String MACHINE_ID_KEY = "machine";
+    
+    /**
+     * The unique instance id within this machine (the current working directory
+     * if not set).
+     */
+    private static final String INSTANCE_ID_KEY = "instance";
+
+    /**
+     * The end of the lease.
+     */
+    private static final String LEASE_END_KEY = "leaseEnd";
+
+    /**
+     * Additional info, such as the process id, for support.
+     */
+    private static final String INFO_KEY = "info";
+    
+    /**
+     * The unique machine id (the MAC address if available).
+     */
+    private static final String MACHINE_ID = getMachineId();
+
+    /**
+     * The process id (if available).
+     */
+    private static final long PROCESS_ID = getProcessId();
+    
+    /**
+     * The current working directory.
+     */
+    private static final String WORKING_DIR = System.getProperty("user.dir", "");
+    
+    /**
+     * The initial number of milliseconds for a lease (1 minute). This value is
+     * only used until the first renewal.
+     */
+    private static final long LEASE_INITIAL = 1000 * 60;
+    
+    /**
+     * The assigned cluster id.
+     */
+    private final int id;
+    
+    /**
+     * The machine id.
+     */
+    private final String machineId;
+    
+    /**
+     * The instance id.
+     */
+    private final String instanceId;
+    
+    /**
+     * The document store that is used to renew the lease.
+     */
+    private final DocumentStore store;
+    
+    /**
+     * The time (in milliseconds UTC) where this instance was started.
+     */
+    private final long startTime;
+
+    /**
+     * A unique id.
+     */
+    private final String uuid = UUID.randomUUID().toString();
+    
+    /**
+     * The time (in milliseconds UTC) where the lease of this instance ends.
+     */
+    private long leaseEndTime;
+    
+    ClusterNodeInfo(int id, DocumentStore store, String machineId, String instanceId) {
+        this.id = id;
+        this.startTime = System.currentTimeMillis();
+        this.leaseEndTime = startTime;
+        this.store = store;
+        this.machineId = machineId;
+        this.instanceId = instanceId;
+    }
+    
+    public int getId() {
+        return id;
+    }
+
+    /**
+     * Create a cluster node info instance for the store.
+     * 
+     * @param store the document store (for the lease)
+     * @param machineId the machine id (null for MAC address)
+     * @param instanceId the instance id (null for current working directory)
+     * @return the cluster node info
+     */
+    public static ClusterNodeInfo getInstance(DocumentStore store, String machineId, String instanceId) {
+        if (machineId == null) {
+            machineId = MACHINE_ID;
+        }
+        if (instanceId == null) {
+            instanceId = WORKING_DIR;
+        }
+        for (int i = 0; i < 10; i++) {
+            ClusterNodeInfo clusterNode = createInstance(store, machineId, instanceId);
+            UpdateOp update = new UpdateOp(null, "" + clusterNode.id, true);
+            update.set(ID_KEY, "" + clusterNode.id);
+            update.set(MACHINE_ID_KEY, clusterNode.machineId);
+            update.set(INSTANCE_ID_KEY, clusterNode.instanceId);
+            update.set(LEASE_END_KEY, System.currentTimeMillis() + LEASE_INITIAL);
+            update.set(INFO_KEY, clusterNode.toString());
+            boolean success = store.create(Collection.CLUSTER_NODES, Collections.singletonList(update));
+            if (success) {
+                return clusterNode;
+            }
+        }
+        throw new MicroKernelException("Could not get cluster node info");
+    }
+    
+    private static ClusterNodeInfo createInstance(DocumentStore store, String machineId, String instanceId) {
+        long now = System.currentTimeMillis();
+        // keys between "0" and "a" includes all possible numbers
+        List<Map<String, Object>> list = store.query(Collection.CLUSTER_NODES, 
+                "0", "a", Integer.MAX_VALUE);
+        int clusterNodeId = 0;
+        int maxId = 0;
+        for (Map<String, Object> doc : list) {
+            String key = "" + doc.get(ID_KEY);
+            int id;
+            try {
+                id = Integer.parseInt(key);
+            } catch (Exception e) {
+                // not an integer - ignore
+                continue;
+            }
+            maxId = Math.max(maxId, id);
+            Long leaseEnd = (Long) doc.get(LEASE_END_KEY);
+            if (leaseEnd != null && leaseEnd > now) {
+                continue;
+            }
+            String mId = "" + doc.get(MACHINE_ID_KEY);
+            String iId = "" + doc.get(INSTANCE_ID_KEY);
+            if (machineId.startsWith(RANDOM_PREFIX)) {
+                // remove expired entries with random keys
+                store.remove(Collection.CLUSTER_NODES, key);
+                continue;
+            }
+            if (!mId.equals(machineId) || 
+                    !iId.equals(instanceId)) {
+                // a different machine or instance
+                continue;
+            }
+            // remove expired matching entries
+            store.remove(Collection.CLUSTER_NODES, key);
+            if (clusterNodeId == 0 || id < clusterNodeId) {
+                // if there are multiple, use the smallest value
+                clusterNodeId = id;
+            }
+        }
+        if (clusterNodeId == 0) {
+            clusterNodeId = maxId + 1;
+        }
+        return new ClusterNodeInfo(clusterNodeId, store, machineId, instanceId);
+    }
+    
+    /**
+     * Renew the cluster id lease. This method needs to be called once in a while,
+     * to ensure the same cluster id is not re-used by a different instance.
+     * 
+     * @param nextCheckMillis the millisecond offset
+     */
+    public void renewLease(long nextCheckMillis) {
+        long now = System.currentTimeMillis();
+        if (now + nextCheckMillis + nextCheckMillis < leaseEndTime) {
+            return;
+        }
+        UpdateOp update = new UpdateOp(null, "" + id, true);
+        leaseEndTime = now + nextCheckMillis;
+        update.set(LEASE_END_KEY, leaseEndTime);
+        store.createOrUpdate(Collection.CLUSTER_NODES, update);
+    }
+    
+    public void dispose() {
+        UpdateOp update = new UpdateOp(null, "" + id, true);
+        update.set(LEASE_END_KEY, null);
+        store.createOrUpdate(Collection.CLUSTER_NODES, update);
+    }
+    
+    public String toString() {
+        return "id: " + id + ",\n" +
+                "startTime: " + startTime + ",\n" +
+                "machineId: " + machineId + ",\n" +
+                "instanceId: " + instanceId + ",\n" +
+                "pid: " + PROCESS_ID + ",\n" +
+                "uuid: " + uuid +"\n";
+    }
+        
+    private static long getProcessId() {
+        try {
+            String name = ManagementFactory.getRuntimeMXBean().getName();
+            return Long.parseLong(name.substring(0, name.indexOf('@')));
+        } catch (Exception e) {
+            LOG.warn("Could not get process id", e);
+            return 0;
+        }
+    }
+    
+    /**
+     * Calculate the unique machine id. This is the lowest MAC address if
+     * available. As an alternative, a randomly generated UUID is used.
+     * 
+     * @return the unique id
+     */
+    private static String getMachineId() {
+        try {
+            ArrayList<String> list = new ArrayList<String>();
+            Enumeration<NetworkInterface> e = NetworkInterface
+                    .getNetworkInterfaces();
+            while (e.hasMoreElements()) {
+                NetworkInterface ni = e.nextElement();
+                byte[] mac = ni.getHardwareAddress();
+                if (mac != null) {
+                    String x = StringUtils.convertBytesToHex(mac);
+                    list.add(x);
+                }
+            }
+            if (list.size() > 0) {
+                // use the lowest value, such that if the order changes,
+                // the same one is used
+                Collections.sort(list);
+                return "mac:" + list.get(0);
+            }
+        } catch (Exception e) {
+            LOG.error("Error calculating the machine id", e);
+        }
+        return RANDOM_PREFIX + UUID.randomUUID().toString();
+    }
+
+}

Modified: jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/DocumentStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/DocumentStore.java?rev=1466363&r1=1466362&r2=1466363&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/DocumentStore.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/DocumentStore.java Wed Apr 10 08:05:17 2013
@@ -32,7 +32,38 @@ public interface DocumentStore {
     /**
      * The list of collections.
      */
-    enum Collection { NODES }
+    enum Collection { 
+        
+        /**
+         * The 'nodes' collection. It contains all the node data, with one document
+         * per node, and the path as the primary key. Each document possibly
+         * contains multiple revisions.
+         * <p>
+         * Key: the path, value: the node data (possibly multiple revisions)
+         * <p>
+         * Old revisions are removed after some time, either by the process that
+         * removed or updated the node, lazily when reading, or in a background
+         * process.
+         */
+        NODES("nodes"), 
+        
+        /**
+         * The 'clusterNodes' collection contains the list of currently running
+         * cluster nodes. The key is the clusterNodeId (0, 1, 2,...).
+         */
+        CLUSTER_NODES("clusterNodes");
+            
+        final String name;
+        
+        Collection(String name) {
+            this.name = name;
+        }
+        
+        public String toString() {
+            return name;
+        }
+        
+    }
 
     /**
      * Get a document.
@@ -62,6 +93,16 @@ public interface DocumentStore {
     @CheckForNull
     Map<String, Object> find(Collection collection, String key, int maxCacheAge);
 
+    /**
+     * Get a list of documents where the key is greater than a start value and
+     * less than an end value.
+     * 
+     * @param collection the collection
+     * @param fromKey the start value (excluding)
+     * @param toKey the end value (excluding)
+     * @param limit the maximum number of entries to return
+     * @return the list (possibly empty)
+     */
     @Nonnull
     List<Map<String, Object>> query(Collection collection, String fromKey, String toKey, int limit);
     
@@ -95,8 +136,14 @@ public interface DocumentStore {
     Map<String, Object> createOrUpdate(Collection collection, UpdateOp update)
             throws MicroKernelException;
 
+    /**
+     * Invalidate the document cache.
+     */
     void invalidateCache();
 
+    /**
+     * Dispose this instance.
+     */
     void dispose();
 
 }

Modified: jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MemoryDocumentStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MemoryDocumentStore.java?rev=1466363&r1=1466362&r2=1466363&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MemoryDocumentStore.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MemoryDocumentStore.java Wed Apr 10 08:05:17 2013
@@ -35,18 +35,16 @@ import org.apache.jackrabbit.mongomk.pro
 public class MemoryDocumentStore implements DocumentStore {
 
     /**
-     * The 'nodes' collection. It contains all the node data, with one document
-     * per node, and the path as the primary key. Each document possibly
-     * contains multiple revisions.
-     * <p>
-     * Key: the path, value: the node data (possibly multiple revisions)
-     * <p>
-     * Old revisions are removed after some time, either by the process that
-     * removed or updated the node, lazily when reading, or in a background
-     * process.
+     * The 'nodes' collection.
      */
     private ConcurrentSkipListMap<String, Map<String, Object>> nodes =
             new ConcurrentSkipListMap<String, Map<String, Object>>();
+    
+    /**
+     * The 'clusterNodes' collection.
+     */
+    private ConcurrentSkipListMap<String, Map<String, Object>> clusterNodes =
+            new ConcurrentSkipListMap<String, Map<String, Object>>();
 
     public Map<String, Object> find(Collection collection, String key, int maxCacheAge) {
         return find(collection, key);
@@ -97,6 +95,8 @@ public class MemoryDocumentStore impleme
         switch (collection) {
         case NODES:
             return nodes;
+        case CLUSTER_NODES:
+            return clusterNodes;
         default:
             throw new IllegalArgumentException(collection.name());
         }
@@ -139,6 +139,12 @@ public class MemoryDocumentStore impleme
         return oldNode;
     }
     
+    /**
+     * Apply the changes to the in-memory map.
+     * 
+     * @param target the target map
+     * @param update the changes to apply
+     */
     public static void applyChanges(Map<String, Object> target, UpdateOp update) {
         for (Entry<String, Operation> e : update.changes.entrySet()) {
             String k = e.getKey();

Modified: jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoDocumentStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoDocumentStore.java?rev=1466363&r1=1466362&r2=1466363&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoDocumentStore.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoDocumentStore.java Wed Apr 10 08:05:17 2013
@@ -52,14 +52,21 @@ public class MongoDocumentStore implemen
 
     private static final boolean LOG_TIME = false;
 
-    private final DBCollection nodesCollection;
+    private final DBCollection nodes;
+    private final DBCollection clusterNodes;
     
-    private long time;
+    /**
+     * The sum of all milliseconds this class waited for MongoDB.
+     */
+    private long timeSum;
     
     private final Cache<String, CachedDocument> cache;
 
     public MongoDocumentStore(DB db) {
-        nodesCollection = db.getCollection(Collection.NODES.toString());
+        nodes = db.getCollection(
+                Collection.NODES.toString());
+        clusterNodes = db.getCollection(
+                Collection.CLUSTER_NODES.toString());
         
         // the _id field is the primary key, so we don't need to define it
         // the following code is just a template in case we need more indexes
@@ -82,7 +89,7 @@ public class MongoDocumentStore implemen
     
     private void end(long start) {
         if (LOG_TIME) {
-            time += System.currentTimeMillis() - start;
+            timeSum += System.currentTimeMillis() - start;
         }
     }
     
@@ -98,19 +105,19 @@ public class MongoDocumentStore implemen
         cache.invalidateAll();
     }
 
-    public Map<String, Object> find(Collection collection, String path) {
-        return find(collection, path, Integer.MAX_VALUE);
+    public Map<String, Object> find(Collection collection, String key) {
+        return find(collection, key, Integer.MAX_VALUE);
     }
     
     @Override
-    public Map<String, Object> find(final Collection collection, final String path, int maxCacheAge) {
+    public Map<String, Object> find(final Collection collection, final String key, int maxCacheAge) {
         try {
             CachedDocument doc;
             while (true) {
-                doc = cache.get(path, new Callable<CachedDocument>() {
+                doc = cache.get(key, new Callable<CachedDocument>() {
                     @Override
                     public CachedDocument call() throws Exception {
-                        Map<String, Object> map = findUncached(collection, path);
+                        Map<String, Object> map = findUncached(collection, key);
                         return new CachedDocument(map);
                     }
                 });
@@ -121,19 +128,19 @@ public class MongoDocumentStore implemen
                     break;
                 }
                 // too old: invalidate, try again
-                cache.invalidate(path);
+                cache.invalidate(key);
             }
             return doc.value;
         } catch (ExecutionException e) {
-            throw new IllegalStateException("Failed to load node " + path, e);
+            throw new IllegalStateException("Failed to load document with " + key, e);
         }
     }
     
-    public Map<String, Object> findUncached(Collection collection, String path) {
+    Map<String, Object> findUncached(Collection collection, String key) {
         DBCollection dbCollection = getDBCollection(collection);
         long start = start();
         try {
-            DBObject doc = dbCollection.findOne(getByPathQuery(path));
+            DBObject doc = dbCollection.findOne(getByKeyQuery(key));
             if (doc == null) {
                 return null;
             }
@@ -171,13 +178,13 @@ public class MongoDocumentStore implemen
     }
 
     @Override
-    public void remove(Collection collection, String path) {
-        log("remove", path);        
+    public void remove(Collection collection, String key) {
+        log("remove", key);        
         DBCollection dbCollection = getDBCollection(collection);
         long start = start();
         try {
-            cache.invalidate(path);
-            WriteResult writeResult = dbCollection.remove(getByPathQuery(path), WriteConcern.SAFE);
+            cache.invalidate(key);
+            WriteResult writeResult = dbCollection.remove(getByKeyQuery(key), WriteConcern.SAFE);
             if (writeResult.getError() != null) {
                 throw new MicroKernelException("Remove failed: " + writeResult.getError());
             }
@@ -230,7 +237,7 @@ public class MongoDocumentStore implemen
             }
         }
 
-        DBObject query = getByPathQuery(updateOp.key);
+        DBObject query = getByKeyQuery(updateOp.key);
         BasicDBObject update = new BasicDBObject();
         if (!setUpdates.isEmpty()) {
             update.append("$set", setUpdates);
@@ -348,22 +355,24 @@ public class MongoDocumentStore implemen
     private DBCollection getDBCollection(Collection collection) {
         switch (collection) {
             case NODES:
-                return nodesCollection;
+                return nodes;
+            case CLUSTER_NODES:
+                return clusterNodes;
             default:
                 throw new IllegalArgumentException(collection.name());
         }
     }
 
-    private static DBObject getByPathQuery(String path) {
-        return QueryBuilder.start(UpdateOp.ID).is(path).get();
+    private static DBObject getByKeyQuery(String key) {
+        return QueryBuilder.start(UpdateOp.ID).is(key).get();
     }
     
     @Override
     public void dispose() {
         if (LOG.isDebugEnabled()) {
-            LOG.debug("MongoDB time: " + time);
+            LOG.debug("MongoDB time: " + timeSum);
         }
-        nodesCollection.getDB().getMongo().close();
+        nodes.getDB().getMongo().close();
     }
     
     private static void log(String message, Object... args) {

Modified: jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoMK.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoMK.java?rev=1466363&r1=1466362&r2=1466363&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoMK.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/main/java/org/apache/jackrabbit/mongomk/prototype/MongoMK.java Wed Apr 10 08:05:17 2013
@@ -109,7 +109,7 @@ public class MongoMK implements MicroKer
     /**
      * The unique cluster id, similar to the unique machine id in MongoDB.
      */
-    private final int clusterId;
+    private int clusterId;
 
     /**
      * The node cache.
@@ -188,6 +188,7 @@ public class MongoMK implements MicroKer
         this.store = builder.getDocumentStore();
         this.blobStore = builder.getBlobStore();
         this.clusterId = builder.getClusterId();
+        clusterId = Integer.getInteger("oak.mongoMK.clusterId", clusterId);
         this.asyncDelay = builder.getAsyncDelay();
 
         //TODO Use size based weigher

Modified: jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/prototype/ClusterTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/prototype/ClusterTest.java?rev=1466363&r1=1466362&r2=1466363&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/prototype/ClusterTest.java (original)
+++ jackrabbit/oak/trunk/oak-mongomk/src/test/java/org/apache/jackrabbit/mongomk/prototype/ClusterTest.java Wed Apr 10 08:05:17 2013
@@ -36,6 +36,56 @@ public class ClusterTest {
     
     private MemoryDocumentStore ds;
     private MemoryBlobStore bs;
+
+    @Test
+    public void clusterNodeInfoLease() throws InterruptedException {
+        MemoryDocumentStore store = new MemoryDocumentStore();
+        ClusterNodeInfo c1, c2;
+        c1 = ClusterNodeInfo.getInstance(store, "m1", null);
+        assertEquals(1, c1.getId());
+        // this will quickly expire
+        c1.renewLease(1);
+        Thread.sleep(10);
+        c2 = ClusterNodeInfo.getInstance(store, "m1", null);
+        assertEquals(1, c2.getId());
+    }
+    
+    @Test
+    public void clusterNodeInfo() {
+        MemoryDocumentStore store = new MemoryDocumentStore();
+        ClusterNodeInfo c1, c2, c3, c4;
+        
+        c1 = ClusterNodeInfo.getInstance(store, "m1", null);
+        assertEquals(1, c1.getId());
+        c1.dispose();
+        
+        // get the same id
+        c1 = ClusterNodeInfo.getInstance(store, "m1", null);
+        assertEquals(1, c1.getId());
+        c1.dispose();
+        
+        // now try to add another one:
+        // must get a new id
+        c2 = ClusterNodeInfo.getInstance(store, "m2", null);
+        assertEquals(2, c2.getId());
+        
+        // a different machine
+        c3 = ClusterNodeInfo.getInstance(store, "m3", "/a");
+        assertEquals(3, c3.getId());
+        
+        c2.dispose();
+        c3.dispose();
+        
+        c3 = ClusterNodeInfo.getInstance(store, "m3", "/a");
+        assertEquals(3, c3.getId());
+
+        c3.dispose();
+        
+        c4 = ClusterNodeInfo.getInstance(store, "m3", "/b");
+        assertEquals(4, c4.getId());
+
+        c1.dispose();
+    }
     
     @Test
     public void conflict() {