/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. */ package org.olat.commons.coordinate.cluster; import org.olat.core.commons.persistence.DB; import org.olat.core.commons.services.lock.pessimistic.PessimisticLockManager; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.AssertException; import org.olat.core.logging.OLog; import org.olat.core.logging.Tracing; import org.olat.core.util.coordinate.Syncer; import org.olat.core.util.coordinate.SyncerCallback; import org.olat.core.util.coordinate.SyncerExecutor; import org.olat.core.util.coordinate.util.DerivedStringSyncer; import org.olat.core.util.resource.OresHelper; /** * Description:<br> * cluster mode implementation of the Syncer * * <P> * Initial Date: 21.09.2007 <br> * @author Felix Jost, http://www.goodsolutions.ch */ public class ClusterSyncer implements Syncer { private static final OLog log = Tracing.createLoggerFor(ClusterSyncer.class); private int executionTimeThreshold = 3000; // warn if the execution takes longer than three seconds private final ThreadLocal<ThreadLocalClusterSyncer> data = new ThreadLocal<ThreadLocalClusterSyncer>(); private PessimisticLockManager pessimisticLockManager; private DB dbInstance; /** * [used by spring] * @param pessimisticLockManager */ private ClusterSyncer(PessimisticLockManager pessimisticLockManager) { this.setPessimisticLockManager(pessimisticLockManager); } public void setDbInstance(DB db){ dbInstance = db; } /** * @see org.olat.core.util.coordinate.Syncer#doInSync(org.olat.core.id.OLATResourceable, org.olat.core.util.coordinate.SyncerCallback) */ public <T> T doInSync(OLATResourceable ores, SyncerCallback<T> callback) { getData().setSyncObject(ores);// Store ores-object for assertAlreadyDoInSyncFor(ores) String asset = OresHelper.createStringRepresenting(ores); // 1. sync on vm (performance and net bandwith reason, and also for a fair per-node handling of db request) // cluster:::: measure throughput with/without this sync // : maybe also measure if with a n-Semaphore (at most n concurrent accesses) throughput incs or decs long start = 0; boolean isDebug = log.isDebug(); if (isDebug) start = System.currentTimeMillis(); T res; Object syncObj = DerivedStringSyncer.getInstance().getSynchLockFor(ores); synchronized (syncObj) {//cluster_ok is per vm only. this synchronized is needed for multi-core processors to handle // memory-flushing from registers correctly. without this synchronized you could have different // states of (instance-/static-)fields in different cores getData().incrementAndCheckNestedLevelCounter(); // 2. sync on cluster // acquire a db lock with select for update which blocks other db select for updates on the same record // until the transaction is committed or rollbacked try { getPessimisticLockManager().findOrPersistPLock(asset); // now execute the task, which may or may not contain further db queries. res = callback.execute(); } finally { getData().decrementNestedLevelCounter(); } //clear the thread local if(getData().getNestedLevel() == 0) { data.remove(); } // we used to not do a commit here but delay that to the end of the dispatching-process. the comment // was: "the lock will be released after calling commit at the end of dispatching-process // needed postcondition after the servlet has finished the request: a commit or rollback on the db to release the lock. // otherwise the database will throw a "lock wait timeout exceeded" message after some time and thus release the lock." // but realizing that this can a) cause long locking phases and b) deadlocks between VMs // we decided to do a commit here and work with its consequence which is that everything that happened // prior to the doInSync call is also committed. This though corresponds to the OLAT 6.0.x model and // was acceptable there as well. dbInstance.commit(); } if (isDebug) { long stop = System.currentTimeMillis(); if (stop-start > executionTimeThreshold) { log.warn("execution time exceeded limit of "+executionTimeThreshold+": "+(stop-start), new AssertException("generate stacktrace")); } } return res; } /** * @see org.olat.core.util.coordinate.Syncer#doInSync(org.olat.core.id.OLATResourceable, org.olat.core.util.coordinate.SyncerExecutor) */ public void doInSync(OLATResourceable ores, final SyncerExecutor executor) { // call the other doInSync variant to avoid duplicate code here doInSync(ores, new SyncerCallback<Object>() { public Object execute() { executor.execute(); return null; } }); } /** * @see org.olat.core.util.coordinate.Syncer#assertAlreadyDoInSyncFor(org.olat.core.id.OLATResourceable) */ @Override public void assertAlreadyDoInSyncFor(OLATResourceable ores) { if (getData().getSyncObject() == null || !getData().isEquals(ores) || (getData().getNestedLevel() == 0) ) { throw new AssertException("This method must be called from doInSync block with ores=" + ores); } } /** * [used by spring] * @param executionTimeThreshold */ public void setExecutionTimeThreshold(int executionTimeThreshold) { this.executionTimeThreshold = executionTimeThreshold; } private void setData(ThreadLocalClusterSyncer data) { this.data.set(data); } private ThreadLocalClusterSyncer getData() { ThreadLocalClusterSyncer tld = data.get(); if (tld == null) { tld = new ThreadLocalClusterSyncer(); setData(tld); } return tld; } public void setPessimisticLockManager(PessimisticLockManager pessimisticLockManager) { this.pessimisticLockManager = pessimisticLockManager; } public PessimisticLockManager getPessimisticLockManager() { return pessimisticLockManager; } ////////////// // Inner class ////////////// /** * A <b>ThreadLocalClusterSyncer</b> is used as a central place to store data on a per * thread basis. * * @author Christian Guretzki */ private class ThreadLocalClusterSyncer { private int nestedLevelCounter = 0; private OLATResourceable ores; protected void incrementAndCheckNestedLevelCounter() { nestedLevelCounter++; if (nestedLevelCounter > 1) { nestedLevelCounter--; throw new AssertException("ClusterSyncer: nested doInSync is not allowed"); } } public int getNestedLevel() { return nestedLevelCounter; } protected void decrementNestedLevelCounter() { nestedLevelCounter--; if (nestedLevelCounter < 0) { throw new AssertException("ClusterSyncer nestedLevelCounter could not be < 0, do not call decrementNestedLevelCounter twice"); } } protected OLATResourceable getSyncObject() { return ores; } protected void setSyncObject(OLATResourceable ores) { this.ores = ores; } protected boolean isEquals(OLATResourceable res) { if (!ores.getResourceableTypeName().equals(res.getResourceableTypeName())) { return false; } if (!ores.getResourceableId().equals(res.getResourceableId())) { return false; } return true; } } }