Author: christian.bauer(a)jboss.com
Date: 2009-04-06 03:20:49 -0400 (Mon, 06 Apr 2009)
New Revision: 10309
Added:
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetFlushEventListener.java
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetLockTimeoutException.java
Removed:
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetMonitor.java
Modified:
trunk/examples/wiki/src/etc/META-INF/persistence-dev-war.xml
trunk/examples/wiki/src/etc/META-INF/persistence-prod-war.xml
trunk/examples/wiki/src/etc/META-INF/persistence-test-war.xml
trunk/examples/wiki/src/etc/WEB-INF/pages.xml
trunk/examples/wiki/src/etc/i18n/messages_en.properties
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/action/Pager.java
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetOperation.java
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostDeleteEventListener.java
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostInsertEventListener.java
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html
trunk/examples/wiki/view/includes/pager.xhtml
Log:
JBSEAM-4069, new approach to nested set locking
Modified: trunk/examples/wiki/src/etc/META-INF/persistence-dev-war.xml
===================================================================
--- trunk/examples/wiki/src/etc/META-INF/persistence-dev-war.xml 2009-04-06 04:49:46 UTC
(rev 10308)
+++ trunk/examples/wiki/src/etc/META-INF/persistence-dev-war.xml 2009-04-06 07:20:49 UTC
(rev 10309)
@@ -76,6 +76,8 @@
value="read-write, WikiFeedEntryCollection"/>
<!-- Nested Set handling through special Hibernate event listeners -->
+ <property name="hibernate.ejb.event.flush"
+
value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetFlushEventListener"/>
<property name="hibernate.ejb.event.post-insert"
value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetPostInsertEventListener"/>
<property name="hibernate.ejb.event.post-delete"
Modified: trunk/examples/wiki/src/etc/META-INF/persistence-prod-war.xml
===================================================================
--- trunk/examples/wiki/src/etc/META-INF/persistence-prod-war.xml 2009-04-06 04:49:46 UTC
(rev 10308)
+++ trunk/examples/wiki/src/etc/META-INF/persistence-prod-war.xml 2009-04-06 07:20:49 UTC
(rev 10309)
@@ -73,6 +73,8 @@
value="read-write, WikiFeedEntryCollection"/>
<!-- Nested Set handling through special Hibernate event listeners -->
+ <property name="hibernate.ejb.event.flush"
+
value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetFlushEventListener"/>
<property name="hibernate.ejb.event.post-insert"
value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetPostInsertEventListener"/>
<property name="hibernate.ejb.event.post-delete"
Modified: trunk/examples/wiki/src/etc/META-INF/persistence-test-war.xml
===================================================================
--- trunk/examples/wiki/src/etc/META-INF/persistence-test-war.xml 2009-04-06 04:49:46 UTC
(rev 10308)
+++ trunk/examples/wiki/src/etc/META-INF/persistence-test-war.xml 2009-04-06 07:20:49 UTC
(rev 10309)
@@ -48,6 +48,8 @@
<property name="hibernate.session_factory_name"
value="SessionFactories/lacewikiSF"/>
<!-- Nested Set handling through special Hibernate event listeners -->
+ <property name="hibernate.ejb.event.flush"
+
value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetFlushEventListener"/>
<property name="hibernate.ejb.event.post-insert"
value="org.jboss.seam.wiki.core.nestedset.listener.NestedSetPostInsertEventListener"/>
<property name="hibernate.ejb.event.post-delete"
Modified: trunk/examples/wiki/src/etc/WEB-INF/pages.xml
===================================================================
--- trunk/examples/wiki/src/etc/WEB-INF/pages.xml 2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/etc/WEB-INF/pages.xml 2009-04-06 07:20:49 UTC (rev 10309)
@@ -335,6 +335,21 @@
</redirect>
</exception>
+ <exception
class="org.jboss.seam.wiki.core.nestedset.listener.NestedSetLockTimeoutException">
+ <end-conversation/>
+ <redirect view-id="/wiki.xhtml">
+ <message
severity="WARN">#{messages['lacewiki.msg.LockTimeoutError']}</message>
+ </redirect>
+ </exception>
+
+ <!-- This occurs on concurrent delete of comments -->
+ <exception class="javax.persistence.EntityNotFoundException">
+ <end-conversation/>
+ <redirect view-id="/wiki.xhtml">
+ <message
severity="WARN">#{messages['lacewiki.msg.EntityNotFound']}</message>
+ </redirect>
+ </exception>
+
<exception class="org.jboss.seam.security.AuthorizationException">
<end-conversation/>
<redirect view-id="/message.xhtml">
Modified: trunk/examples/wiki/src/etc/i18n/messages_en.properties
===================================================================
--- trunk/examples/wiki/src/etc/i18n/messages_en.properties 2009-04-06 04:49:46 UTC (rev
10308)
+++ trunk/examples/wiki/src/etc/i18n/messages_en.properties 2009-04-06 07:20:49 UTC (rev
10309)
@@ -267,10 +267,6 @@
lacewiki.label.dirDisplay.ReadAccess=Read Access
lacewiki.label.dirDisplay.WriteAccess=Write Access
lacewiki.label.dirDisplay.DirectoryIsEmpty=This directory is empty.
-lacewiki.label.dirDisplay.PagerShowing=
-lacewiki.label.dirDisplay.PagerTo=to
-lacewiki.label.dirDisplay.PagerOf=of
-lacewiki.label.dirDisplay.PagerElements=
lacewiki.label.dirDisplay.ShowItems=Show items
lacewiki.label.dirDisplay.All=All
lacewiki.button.dirDisplay.Refresh=Re<u>f</u>resh
@@ -652,6 +648,9 @@
lacewiki.msg.Clipboard.DuplicatePasteName=The name '{0}' was already in use in
this area, renamed item to '{1}'.
lacewiki.msg.Clipboard.DuplicatePasteNameFailure=The name '{0}' was already in
use in this area and is too long to be renamed, skipping paste.
+# Pager
+lacewiki.label.pagerTo=to
+lacewiki.label.pagerOf=of
# Entity update/delete/persist
@@ -717,6 +716,8 @@
lacewiki.msg.TrashAreaNotFound=Could not find trash area with name {0} - your
configuration is broken, please change it.
lacewiki.msg.HelpAreaNotFound=Could not find help area with name {0} - your
configuration is broken, please change it.
lacewiki.msg.OptimisticLockError=Someone modified the same record while you were editing
it. Your workspace has been closed.
+lacewiki.msg.LockTimeoutError=Your action conflicted with the action of another user,
please try again in a few seconds.
+lacewiki.msg.EntityNotFound=The requested entity was not found.
lacewiki.msg.AccessDenied=Access Denied
lacewiki.msg.FatalError=Request failed, please check the application log or contact the
administrator
lacewiki.msg.RequestError=Request failed, most likely because a request parameter was
missing
Modified: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/action/Pager.java
===================================================================
--- trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/action/Pager.java 2009-04-06
04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/action/Pager.java 2009-04-06
07:20:49 UTC (rev 10309)
@@ -14,6 +14,7 @@
@AutoCreate
public class Pager implements Serializable {
+ private String pagerEventName = "Pager";
private Long numOfRecords = 0l;
private Integer page = 0;
private Long pageSize = 15l;
@@ -24,6 +25,15 @@
this.pageSize = pageSize;
}
+ public Pager(String pagerEventName) {
+ this.pagerEventName = pagerEventName;
+ }
+
+ public Pager(String pagerEventName, Long pageSize) {
+ this.pagerEventName = pagerEventName;
+ this.pageSize = pageSize;
+ }
+
public Long getNumOfRecords() {
return numOfRecords;
}
@@ -103,22 +113,22 @@
public void setFirstPage() {
setPage(getFirstPage());
- Events.instance().raiseEvent("Pager.pageChanged");
+ Events.instance().raiseEvent(pagerEventName + "pageChanged");
}
public void setPreviousPage() {
setPage(getPreviousPage());
- Events.instance().raiseEvent("Pager.pageChanged");
+ Events.instance().raiseEvent(pagerEventName + ".pageChanged");
}
public void setNextPage() {
setPage(getNextPage());
- Events.instance().raiseEvent("Pager.pageChanged");
+ Events.instance().raiseEvent(pagerEventName + ".pageChanged");
}
public void setLastPage() {
setPage(new Long(getLastPage()).intValue());
- Events.instance().raiseEvent("Pager.pageChanged");
+ Events.instance().raiseEvent(pagerEventName + ".pageChanged");
}
public String toString() {
Added:
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetFlushEventListener.java
===================================================================
---
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetFlushEventListener.java
(rev 0)
+++
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetFlushEventListener.java 2009-04-06
07:20:49 UTC (rev 10309)
@@ -0,0 +1,105 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Distributable under LGPL license.
+ * See terms of license at
gnu.org.
+ */
+package org.jboss.seam.wiki.core.nestedset.listener;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.hibernate.HibernateException;
+import org.hibernate.ejb.event.EJB3FlushEventListener;
+import org.hibernate.event.EventSource;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * <p>
+ * TODO: This is really the issue why a nice nested set implementation with Hibernate and
MySQL is impossible!
+ * </p>
+ * <p>
+ * Any nested set tree modification potentially updates all rows in a database table.
This
+ * requires several <tt>UPDATE</tt> statements, and also
<tt>INSERT</tt> and <tt>DELETE</tt>.
+ * Any concurrent insertion or deletion to the rows betwen <tt>UPDATE</tt>
statements would be fatal and
+ * corrupt the tree information. Transactions that modify nested set data should be
serialized.
+ * </p>
+ * <p>
+ * For example, without any additional locking we'd run into the following deadlock
situation with MySQL. Consider
+ * the following threads and the required modifications when two nested set node are
deleted concurrently:
+ * </p>
+ *
+ * <pre>
+ * Thread I Nested Set Nodes Thread II
+ * --- 1. DELETE ---> A
+ *
+ * B <--- 2. DELETE ---
+ *
+ * --- 2. UPDATE ---> B
+ * (Waits for lock)
+ * A <--- 3. UPDATE ---
+ * (Waits for lock)
+ *
+ * </pre>
+ *
+ * <p>
+ * This results in a MySQL deadlock detection exception and a rollback of the transaction
in Thread II.
+ * The usual solution is to lock the whole table(s) to force serialized execution of
threads that modify
+ * nested set tree state.
+ * </p>
+ * <p>
+ * However, because MySQL has an unusable locking system (locking a table commits the
current transaction, you
+ * need to lock all tables you are going to use from that point on, etc.), and because
portability is
+ * a concern of this nested set implementation, we work around the problem with an
in-memory exclusive lock.
+ * </p>
+ * <p>
+ * The situation is further complicated by Hibernate's flushing/eventing behavior.
There is no way how we can
+ * only lock for nested set updates, we need to lock on every execution of a flush.
WARNING: This severely
+ * degrades performance of your application, as any automatic or manual flush of the
Hibernate persistence context
+ * will be serialized application-wide! Luckily, we can only lock when deletions or
insertions are queued so
+ * flushing with no modifications in the persistence context (e.g. before a query) is not
affected.
+ * </p>
+ * <p>
+ * <b>NOTE:</b> This does NOT work if several applications modify the nested
set tree on the same database tables!
+ * </p>
+ *
+ * @author Christian Bauer
+ */
+public class NestedSetFlushEventListener extends EJB3FlushEventListener {
+
+ private static final Log log = LogFactory.getLog(NestedSetFlushEventListener.class);
+
+ private static final int LOCK_TIMEOUT_SECONDS = 15;
+
+ private static final Lock lock = new ReentrantLock(true);
+
+ @Override
+ protected void performExecutions(EventSource eventSource) throws HibernateException
{
+
+ if (eventSource.getActionQueue().areInsertionsOrDeletionsQueued()) {
+ try {
+ log.debug("######################### trying to obtain exclusive lock
for " + LOCK_TIMEOUT_SECONDS +
+ " seconds before performing database modifications during
flush");
+ if (lock.tryLock(LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ log.debug("successfully obtained lock, executing flush");
+ try {
+ super.performExecutions(eventSource);
+ } finally {
+ log.debug("releasing exclusive lock after flush
execution");
+ lock.unlock();
+ }
+ } else {
+ throw new NestedSetLockTimeoutException("Could not aquire
exclusive lock during database flush");
+ }
+ } catch (InterruptedException ex) {
+ throw new NestedSetLockTimeoutException("Current thread could not
aquire lock, has been interrupted");
+ }
+ } else {
+ super.performExecutions(eventSource);
+ }
+
+ }
+
+}
\ No newline at end of file
Added:
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetLockTimeoutException.java
===================================================================
---
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetLockTimeoutException.java
(rev 0)
+++
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetLockTimeoutException.java 2009-04-06
07:20:49 UTC (rev 10309)
@@ -0,0 +1,23 @@
+package org.jboss.seam.wiki.core.nestedset.listener;
+
+/**
+ * @author Christian Bauer
+ */
+public class NestedSetLockTimeoutException extends RuntimeException {
+
+ public NestedSetLockTimeoutException() {
+ super();
+ }
+
+ public NestedSetLockTimeoutException(String s) {
+ super(s);
+ }
+
+ public NestedSetLockTimeoutException(String s, Throwable throwable) {
+ super(s, throwable);
+ }
+
+ public NestedSetLockTimeoutException(Throwable throwable) {
+ super(throwable);
+ }
+}
Deleted:
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetMonitor.java
===================================================================
---
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetMonitor.java 2009-04-06
04:49:46 UTC (rev 10308)
+++
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetMonitor.java 2009-04-06
07:20:49 UTC (rev 10309)
@@ -1,49 +0,0 @@
-package org.jboss.seam.wiki.core.nestedset.listener;
-
-import org.hibernate.event.EventSource;
-
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.TimeUnit;
-
-/**
- * An alternative to table locking, we serialize nested set insert/updates in memory.
- *
- * <p>
- * Any nested set tree modification potentially updates all rows in a database table.
This
- * requires several <tt>UPDATE</tt> statements, and also
<tt>INSERT</tt> and <tt>DELETE</tt>.
- * Any concurrent commit to the rows betwen <tt>UPDATE</tt> statements would
be fatal and
- * corrupt the tree information. The usual solution is to lock the whole table(s).
Because
- * MySQL has a compleltey unusable locking system (locking a table commits the current
transaction, you
- * need to lock all tables you are going to use from that point on, etc.), and because
portability is
- * a concern of this Nested Set implementation, we work around the problem with an
in-memory exclusive lock.
- * </p>
- * <p>
- * <b>NOTE:</b> This does NOT work if several applications modify the nested
set
- * tree in the same tables!
- * </p>
- *
- * @author Christian Bauer
- */
-public class NestedSetMonitor {
-
- private static final int LOCK_TIMEOUT_SECONDS = 10;
-
- private static final Lock lock = new ReentrantLock(true);
-
- public static void executeOperation(NestedSetOperation operation, EventSource
session) {
- try {
- if (lock.tryLock(LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
- try {
- operation.execute(session);
- } finally {
- lock.unlock();
- }
- } else {
- throw new RuntimeException("Could not aquire lock to update nested
set tree");
- }
- } catch (InterruptedException ex) {
- throw new RuntimeException("Current thread could not aquire lock, has
been interrupted");
- }
- }
-}
Modified:
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetOperation.java
===================================================================
---
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetOperation.java 2009-04-06
04:49:46 UTC (rev 10308)
+++
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetOperation.java 2009-04-06
07:20:49 UTC (rev 10309)
@@ -43,8 +43,6 @@
* for the deprecated <tt>Session#connection()</tt> method.
* </p>
*
- * TODO: We should lock the tables! Instead we are using the NestedSetMonitor as a
workaround...
- *
* @author Christian Bauer
*/
public class NestedSetOperation {
Modified:
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostDeleteEventListener.java
===================================================================
---
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostDeleteEventListener.java 2009-04-06
04:49:46 UTC (rev 10308)
+++
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostDeleteEventListener.java 2009-04-06
07:20:49 UTC (rev 10309)
@@ -26,10 +26,7 @@
if ( NestedSetNode.class.isAssignableFrom(event.getEntity().getClass())) {
log.debug("executing nested set delete operation, recalculating the
tree");
- NestedSetMonitor.executeOperation(
- new DeleteNestedSetOperation( (NestedSetNode)event.getEntity() ),
- event.getSession()
- );
+ new DeleteNestedSetOperation( (NestedSetNode)event.getEntity()
).execute(event.getSession());
}
}
Modified:
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostInsertEventListener.java
===================================================================
---
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostInsertEventListener.java 2009-04-06
04:49:46 UTC (rev 10308)
+++
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/listener/NestedSetPostInsertEventListener.java 2009-04-06
07:20:49 UTC (rev 10309)
@@ -26,10 +26,7 @@
if ( NestedSetNode.class.isAssignableFrom(event.getEntity().getClass())) {
log.debug("executing nested set insert operation, recalculating the
tree");
- NestedSetMonitor.executeOperation(
- new InsertNestedSetOperation((NestedSetNode)event.getEntity()),
- event.getSession()
- );
+ new
InsertNestedSetOperation((NestedSetNode)event.getEntity()).execute(event.getSession());
}
}
Modified: trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html
===================================================================
---
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html 2009-04-06
04:49:46 UTC (rev 10308)
+++
trunk/examples/wiki/src/main/org/jboss/seam/wiki/core/nestedset/package.html 2009-04-06
07:20:49 UTC (rev 10309)
@@ -59,7 +59,7 @@
<p>
<b>Manual Recursion</b>: Select all the children of node
<tt>C</tt>, and if these nodes have children, recursively
query until the whole subtree is loaded. This can be implemented with a stored
procedure in SQL or by
- recursively exeucting <tt>SELECT</tt> statements in the application
language. This strategy does not scale.
+ recursively executing <tt>SELECT</tt> statements in the application
language. This strategy does not scale.
</p>
</li>
<li>
@@ -201,7 +201,7 @@
</pre>
<p>
- This implementation is based on mix-in and delegates. The <tt>ITEM</tt>
table of the <tt>Item</tt> class will
+ This implementation is based on mix-in and delegates. The <tt>ITEM</tt>
table of the <tt>Item</tt> class
does not carry the left, right, and thread values. This job is delegated to an
additional
<tt>ItemNestedSetDelegate</tt> class:
</p>
@@ -280,11 +280,12 @@
<persistence-unit ...>
...
<properties>
+ <property name="hibernate.ejb.event.flush"
+ value="nestedset.NestedSetFlushEventListener"/>
<property name="hibernate.ejb.event.post-insert"
value="nestedset.NestedSetPostInsertEventListener"/>
<property name="hibernate.ejb.event.post-delete"
value="nestedset.NestedSetPostDeleteEventListener"/>
-
</properties>
</persistence-unit>
</pre>
@@ -296,6 +297,12 @@
</p>
<p>
+ Note that concurrent nested set tree modifications need to be serialized. This
implementation locks flush events
+ in-memory with a <tt>ReentrantLock</tt> and a timeout of 15 seconds. See
<tt>NestedSetFlushEventListener.java</tt>
+ for more information.
+</p>
+
+<p>
To query for a subtree, use the <tt>NestedSetWrapper</tt> and
<tt>NestedSetResultTransformer</tt>
convenience classes. An example, loading the whole subtree starting at
<tt>startNode</tt> (which would
be an instance of <tt>Item</tt> you have already loaded):
Modified: trunk/examples/wiki/view/includes/pager.xhtml
===================================================================
--- trunk/examples/wiki/view/includes/pager.xhtml 2009-04-06 04:49:46 UTC (rev 10308)
+++ trunk/examples/wiki/view/includes/pager.xhtml 2009-04-06 07:20:49 UTC (rev 10309)
@@ -48,10 +48,10 @@
</s:fragment>
<s:fragment>
- <h:outputText
value="#{messages['lacewiki.label.dirDisplay.PagerShowing']}
- #{pager.firstRecord}
#{messages['lacewiki.label.dirDisplay.PagerTo']}
- #{pager.lastRecord}
#{messages['lacewiki.label.dirDisplay.PagerOf']}
- #{pager.numOfRecords}
#{messages['lacewiki.label.dirDisplay.PagerElements']}"/>
+ <h:outputText value="#{pager.firstRecord}
#{messages['lacewiki.label.pagerTo']}
+ #{pager.lastRecord}
#{messages['lacewiki.label.pagerOf']}
+ #{pager.numOfRecords}
+ #{pager.numOfRecords > 1 ? pagerPluralLabel :
pagerSingularLabel}"/>
</s:fragment>
<s:fragment>