jackrabbit-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From mreut...@apache.org
Subject svn commit: r394891 - in /jackrabbit/trunk/jackrabbit/src: main/java/org/apache/jackrabbit/util/Locked.java test/java/org/apache/jackrabbit/core/LockTest.java test/java/org/apache/jackrabbit/core/TestAll.java
Date Tue, 18 Apr 2006 09:45:08 GMT
Author: mreutegg
Date: Tue Apr 18 02:45:06 2006
New Revision: 394891

URL: http://svn.apache.org/viewcvs?rev=394891&view=rev
Log:
Provide a utility that allows to execute a block of code while a lock is held on a lockable
node.

Added:
    jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/util/Locked.java   (with
props)
    jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/LockTest.java   (with
props)
Modified:
    jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/TestAll.java

Added: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/util/Locked.java
URL: http://svn.apache.org/viewcvs/jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/util/Locked.java?rev=394891&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/util/Locked.java (added)
+++ jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/util/Locked.java Tue Apr
18 02:45:06 2006
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  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.util;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Node;
+import javax.jcr.Session;
+import javax.jcr.Repository;
+import javax.jcr.observation.ObservationManager;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.Event;
+import javax.jcr.lock.LockException;
+import javax.jcr.lock.Lock;
+
+/**
+ * <code>Locked</code> is a utility to synchronize modifications on a lockable
+ * node. The modification is applied while the lock on the node is held, thus
+ * ensuring that the modification will never fail with an {@link
+ * javax.jcr.InvalidItemStateException}. This utility can be used with any
+ * JCR Repository, not just Jackrabbit.
+ * <p/>
+ * The following example shows how this utility can be used to implement
+ * a persistent counter:
+ * <pre>
+ * final Node counter = ...;
+ * long nextValue = ((Long) new Locked() {
+ *     protected Object run() throws RepositoryException {
+ *         long value = counter.getProperty("value").getLong();
+ *         counter.setProperty("value", ++value);
+ *         counter.save();
+ *         return new Long(value);
+ *     }
+ * }.with(counter, false)).longValue();
+ * </pre>
+ * If you specify a <code>timeout</code> you need to check the return value
+ * whether the <code>run</code> method could be executed within the timeout
+ * period:
+ * <pre>
+ * final Node counter = ...;
+ * Object ret = new Locked() {
+ *     protected Object run() throws RepositoryException {
+ *         long value = counter.getProperty("value").getLong();
+ *         counter.setProperty("value", ++value);
+ *         counter.save();
+ *         return new Long(value);
+ *     }
+ * }.with(counter, false);
+ * if (ret == Locked.TIMED_OUT) {
+ *     // do whatever you think is appropriate in this case
+ * } else {
+ *     // get the value
+ *     long nextValue = ((Long) ret).longValue();
+ * }
+ * </pre>
+ */
+public abstract class Locked {
+
+    /**
+     * Object returned when timeout is reached without being able to call
+     * {@link #run} while holding the lock.
+     */
+    public static final Object TIMED_OUT = new Object();
+
+    /**
+     * The lock we hold while executing {@link #run}.
+     */
+    private Lock lock;
+
+    /**
+     * An event listener if one was registered
+     */
+    private EventListener listener;
+
+    /**
+     * Executes {@link #run} while the lock on <code>lockable</code> is held.
+     * This method will block until {@link #run} is executed while holding the
+     * lock on node <code>lockable</code>.
+     *
+     * @param lockable a lockable node.
+     * @param isDeep   <code>true</code> if <code>lockable</code>
will be locked
+     *                 deep.
+     * @return the object returned by {@link #run}.
+     * @throws RepositoryException  if {@link #run} throws an exception.
+     * @throws InterruptedException if this thread is interrupted while waiting
+     *                              for the lock on node <code>lockable</code>.
+     */
+    public Object with(Node lockable, boolean isDeep)
+            throws RepositoryException, InterruptedException {
+        return with(lockable, isDeep, Long.MAX_VALUE);
+    }
+
+    /**
+     * Executes the method {@link #run} within the scope of a lock held on
+     * <code>lockable</code>.
+     *
+     * @param lockable the node where the lock is obtained from.
+     * @param isDeep   <code>true</code> if <code>lockable</code>
will be locked
+     *                 deep.
+     * @param timeout  time in milliseconds to wait at most to aquire the lock.
+     * @return the object returned by {@link #run} or {@link #TIMED_OUT} if the
+     *         lock on <code>lockable</code> could not be aquired within the
+     *         specified timeout.
+     * @throws RepositoryException  if {@link #run} throws an exception.
+     * @throws InterruptedException if this thread is interrupted while waiting
+     *                              for the lock on node <code>lockable</code>.
+     */
+    public Object with(Node lockable, boolean isDeep, long timeout)
+            throws RepositoryException, InterruptedException {
+        if (timeout < 0) {
+            throw new IllegalArgumentException("timeout must be >= 0");
+        }
+        Session session = lockable.getSession();
+        try {
+            if (tryLock(lockable, isDeep)) {
+                return runAndUnlock();
+            }
+
+            if (timeout == 0) {
+                return TIMED_OUT;
+            }
+
+            long timelimit;
+            if (timeout == Long.MAX_VALUE) {
+                timelimit = Long.MAX_VALUE;
+            } else {
+                timelimit = System.currentTimeMillis() + timeout;
+            }
+
+            // node is locked by other session -> register event listener if possible
+            if (isObservationSupported(session)) {
+                ObservationManager om = session.getWorkspace().getObservationManager();
+                listener = new EventListener() {
+                    public void onEvent(EventIterator events) {
+                        synchronized (Locked.this) {
+                            Locked.this.notify();
+                        }
+                    }
+                };
+                om.addEventListener(listener, Event.PROPERTY_REMOVED,
+                        lockable.getPath(), false, null, null, true);
+            }
+
+            // now keep trying to aquire the lock
+            // using 'this' as a monitor allows the event listener to notify
+            // the current thread when the lockable node is possibly unlocked
+            for (;;) {
+                synchronized (this) {
+                    if (tryLock(lockable, isDeep)) {
+                        return runAndUnlock();
+                    } else {
+                        // check timeout
+                        if (System.currentTimeMillis() > timelimit) {
+                            return TIMED_OUT;
+                        }
+                        if (listener != null) {
+                            // event listener *should* wake us up, however
+                            // there is a chance that removal of the lockOwner
+                            // property is notified before the node is acutally
+                            // unlocked. therefore we use a safety net to wait
+                            // at most 1000 millis.
+                            this.wait(Math.min(1000, timeout));
+                        } else {
+                            // repository does not support observation
+                            // wait at most 50 millis then retry
+                            this.wait(Math.min(50, timeout));
+                        }
+                    }
+                }
+            }
+        } finally {
+            if (listener != null) {
+                session.getWorkspace().getObservationManager().removeEventListener(listener);
+            }
+        }
+    }
+
+    /**
+     * This method is executed while holding the lock.
+     * @return an object which is then returned by {@link #with with()}.
+     * @throws RepositoryException if an error occurs.
+     */
+    protected abstract Object run() throws RepositoryException;
+
+    /**
+     * Executes {@link #run} and unlocks the lockable node in any case, even
+     * when an exception is thrown.
+     *
+     * @return the object returned by {@link #run()}.
+     * @throws RepositoryException if an error occurs.
+     */
+    private Object runAndUnlock() throws RepositoryException {
+        try {
+            return run();
+        } finally {
+            lock.getNode().unlock();
+        }
+    }
+
+    /**
+     * Tries to aquire a session scoped lock on <code>lockable</code>.
+     *
+     * @param lockable the lockable node
+     * @param isDeep   <code>true</code> if the lock should be deep
+     * @return <code>true</code> if the lock could be aquired.
+     * @throws RepositoryException if an error occurs
+     */
+    private boolean tryLock(Node lockable, boolean isDeep) throws RepositoryException {
+        try {
+            lock = lockable.lock(isDeep, true);
+            // if we get here we have a lock
+            return true;
+        } catch (LockException e) {
+            // locked by some other session
+        }
+        return false;
+    }
+
+    /**
+     * Returns <code>true</code> if the repository supports observation.
+     *
+     * @param s a session of the repository.
+     * @return <code>true</code> if the repository supports observation.
+     */
+    private boolean isObservationSupported(Session s) {
+        return "true".equalsIgnoreCase(s.getRepository().getDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED));
+    }
+}

