You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by dp...@apache.org on 2008/10/27 15:31:01 UTC

svn commit: r708199 - in /jackrabbit/branches/1.5/jackrabbit-core/src: main/java/org/apache/jackrabbit/core/cluster/ test/java/org/apache/jackrabbit/core/cluster/ test/java/org/apache/jackrabbit/core/journal/

Author: dpfister
Date: Mon Oct 27 07:31:01 2008
New Revision: 708199

URL: http://svn.apache.org/viewvc?rev=708199&view=rev
Log:
JCR-1553 - ClusterNode not properly shutdown when repository has shutdown

Added:
    jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java   (with props)
Modified:
    jackrabbit/branches/1.5/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterNode.java
    jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterRecordTest.java
    jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/MemoryJournal.java

Modified: jackrabbit/branches/1.5/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterNode.java
URL: http://svn.apache.org/viewvc/jackrabbit/branches/1.5/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterNode.java?rev=708199&r1=708198&r2=708199&view=diff
==============================================================================
--- jackrabbit/branches/1.5/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterNode.java (original)
+++ jackrabbit/branches/1.5/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterNode.java Mon Oct 27 07:31:01 2008
@@ -35,6 +35,7 @@
 import org.apache.jackrabbit.core.state.ChangeLog;
 import org.apache.jackrabbit.uuid.UUID;
 
+import EDU.oswego.cs.dl.util.concurrent.Latch;
 import EDU.oswego.cs.dl.util.concurrent.Mutex;
 
 import javax.jcr.RepositoryException;
@@ -69,6 +70,11 @@
     private static final String PRODUCER_ID = "JR";
 
     /**
+     * Default stop delay.
+     */
+    private static final long DEFAULT_STOP_DELAY = 5000;
+
+    /**
      * Status constant.
      */
     private static final int NONE = 0;
@@ -104,16 +110,31 @@
     private long syncDelay;
 
     /**
+     * Stop delay, in milliseconds.
+     */
+    private long stopDelay;
+
+    /**
      * Journal used.
      */
     private Journal journal;
 
     /**
+     * Synchronization thread.
+     */
+    private Thread syncThread;
+
+    /**
      * Mutex used when syncing.
      */
     private final Mutex syncLock = new Mutex();
 
     /**
+     * Latch used to communicate a stop request to the synchronization thread.
+     */
+    private final Latch stopLatch = new Latch();
+
+    /**
      * Status flag, one of {@link #NONE}, {@link #STARTED} or {@link #STOPPED}.
      */
     private int status;
