/** * Copyright (c) 2008-2011 Sonatype, Inc. * All rights reserved. Includes the third-party code listed at http://www.sonatype.com/products/nexus/attributions. * * This program is free software: you can redistribute it and/or modify it only under the terms of the GNU Affero General * Public License Version 3 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License Version 3 * for more details. * * You should have received a copy of the GNU Affero General Public License Version 3 along with this program. If not, see * http://www.gnu.org/licenses. * * Sonatype Nexus (TM) Open Source Version is available from Sonatype, Inc. Sonatype and Sonatype Nexus are trademarks of * Sonatype, Inc. Apache Maven is a trademark of the Apache Foundation. M2Eclipse is a trademark of the Eclipse Foundation. * All other trademarks are the property of their respective owners. */ package org.sonatype.nexus.proxy.item; import java.util.EmptyStackException; import java.util.HashMap; import java.util.Map; import java.util.Stack; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.sonatype.nexus.proxy.access.Action; import org.sonatype.nexus.proxy.item.uid.Attribute; import org.sonatype.nexus.proxy.repository.Repository; /** * The Class RepositoryItemUid. This class represents unique and constant label of all items/files originating from a * Repository, thus backed by some storage (eg. Filesystem). */ public class DefaultRepositoryItemUid implements RepositoryItemUid { private static enum LockStep { READ( true ), WRITE( true ), READ_WRITE_UPGRADE( true ), READ_AS_BEFORE( false ), WRITE_AS_BEFORE( false ); private final boolean mustAct; private LockStep( boolean mustAct ) { this.mustAct = mustAct; } public boolean isMustAct() { return mustAct; } public boolean isReadLockLastLocked() { return READ.equals( this ) || READ_AS_BEFORE.equals( this ); } public LockStep getFollowingLockstep( Action action ) { // sanity check, "action" and "this" must be aligned: // reading actions may be with both locks (because WRITE will be not downgraded) // writing actions may be with WRITE_* and upgrade steps only if ( !action.isReadAction() ) { // write action can be together with LockSteps: WRITE, WRITE_AS_BEFORE, READ_WRITE_UPGRADE switch ( this ) { case WRITE: case WRITE_AS_BEFORE: case READ_WRITE_UPGRADE: break; default: throw new IllegalStateException( "Illegal combination of action \"" + action.toString() + "\" with lockstep " + this.toString() ); } } // return the appropriate "same as" step switch ( this ) { case READ: case READ_AS_BEFORE: return READ_AS_BEFORE; case WRITE: case WRITE_AS_BEFORE: case READ_WRITE_UPGRADE: return WRITE_AS_BEFORE; default: throw new IllegalStateException( "Unknown lockstep " + this.toString() ); } } } private static final ThreadLocal<Map<String, Stack<LockStep>>> threadCtx = new ThreadLocal<Map<String, Stack<LockStep>>>() { @Override protected synchronized Map<String, Stack<LockStep>> initialValue() { return new HashMap<String, Stack<LockStep>>(); }; }; private final RepositoryItemUidFactory factory; private final ReentrantReadWriteLock contentLock; private final ReentrantReadWriteLock attributesLock; /** The repository. */ private final Repository repository; /** The path. */ private final String path; protected DefaultRepositoryItemUid( RepositoryItemUidFactory factory, Repository repository, String path ) { super(); this.factory = factory; this.contentLock = new ReentrantReadWriteLock(); this.attributesLock = new ReentrantReadWriteLock(); this.repository = repository; this.path = path; } public RepositoryItemUidFactory getRepositoryItemUidFactory() { return factory; } @Override public Repository getRepository() { return repository; } @Override public String getPath() { return path; } @Override public void lock( Action action ) { doLock( action, getLockKey(), contentLock ); } @Override public void unlock() { doUnlock( null, getLockKey(), contentLock ); } @Override public void lockAttributes( Action action ) { doLock( action, getAttributeLockKey(), attributesLock ); } @Override public void unlockAttributes() { doUnlock( null, getAttributeLockKey(), attributesLock ); } @Override public <A extends Attribute<?>> A getAttribute( Class<A> attrClass ) { return getRepository().getRepositoryItemUidAttributeManager().getAttribute( attrClass, this ); } @Override public <A extends Attribute<V>, V> V getAttributeValue( Class<A> attrClass ) { A attr = getAttribute( attrClass ); if ( attr != null ) { return attr.getValueFor( this ); } else { return null; } } @Override public <A extends Attribute<Boolean>> boolean getBooleanAttributeValue( Class<A> attr ) { Boolean bool = getAttributeValue( attr ); if ( bool != null && bool.booleanValue() ) { return true; } else { return false; } } /** * toString() will return a "string representation" of this UID in form of repoId + ":" + path */ @Override public String toString() { return getRepository().getId() + ":" + getPath(); } public String toDebugString() { return getRepository().getId() + ":" + getPath() + " (" + super.toString() + ")"; } // == protected LockStep getLastStep( String lockKey ) { Map<String, Stack<LockStep>> threadMap = threadCtx.get(); if ( !threadMap.containsKey( lockKey ) ) { return null; } else { try { return threadMap.get( lockKey ).peek(); } catch ( EmptyStackException e ) { return null; } } } protected void putLastStep( String lockKey, LockStep lock ) { Map<String, Stack<LockStep>> threadMap = threadCtx.get(); if ( lock != null ) { if ( !threadMap.containsKey( lockKey ) ) { threadMap.put( lockKey, new Stack<LockStep>() ); } threadMap.get( lockKey ).push( lock ); } else { Stack<LockStep> stack = threadMap.get( lockKey ); stack.pop(); // cleanup if stack is empty if ( stack.isEmpty() ) { threadMap.remove( lockKey ); } } } protected void doLock( Action action, String lockKey, ReentrantReadWriteLock rwLock ) { // we always go from "weaker" to "stronger" lock (read is shared, while write is exclusive lock) // because of Nexus nature (wrong nature?), the calls are heavily boxed, hence a thread once acquired write // may re-lock with read action. In this case, we keep the write lock, since it is "stronger" // The proper downgrade of locks happens in unlock method, while unraveling the stack of locking steps. LockStep step = getLastStep( lockKey ); if ( step != null && step.isReadLockLastLocked() && !action.isReadAction() ) { // we need lock upgrade (r->w) // java5+ does not supports this direction, do it "risky" way try { getActionLock( rwLock, true ).unlock(); } catch ( IllegalMonitorStateException e ) { // increasing the details level IllegalMonitorStateException ie = new IllegalMonitorStateException( "Unable to upgrade lock for: '" + lockKey + "' on " + this.toString() + " caused by: " + e.getMessage() ); ie.initCause( e ); throw ie; } getActionLock( rwLock, false ).lock(); step = LockStep.READ_WRITE_UPGRADE; } else if ( step == null ) { // just lock it, this is first timer getActionLock( rwLock, action.isReadAction() ).lock(); step = action.isReadAction() ? LockStep.READ : LockStep.WRITE; } else { // not a first timer (we already have some lock) and no lock upgrade needed, hence we have the proper lock // already, just do nothing but note the fact in Stack // just DO NOT lock it, we already own the needed lock, and upgrade/dowgrade // becomes unmaneagable if we have reentrant locks! // // example code (the call tree actually, this code may be in multiple, even recursive calls): // // lock(read); // ... // lock(read); // ... // lock(write); <- This call will stumble and lockup on itself, see above about upgrade // ... // ... // unlock(); // ... // unlock(); // ... // unlock(); step = step.getFollowingLockstep( action ); } putLastStep( lockKey, step ); } protected void doUnlock( Action action, String lockKey, ReentrantReadWriteLock rwLock ) { LockStep step = getLastStep( lockKey ); if ( step == null ) { // this is error here throw new IllegalMonitorStateException( "UID \"" + toString() + "\" was tried to be unlocked but had no step-history..." ); } if ( step.isMustAct() ) { if ( LockStep.READ.equals( step ) ) { getActionLock( rwLock, true ).unlock(); } else if ( LockStep.WRITE.equals( step ) ) { getActionLock( rwLock, false ).unlock(); } else if ( LockStep.READ_WRITE_UPGRADE.equals( step ) ) { // now we need to downgrade (w->r) // java5+ supports this direction, do it by the "book" getActionLock( rwLock, true ).lock(); getActionLock( rwLock, false ).unlock(); } } putLastStep( lockKey, null ); } private Lock getActionLock( ReadWriteLock rwLock, boolean isReadAction ) { if ( isReadAction ) { return rwLock.readLock(); } else { return rwLock.writeLock(); } } private String getLockKey() { return toString() + " : itemlock"; } private String getAttributeLockKey() { return toString() + " : attrlock"; } // for Debug/tests vvv protected ReentrantReadWriteLock getContentLock() { return contentLock; } protected ReentrantReadWriteLock getAttributesLock() { return attributesLock; } // for Debug/tests ^^^ }