Propchange: jackrabbit/trunk/jackrabbit/src/main/java/org/apache/jackrabbit/util/Locked.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/LockTest.java
URL: http://svn.apache.org/viewcvs/jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/LockTest.java?rev=394891&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/LockTest.java (added)
+++ jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/LockTest.java Tue
Apr 18 02:45:06 2006
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  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;
+
+import org.apache.jackrabbit.test.AbstractJCRTest;
+import org.apache.jackrabbit.util.Locked;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Node;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Random;
+
+/**
+ * <code>LockTest</code> tests the utility {@link org.apache.jackrabbit.util.Locked}.
+ */
+public class LockTest extends AbstractJCRTest {
+
+    private static final int NUM_THREADS = 100;
+
+    private static final int NUM_CHANGES = 10;
+
+    private static final int NUM_VALUE_GETS = 10;
+
+    /**
+     * Tests the utility {@link org.apache.jackrabbit.util.Locked} by
+     * implementing running multiple threads concurrently that apply changes
+     * to a lockable node.
+     */
+    public void testLockUtility() throws RepositoryException {
+        final Node lockable = testRootNode.addNode(nodeName1);
+        lockable.addMixin(mixLockable);
+        testRootNode.save();
+
+        final List worker = new ArrayList();
+        for (int i = 0; i < NUM_THREADS; i++) {
+            worker.add(new Thread() {
+
+                private final int threadNumber = worker.size();
+
+                public void run() {
+                    Session s;
+                    try {
+                        s = helper.getSuperuserSession();
+                    } catch (RepositoryException e) {
+                        return;
+                    }
+                    try {
+                        for (int i = 0; i < NUM_CHANGES; i++) {
+                            final Node n = (Node) s.getItem(lockable.getPath());
+                            new Locked() {
+                                protected Object run() throws RepositoryException {
+                                    String nodeName = "node" + threadNumber;
+                                    if (n.hasNode(nodeName)) {
+                                        n.getNode(nodeName).remove();
+                                    } else {
+                                        n.addNode(nodeName);
+                                    }
+                                    n.save();
+                                    System.out.println("Thread" + threadNumber + ": saved
modification");
+
+                                    return null;
+                                }
+                            }.with(n, false);
+                            // do a random wait
+                            Thread.sleep(new Random().nextInt(100));
+                        }
+                    } catch (RepositoryException e) {
+                        System.out.println("exception while running code with lock:" + e.getMessage());
+                    } catch (InterruptedException e) {
+                        System.out.println(Thread.currentThread() + " interrupted while waiting
for lock");
+                    } finally {
+                        s.logout();
+                    }
+                }
+            });
+        }
+
+        for (Iterator it = worker.iterator(); it.hasNext(); ) {
+            ((Thread) it.next()).start();
+        }
+
+        for (Iterator it = worker.iterator(); it.hasNext(); ) {
+            try {
+                ((Thread) it.next()).join();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Tests the utility {@link org.apache.jackrabbit.util.Locked} by
+     * implementing a persistent counter.
+     */
+    public void testSequence() throws RepositoryException {
+        final Node counter = testRootNode.addNode(nodeName1);
+        counter.setProperty("value", 0);
+        counter.addMixin(mixLockable);
+        testRootNode.save();
+
+        final List worker = new ArrayList();
+        for (int i = 0; i < NUM_THREADS; i++) {
+            worker.add(new Thread() {
+
+                private final int threadNumber = worker.size();
+
+                public void run() {
+                    Session s;
+                    try {
+                        s = helper.getSuperuserSession();
+                    } catch (RepositoryException e) {
+                        return;
+                    }
+                    try {
+                        for (int i = 0; i < NUM_VALUE_GETS; i++) {
+                            final Node n = (Node) s.getItem(counter.getPath());
+                            long currentValue = ((Long) new Locked() {
+                                protected Object run() throws RepositoryException {
+                                    long value = n.getProperty("value").getLong();
+                                    n.setProperty("value", ++value);
+                                    n.save();
+                                    return new Long(value);
+                                }
+                            }.with(n, false)).longValue();
+                            System.out.println("Thread" + threadNumber + ": got sequence
number: " + currentValue);
+                            // do a random wait
+                            Thread.sleep(new Random().nextInt(100));
+                        }
+                    } catch (RepositoryException e) {
+                        System.out.println("exception while running code with lock:" + e.getMessage());
+                    } catch (InterruptedException e) {
+                        System.out.println(Thread.currentThread() + " interrupted while waiting
for lock");
+                    } finally {
+                        s.logout();
+                    }
+                }
+            });
+        }
+
+        for (Iterator it = worker.iterator(); it.hasNext(); ) {
+            ((Thread) it.next()).start();
+        }
+
+        for (Iterator it = worker.iterator(); it.hasNext(); ) {
+            try {
+                ((Thread) it.next()).join();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Tests the utility {@link org.apache.jackrabbit.util.Locked} by
+     * implementing a persistent counter with a timeout when the next value of
+     * the counter is retrieved. The number of values that can be retrieved by
+     * this test depends on system performance and the configured persistence
+     * manager. 
+     */
+    public void testSequenceWithTimeout() throws RepositoryException {
+        final Node counter = testRootNode.addNode(nodeName1);
+        counter.setProperty("value", 0);
+        counter.addMixin(mixLockable);
+        testRootNode.save();
+
+        final List worker = new ArrayList();
+        for (int i = 0; i < NUM_THREADS; i++) {
+            worker.add(new Thread() {
+
+                private final int threadNumber = worker.size();
+
+                public void run() {
+                    Session s;
+                    try {
+                        s = helper.getSuperuserSession();
+                    } catch (RepositoryException e) {
+                        return;
+                    }
+                    try {
+                        for (int i = 0; i < NUM_VALUE_GETS; i++) {
+                            final Node n = (Node) s.getItem(counter.getPath());
+                            Object ret = new Locked() {
+                                protected Object run() throws RepositoryException {
+                                    long value = n.getProperty("value").getLong();
+                                    n.setProperty("value", ++value);
+                                    n.save();
+                                    return new Long(value);
+                                }
+                            }.with(n, false, 10 * 1000); // expect a value after ten seconds
+                            if (ret == Locked.TIMED_OUT) {
+                                System.out.println("Thread" + threadNumber + ": could not
get a sequence number within 10 seconds");
+                            } else {
+                                long currentValue = ((Long) ret).longValue();
+                                System.out.println("Thread" + threadNumber + ": got sequence
number: " + currentValue);
+                            }
+                            // do a random wait
+                            Thread.sleep(new Random().nextInt(100));
+                        }
+                    } catch (RepositoryException e) {
+                        System.out.println("exception while running code with lock:" + e.getMessage());
+                    } catch (InterruptedException e) {
+                        System.out.println(Thread.currentThread() + " interrupted while waiting
for lock");
+                    } finally {
+                        s.logout();
+                    }
+                }
+            });
+        }
+
+        for (Iterator it = worker.iterator(); it.hasNext(); ) {
+            ((Thread) it.next()).start();
+        }
+
+        for (Iterator it = worker.iterator(); it.hasNext(); ) {
+            try {
+                ((Thread) it.next()).join();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}

Propchange: jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/LockTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/TestAll.java
URL: http://svn.apache.org/viewcvs/jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/TestAll.java?rev=394891&r1=394890&r2=394891&view=diff
==============================================================================
--- jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/TestAll.java (original)
+++ jackrabbit/trunk/jackrabbit/src/test/java/org/apache/jackrabbit/core/TestAll.java Tue
Apr 18 02:45:06 2006
@@ -36,6 +36,7 @@
 
         //suite.addTestSuite(ConcurrencyTest.class);
         //suite.addTestSuite(ConcurrentSaveTest.class);
+        //suite.addTestSuite(LockTest.class);
         suite.addTestSuite(TransientRepositoryTest.class);
         suite.addTestSuite(XATest.class);
 



Mime
View raw message