@@ -209,6 +230,26 @@
     }
 
     /**
+     * Set the stop delay, i.e. number of millseconds to wait for the
+     * synchronization thread to stop.
+     *
+     * @param stopDelay stop delay in milliseconds
+     */
+    public void setStopDelay(long stopDelay) {
+        this.stopDelay = stopDelay;
+    }
+
+    /**
+     * Return the stop delay.
+     *
+     * @return stop delay
+     * @see #setStopDelay(long)
+     */
+    public long getStopDelay() {
+        return stopDelay;
+    }
+
+    /**
      * Starts this cluster node.
      *
      * @throws ClusterException if an error occurs
@@ -220,6 +261,7 @@
             Thread t = new Thread(this, "ClusterNode-" + clusterNodeId);
             t.setDaemon(true);
             t.start();
+            syncThread = t;
 
             status = STARTED;
         }
@@ -230,14 +272,13 @@
      */
     public void run() {
         for (;;) {
-            synchronized (this) {
-                try {
-                    wait(syncDelay);
-                } catch (InterruptedException e) {}
-
-                if (status == STOPPED) {
-                    return;
+            try {
+                if (stopLatch.attempt(syncDelay)) {
+                    break;
                 }
+            } catch (InterruptedException e) {
+                String msg = "Interrupted while waiting for stop latch.";
+                log.warn(msg);
             }
             try {
                 sync();
@@ -284,13 +325,24 @@
         if (status != STOPPED) {
             status = STOPPED;
 
+            stopLatch.release();
+
+            // Give synchronization thread some time to finish properly before
+            // closing down the journal (see JCR-1553)
+            if (syncThread != null) {
+                try {
+                    syncThread.join(stopDelay);
+                } catch (InterruptedException e) {
+                    String msg = "Interrupted while joining synchronization thread.";
+                    log.warn(msg);
+                }
+            }
             if (journal != null) {
                 journal.close();
             }
             if (instanceRevision != null) {
                 instanceRevision.close();
             }
-            notifyAll();
         }
     }
 

Modified: jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterRecordTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterRecordTest.java?rev=708199&r1=708198&r2=708199&view=diff
==============================================================================
--- jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterRecordTest.java (original)
+++ jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterRecordTest.java Mon Oct 27 07:31:01 2008
@@ -17,16 +17,9 @@
 package org.apache.jackrabbit.core.cluster;
 
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
 import java.util.Properties;
 
-import javax.jcr.Session;
-import javax.jcr.observation.Event;
-
 import org.apache.jackrabbit.core.NodeId;
-import org.apache.jackrabbit.core.PropertyId;
-import org.apache.jackrabbit.core.RepositoryImpl;
 import org.apache.jackrabbit.core.cluster.SimpleEventListener.LockEvent;
 import org.apache.jackrabbit.core.cluster.SimpleEventListener.NamespaceEvent;
 import org.apache.jackrabbit.core.cluster.SimpleEventListener.NodeTypeEvent;
@@ -37,17 +30,9 @@
 import org.apache.jackrabbit.core.config.JournalConfig;
 import org.apache.jackrabbit.core.journal.MemoryJournal;
 import org.apache.jackrabbit.core.nodetype.NodeTypeDef;
-import org.apache.jackrabbit.core.observation.EventState;
-import org.apache.jackrabbit.core.state.ChangeLog;
-import org.apache.jackrabbit.core.state.NodeState;
-import org.apache.jackrabbit.core.state.PropertyState;
 import org.apache.jackrabbit.spi.Name;
-import org.apache.jackrabbit.spi.NameFactory;
-import org.apache.jackrabbit.spi.Path;
-import org.apache.jackrabbit.spi.PathFactory;
 import org.apache.jackrabbit.spi.commons.name.NameConstants;
 import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
-import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
 import org.apache.jackrabbit.test.JUnitTest;
 import org.apache.jackrabbit.uuid.UUID;
 
@@ -63,34 +48,14 @@
     private static final String DEFAULT_WORKSPACE = "default";
 
     /**
-     * Default user.
-     */
-    private static final String DEFAULT_USER = "admin";
-
-    /**
-     * Root node id.
-     */
-    private static final NodeId ROOT_NODE_ID = RepositoryImpl.ROOT_NODE_ID;
-
-    /**
      * Default sync delay: 5 seconds.
      */
     private static final long SYNC_DELAY = 5000;
 
     /**
-     * Default session, used for event state creation.
+     * Update event factory.
      */
-    private final Session session = new ClusterSession(DEFAULT_USER);
-
-    /**
-     * Name factory.
-     */
-    private NameFactory nameFactory = NameFactoryImpl.getInstance();
-
-    /**
-     * Path factory.
-     */
-    private PathFactory pathFactory = PathFactoryImpl.getInstance();
+    private final UpdateEventFactory factory = UpdateEventFactory.getInstance();
 
     /**
      * Records shared among multiple memory journals.
@@ -137,26 +102,7 @@
      * @throws Exception
      */
     public void testUpdateOperation() throws Exception {
-        NodeState n1 = createNodeState();
-        NodeState n2 = createNodeState();
-        NodeState n3 = createNodeState();
-        PropertyState p1 = createPropertyState(n1.getNodeId(), "{}a");
-        PropertyState p2 = createPropertyState(n2.getNodeId(), "{}b");
-
-        ChangeLog changes = new ChangeLog();
-        changes.added(n1);
-        changes.added(p1);
-        changes.deleted(p2);
-        changes.modified(n2);
-        changes.deleted(n3);
-
-        List events = new ArrayList();
-        events.add(createEventState(n1, Event.NODE_ADDED, "{}n1"));
-        events.add(createEventState(p1, n1, Event.PROPERTY_ADDED));
-        events.add(createEventState(p2, n2, Event.PROPERTY_REMOVED));
-        events.add(createEventState(n3, Event.NODE_REMOVED, "{}n3"));
-
-        UpdateEvent update = new UpdateEvent(changes, events);
+        UpdateEvent update = factory.createUpdateOperation();
 
         UpdateEventChannel channel = master.createUpdateChannel(DEFAULT_WORKSPACE);
         channel.updateCreated(update);
@@ -328,88 +274,4 @@
         }
         return clusterNode;
     }
-
-    /**
-     * Create a node state.
-     *
-     * @return node state
-     */
-    private NodeState createNodeState() {
-        Name ntName = nameFactory.create("{}testnt");
-        NodeState n = new NodeState(
-                new NodeId(UUID.randomUUID()), ntName,
-                ROOT_NODE_ID, NodeState.STATUS_EXISTING, false);
-        n.setMixinTypeNames(Collections.EMPTY_SET);
-        return n;
-    }
-
-    /**
-     * Create a property state.
-     *
-     * @param parentId parent node id
-     * @param name property name
-     */
-    private PropertyState createPropertyState(NodeId parentId, String name) {
-        Name propName = nameFactory.create(name);
-        return new PropertyState(
-                new PropertyId(parentId, propName),
-                NodeState.STATUS_EXISTING, false);
-    }
-
-    /**
-     * Create an event state for an operation on a node.
-     *
-     * @param n node state
-     * @param type <code>Event.NODE_ADDED</code> or <code>Event.NODE_REMOVED</code>
-     * @param name node name
-     * @return event state
-     */
-    private EventState createEventState(NodeState n, int type, String name) {
-        Path.Element relPath = pathFactory.createElement(nameFactory.create(name));
-
-        switch (type) {
-        case Event.NODE_ADDED:
-            return EventState.childNodeAdded(
-                    n.getParentId(), pathFactory.getRootPath(),
-                    n.getNodeId(), relPath, n.getNodeTypeName(),
-                    n.getMixinTypeNames(), session);
-        case Event.NODE_REMOVED:
-            return EventState.childNodeRemoved(
-                    n.getParentId(), pathFactory.getRootPath(),
-                    n.getNodeId(), relPath, n.getNodeTypeName(),
-                    n.getMixinTypeNames(), session);
-        }
-        return null;
-    }
-
-    /**
-     * Create an event state for a property operation.
-     *
-     * @param n node state
-     * @param type <code>Event.NODE_ADDED</code> or <code>Event.NODE_REMOVED</code>
-     * @param name property name
-     * @return event state
-     */
-    private EventState createEventState(PropertyState p, NodeState parent, int type) {
-        Path.Element relPath = pathFactory.createElement(p.getName());
-
-        switch (type) {
-        case Event.PROPERTY_ADDED:
-            return EventState.propertyAdded(
-                    p.getParentId(), pathFactory.getRootPath(), relPath,
-                    parent.getNodeTypeName(), parent.getMixinTypeNames(),
-                    session);
-        case Event.PROPERTY_CHANGED:
-            return EventState.propertyChanged(
-                    p.getParentId(), pathFactory.getRootPath(), relPath,
-                    parent.getNodeTypeName(), parent.getMixinTypeNames(),
-                    session);
-        case Event.PROPERTY_REMOVED:
-            return EventState.propertyRemoved(
-                    p.getParentId(), pathFactory.getRootPath(), relPath,
-                    parent.getNodeTypeName(), parent.getMixinTypeNames(),
-                    session);
-        }
-        return null;
-    }
 }

Added: jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java
URL: http://svn.apache.org/viewvc/jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java?rev=708199&view=auto
==============================================================================
--- jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java (added)
+++ jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java Mon Oct 27 07:31:01 2008
@@ -0,0 +1,206 @@
+/*
+ * 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.core.cluster;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+
+import org.apache.jackrabbit.core.NodeId;
+import org.apache.jackrabbit.core.PropertyId;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.cluster.SimpleEventListener.UpdateEvent;
+import org.apache.jackrabbit.core.observation.EventState;
+import org.apache.jackrabbit.core.state.ChangeLog;
+import org.apache.jackrabbit.core.state.NodeState;
+import org.apache.jackrabbit.core.state.PropertyState;
+import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.spi.NameFactory;
+import org.apache.jackrabbit.spi.Path;
+import org.apache.jackrabbit.spi.PathFactory;
+import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
+import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
+import org.apache.jackrabbit.uuid.UUID;
+
+/**
+ * Simplistic factory that produces update events, consisting of node identifiers,
+ * property identifiers and event states.
+ */
+public class UpdateEventFactory {
+
+    /**
+     * Root node id.
+     */
+    private static final NodeId ROOT_NODE_ID = RepositoryImpl.ROOT_NODE_ID;
+
+    /**
+     * Default user.
+     */
+    private static final String DEFAULT_USER = "admin";
+
+    /**
+     * Instance of this class.
+     */
+    private static final UpdateEventFactory INSTANCE = new UpdateEventFactory();
+
+    /**
+     * Default session, used for event state creation.
+     */
+    private final Session session = new ClusterSession(DEFAULT_USER);
+
+    /**
+     * Name factory.
+     */
+    private NameFactory nameFactory = NameFactoryImpl.getInstance();
+
+    /**
+     * Path factory.
+     */
+    private PathFactory pathFactory = PathFactoryImpl.getInstance();
+
+    /**
+     * Create a new instance of this class. Private as there is only one
+     * instance acting as singleton.
+     */
+    private UpdateEventFactory() {
+    }
+
+    /**
+     * Return the instance of this class.
+     *
+     * @return factory
+     */
+    public static UpdateEventFactory getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * Create an update operation.
+     *
+     * @return update operation
+     */
+    public UpdateEvent createUpdateOperation() {
+        NodeState n1 = createNodeState();
+        NodeState n2 = createNodeState();
+        NodeState n3 = createNodeState();
+        PropertyState p1 = createPropertyState(n1.getNodeId(), "{}a");
+        PropertyState p2 = createPropertyState(n2.getNodeId(), "{}b");
+
+        ChangeLog changes = new ChangeLog();
+        changes.added(n1);
+        changes.added(p1);
+        changes.deleted(p2);
+        changes.modified(n2);
+        changes.deleted(n3);
+
+        List events = new ArrayList();
+        events.add(createEventState(n1, Event.NODE_ADDED, "{}n1"));
+        events.add(createEventState(p1, n1, Event.PROPERTY_ADDED));
+        events.add(createEventState(p2, n2, Event.PROPERTY_REMOVED));
+        events.add(createEventState(n3, Event.NODE_REMOVED, "{}n3"));
+
+        return new UpdateEvent(changes, events);
+    }
+
+
+    /**
+     * Create a node state.
+     *
+     * @return node state
+     */
+    protected NodeState createNodeState() {
+        Name ntName = nameFactory.create("{}testnt");
+        NodeState n = new NodeState(
+                new NodeId(UUID.randomUUID()), ntName,
+                ROOT_NODE_ID, NodeState.STATUS_EXISTING, false);
+        n.setMixinTypeNames(Collections.EMPTY_SET);
+        return n;
+    }
+
+    /**
+     * Create a property state.
+     *
+     * @param parentId parent node id
+     * @param name property name
+     */
+    protected PropertyState createPropertyState(NodeId parentId, String name) {
+        Name propName = nameFactory.create(name);
+        return new PropertyState(
+                new PropertyId(parentId, propName),
+                NodeState.STATUS_EXISTING, false);
+    }
+
+    /**
+     * Create an event state for an operation on a node.
+     *
+     * @param n node state
+     * @param type <code>Event.NODE_ADDED</code> or <code>Event.NODE_REMOVED</code>
+     * @param name node name
+     * @return event state
+     */
+    protected EventState createEventState(NodeState n, int type, String name) {
+        Path.Element relPath = pathFactory.createElement(nameFactory.create(name));
+
+        switch (type) {
+        case Event.NODE_ADDED:
+            return EventState.childNodeAdded(
+                    n.getParentId(), pathFactory.getRootPath(),
+                    n.getNodeId(), relPath, n.getNodeTypeName(),
+                    n.getMixinTypeNames(), session);
+        case Event.NODE_REMOVED:
+            return EventState.childNodeRemoved(
+                    n.getParentId(), pathFactory.getRootPath(),
+                    n.getNodeId(), relPath, n.getNodeTypeName(),
+                    n.getMixinTypeNames(), session);
+        }
+        return null;
+    }
+
+    /**
+     * Create an event state for a property operation.
+     *
+     * @param n node state
+     * @param type <code>Event.NODE_ADDED</code> or <code>Event.NODE_REMOVED</code>
+     * @param name property name
+     * @return event state
+     */
+    protected EventState createEventState(PropertyState p, NodeState parent, int type) {
+        Path.Element relPath = pathFactory.createElement(p.getName());
+
+        switch (type) {
+        case Event.PROPERTY_ADDED:
+            return EventState.propertyAdded(
+                    p.getParentId(), pathFactory.getRootPath(), relPath,
+                    parent.getNodeTypeName(), parent.getMixinTypeNames(),
+                    session);
+        case Event.PROPERTY_CHANGED:
+            return EventState.propertyChanged(
+                    p.getParentId(), pathFactory.getRootPath(), relPath,
+                    parent.getNodeTypeName(), parent.getMixinTypeNames(),
+                    session);
+        case Event.PROPERTY_REMOVED:
+            return EventState.propertyRemoved(
+                    p.getParentId(), pathFactory.getRootPath(), relPath,
+                    parent.getNodeTypeName(), parent.getMixinTypeNames(),
+                    session);
+        }
+        return null;
+    }
+}

Propchange: jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/MemoryJournal.java
URL: http://svn.apache.org/viewvc/jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/MemoryJournal.java?rev=708199&r1=708198&r2=708199&view=diff
==============================================================================
--- jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/MemoryJournal.java (original)
+++ jackrabbit/branches/1.5/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/MemoryJournal.java Mon Oct 27 07:31:01 2008
@@ -29,6 +29,8 @@
 import org.apache.jackrabbit.core.journal.JournalException;
 import org.apache.jackrabbit.core.journal.RecordIterator;
 import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Memory-based journal, useful for testing purposes only.
@@ -36,6 +38,21 @@
 public class MemoryJournal extends AbstractJournal {
 
     /**
+     * Default read delay: none.
+     */
+    private static final long DEFAULT_READ_DELAY = 0;
+
+    /**
+     * Default write delay: none.
+     */
+    private static final long DEFAULT_WRITE_DELAY = 0;
+
+    /**
+     * Logger.
+     */
+    private static Logger log = LoggerFactory.getLogger(MemoryJournal.class);
+
+    /**
      * Revision.
      */
     private InstanceRevision revision = new MemoryRevision();
@@ -46,6 +63,23 @@
     private ArrayList records = new ArrayList();
 
     /**
+     * Set the read delay, i.e. the time in ms to wait before returning
+     * a record.
+     */
+    private long readDelay = DEFAULT_READ_DELAY;
+
+    /**
+     * Set the write delay, i.e. the time in ms to wait before appending
+     * a record.
+     */
+    private long writeDelay = DEFAULT_WRITE_DELAY;
+
+    /**
+     * Flag indicating whether this journal is closed.
+     */
+    private boolean closed;
+
+    /**
      * {@inheritDoc}
      */
     public InstanceRevision getInstanceRevision() throws JournalException {
@@ -65,7 +99,7 @@
      * {@inheritDoc}
      */
     protected void doLock() throws JournalException {
-        // not implemented
+        checkState();
     }
 
     /**
@@ -74,6 +108,8 @@
     protected void append(AppendRecord record, InputStream in, int length)
             throws JournalException {
 
+        checkState();
+
         byte[] data = new byte[length];
         int off = 0;
 
@@ -90,6 +126,11 @@
                 throw new JournalException(msg, e);
             }
         }
+        try {
+            Thread.sleep(writeDelay);
+        } catch (InterruptedException e) {
+            throw new JournalException("Interrupted in append().");
+        }
         records.add(new MemoryRecord(getId(), record.getProducerId(), data));
         record.setRevision(records.size());
     }
@@ -98,7 +139,11 @@
      * {@inheritDoc}
      */
     protected void doUnlock(boolean successful) {
-        // not implemented
+        try {
+            checkState();
+        } catch (JournalException e) {
+            log.warn("Journal already closed while unlocking.");
+        }
     }
 
     /**
@@ -107,6 +152,8 @@
     protected RecordIterator getRecords(long startRevision)
             throws JournalException {
 
+        checkState();
+
         startRevision = Math.max(startRevision, 0);
         long stopRevision = records.size();
 
@@ -123,12 +170,56 @@
     }
 
     /**
+     * Return the read delay in milliseconds.
+     *
+     * @return read delay
+     */
+    public long getReadDelay() {
+        return readDelay;
+    }
+
+    /**
+     * Set the read delay in milliseconds.
+     *
+     * @param readDelay read delay
+     */
+    public void setReadDelay(long readDelay) {
+        this.readDelay = readDelay;
+    }
+
+    /**
+     * Return the write delay in milliseconds.
+     *
+     * @return write delay
+     */
+    public long getWriteDelay() {
+        return writeDelay;
+    }
+
+    /**
+     * Set the write delay in milliseconds.
+     *
+     * @param writeDelay write delay
+     */
+    public void setWriteDelay(long writeDelay) {
+        this.writeDelay = writeDelay;
+    }
+
+    /**
      * {@inheritDoc}
      */
     public void close() {
-        // nothing to be done here
+        closed = true;
     }
 
+    /**
+     * Check state of this journal.
+     */
+    private void checkState() throws JournalException {
+        if (closed) {
+            throw new JournalException("Journal closed.");
+        }
+    }
 
     /**
      * Memory record.
@@ -233,10 +324,18 @@
             int index = (int) revision;
             MemoryRecord record = (MemoryRecord) records.get(index);
 
+            checkState();
+
             byte[] data = record.getData();
             DataInputStream dataIn = new DataInputStream(
                     new ByteArrayInputStream(data));
 
+            try {
+                Thread.sleep(readDelay);
+            } catch (InterruptedException e) {
+                throw new JournalException("Interrupted in read().");
+            }
+
             return new ReadRecord(record.getJournalId(), record.getProducerId(),
                     ++revision, dataIn, data.length,
                     getResolver(), getNamePathResolver());