[Bugfix] fixed not unlocked objects after TX
When a TX is closed, all locked objects are unlocked. The but originated from multiple commands being performed for the same object, thus the object was locked multiple times i.e. the lock counter was >1. Now added a releaseLock() method which is called by the TX when the TX is closed so that the lock counter is really 0 and the lock is released. - Added tests for this situation - documented the LockHandler
This commit is contained in:
parent
f236779697
commit
e44775f30b
|
@ -1 +1 @@
|
||||||
Subproject commit a67df72f3f9b795796896889c2dbe1cf8561ccd2
|
Subproject commit cde6eb652ec2c12ce22c8cb21a16589d56f8a49f
|
|
@ -15,14 +15,67 @@
|
||||||
*/
|
*/
|
||||||
package li.strolch.agent.api;
|
package li.strolch.agent.api;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
|
import li.strolch.agent.impl.DefaultLockHandler;
|
||||||
|
import li.strolch.model.Locator;
|
||||||
import li.strolch.model.StrolchRootElement;
|
import li.strolch.model.StrolchRootElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* <p>
|
||||||
|
* In Strolch locking of objects is done by keeping a lock for every {@link StrolchRootElement} by using the
|
||||||
|
* {@link Locator} to find the lock. Instead of adding a lock to the model, the lock is stored by the
|
||||||
|
* {@link LockHandler}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Since new {@link StrolchRootElement} might not be known by the {@link ElementMap ElementMaps} but you still want to
|
||||||
|
* lock an object globally, then locking on the {@link Locator} solves this, as the locator is an immutable object and
|
||||||
|
* can easily be created before the actual object exists
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* See concrete implementations for which concrete locking implementation is used
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see DefaultLockHandler
|
||||||
|
*
|
||||||
* @author Robert von Burg <eitch@eitchnet.ch>
|
* @author Robert von Burg <eitch@eitchnet.ch>
|
||||||
*/
|
*/
|
||||||
public interface LockHandler {
|
public interface LockHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the given element by using the element's {@link Locator} and creating a lock on it. Calling lock multiple
|
||||||
|
* times from the same thread will not lock, it is up to the concrete implementation to define if a lock counter is
|
||||||
|
* used
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
* the element for which a {@link Lock} on its {@link Locator} is to be created and/or locked
|
||||||
|
*/
|
||||||
public void lock(StrolchRootElement element);
|
public void lock(StrolchRootElement element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Unlocks the given element by finding the element's lock by its {@link Locator}. It is up to the concrete
|
||||||
|
* implementation to define if unlocking an unlocked element will fail or not. This method might not completely
|
||||||
|
* unlock the element if a lock counter is used and the object was locked multiple times.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If the lock must be completely released, then use {@link #releaseLock(StrolchRootElement)}
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
* the element for which the current/last {@link Lock} is to be unlocked
|
||||||
|
*/
|
||||||
public void unlock(StrolchRootElement element);
|
public void unlock(StrolchRootElement element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases the lock on the given element, by unlocking all locks, i.e. after this method is called, no lock will be
|
||||||
|
* held anymore by the current thread
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
* the element for which the {@link Lock} on the {@link Locator} is to be released
|
||||||
|
*/
|
||||||
|
public void releaseLock(StrolchRootElement element);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ public interface StrolchRealm {
|
||||||
|
|
||||||
public void unlock(StrolchRootElement lockedElement);
|
public void unlock(StrolchRootElement lockedElement);
|
||||||
|
|
||||||
|
public void releaseLock(StrolchRootElement lockedElement);
|
||||||
|
|
||||||
public DataStoreMode getMode();
|
public DataStoreMode getMode();
|
||||||
|
|
||||||
public StrolchTransaction openTx(Certificate certificate, Class<?> clazz);
|
public StrolchTransaction openTx(Certificate certificate, Class<?> clazz);
|
||||||
|
|
|
@ -68,7 +68,7 @@ public class DefaultLockHandler implements LockHandler {
|
||||||
this.lockMap.put(locator, lock);
|
this.lockMap.put(locator, lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock(this.tryLockTimeUnit, this.tryLockTime, lock);
|
lock(this.tryLockTimeUnit, this.tryLockTime, lock, element);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -82,15 +82,26 @@ public class DefaultLockHandler implements LockHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseLock(StrolchRootElement element) {
|
||||||
|
Locator locator = element.getLocator();
|
||||||
|
ReentrantLock lock = this.lockMap.get(locator);
|
||||||
|
if (lock == null || !lock.isHeldByCurrentThread()) {
|
||||||
|
logger.error(MessageFormat.format("Trying to unlock not locked element {0}", locator)); //$NON-NLS-1$
|
||||||
|
} else {
|
||||||
|
releaseLock(lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see java.util.concurrent.locks.ReentrantLock#tryLock(long, TimeUnit)
|
* @see java.util.concurrent.locks.ReentrantLock#tryLock(long, TimeUnit)
|
||||||
*/
|
*/
|
||||||
private void lock(TimeUnit timeUnit, long tryLockTime, ReentrantLock lock) {
|
private void lock(TimeUnit timeUnit, long tryLockTime, ReentrantLock lock, StrolchRootElement element) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (!lock.tryLock(tryLockTime, timeUnit)) {
|
if (!lock.tryLock(tryLockTime, timeUnit)) {
|
||||||
String msg = "Failed to acquire lock after {0}s for {1}"; //$NON-NLS-1$
|
String msg = "Failed to acquire lock after {0}s for {1}"; //$NON-NLS-1$
|
||||||
msg = MessageFormat.format(msg, timeUnit.toSeconds(tryLockTime), toString());
|
msg = MessageFormat.format(msg, timeUnit.toSeconds(tryLockTime), element.getLocator());
|
||||||
throw new StrolchException(msg);
|
throw new StrolchException(msg);
|
||||||
}
|
}
|
||||||
if (logger.isDebugEnabled())
|
if (logger.isDebugEnabled())
|
||||||
|
@ -108,4 +119,13 @@ public class DefaultLockHandler implements LockHandler {
|
||||||
if (logger.isDebugEnabled())
|
if (logger.isDebugEnabled())
|
||||||
logger.debug("unlocking " + toString()); //$NON-NLS-1$
|
logger.debug("unlocking " + toString()); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see java.util.concurrent.locks.ReentrantLock#unlock()
|
||||||
|
*/
|
||||||
|
private void releaseLock(ReentrantLock lock) {
|
||||||
|
while (lock.isHeldByCurrentThread() && lock.isLocked()) {
|
||||||
|
unlock(lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,13 +77,17 @@ public abstract class InternalStrolchRealm implements StrolchRealm {
|
||||||
this.lockHandler.unlock(lockedElement);
|
this.lockHandler.unlock(lockedElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseLock(StrolchRootElement lockedElement) {
|
||||||
|
this.lockHandler.releaseLock(lockedElement);
|
||||||
|
}
|
||||||
|
|
||||||
public void initialize(ComponentContainer container, ComponentConfiguration configuration) {
|
public void initialize(ComponentContainer container, ComponentConfiguration configuration) {
|
||||||
|
|
||||||
String propTryLockTimeUnit = StrolchConstants.makeRealmKey(this.realm, PROP_TRY_LOCK_TIME_UNIT);
|
String propTryLockTimeUnit = StrolchConstants.makeRealmKey(this.realm, PROP_TRY_LOCK_TIME_UNIT);
|
||||||
String propTryLockTime = StrolchConstants.makeRealmKey(this.realm, PROP_TRY_LOCK_TIME);
|
String propTryLockTime = StrolchConstants.makeRealmKey(this.realm, PROP_TRY_LOCK_TIME);
|
||||||
|
|
||||||
String enableAuditKey = StrolchConstants.makeRealmKey(getRealm(),
|
String enableAuditKey = StrolchConstants.makeRealmKey(getRealm(), DefaultRealmHandler.PROP_ENABLE_AUDIT_TRAIL);
|
||||||
DefaultRealmHandler.PROP_ENABLE_AUDIT_TRAIL);
|
|
||||||
this.auditTrailEnabled = configuration.getBoolean(enableAuditKey, Boolean.FALSE);
|
this.auditTrailEnabled = configuration.getBoolean(enableAuditKey, Boolean.FALSE);
|
||||||
|
|
||||||
String enableAuditForReadKey = StrolchConstants.makeRealmKey(getRealm(),
|
String enableAuditForReadKey = StrolchConstants.makeRealmKey(getRealm(),
|
||||||
|
|
|
@ -199,9 +199,9 @@ public abstract class AbstractTransaction implements StrolchTransaction {
|
||||||
this.lockedElements.remove(element);
|
this.lockedElements.remove(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unlockElements() {
|
private void releaseElementLocks() {
|
||||||
for (StrolchRootElement lockedElement : this.lockedElements) {
|
for (StrolchRootElement lockedElement : this.lockedElements) {
|
||||||
this.realm.unlock(lockedElement);
|
this.realm.releaseLock(lockedElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -472,7 +472,7 @@ public abstract class AbstractTransaction implements StrolchTransaction {
|
||||||
throw new StrolchPersistenceException(msg, e);
|
throw new StrolchPersistenceException(msg, e);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
unlockElements();
|
releaseElementLocks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -487,7 +487,7 @@ public abstract class AbstractTransaction implements StrolchTransaction {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleFailure(start, e);
|
handleFailure(start, e);
|
||||||
} finally {
|
} finally {
|
||||||
unlockElements();
|
releaseElementLocks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,19 +510,23 @@ public abstract class AbstractTransaction implements StrolchTransaction {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("TX user=");
|
sb.append("TX user=");
|
||||||
sb.append(this.certificate.getUsername());
|
sb.append(this.certificate.getUsername());
|
||||||
|
sb.append(", action=");
|
||||||
|
sb.append(this.action);
|
||||||
sb.append(", realm="); //$NON-NLS-1$
|
sb.append(", realm="); //$NON-NLS-1$
|
||||||
sb.append(getRealmName());
|
sb.append(getRealmName());
|
||||||
sb.append(", took="); //$NON-NLS-1$
|
sb.append(", took="); //$NON-NLS-1$
|
||||||
sb.append(StringHelper.formatNanoDuration(txDuration));
|
sb.append(StringHelper.formatNanoDuration(txDuration));
|
||||||
sb.append(", close="); //$NON-NLS-1$
|
if (closeDuration >= 100000000L) {
|
||||||
sb.append(StringHelper.formatNanoDuration(closeDuration));
|
sb.append(", close="); //$NON-NLS-1$
|
||||||
|
sb.append(StringHelper.formatNanoDuration(closeDuration));
|
||||||
if (isAuditTrailEnabled()) {
|
}
|
||||||
|
|
||||||
|
if (isAuditTrailEnabled() && auditTrailDuration >= 100000000L) {
|
||||||
sb.append(", auditTrail="); //$NON-NLS-1$
|
sb.append(", auditTrail="); //$NON-NLS-1$
|
||||||
sb.append(StringHelper.formatNanoDuration(auditTrailDuration));
|
sb.append(StringHelper.formatNanoDuration(auditTrailDuration));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isObserverUpdatesEnabled()) {
|
if (isObserverUpdatesEnabled() && observerUpdateDuration >= 100000000L) {
|
||||||
sb.append(", updates="); //$NON-NLS-1$
|
sb.append(", updates="); //$NON-NLS-1$
|
||||||
sb.append(StringHelper.formatNanoDuration(observerUpdateDuration));
|
sb.append(StringHelper.formatNanoDuration(observerUpdateDuration));
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class DefaultStrolchSessionHandler extends StrolchComponent implements St
|
||||||
this.privilegeHandler = getContainer().getComponent(PrivilegeHandler.class);
|
this.privilegeHandler = getContainer().getComponent(PrivilegeHandler.class);
|
||||||
this.certificateMap = new HashMap<>();
|
this.certificateMap = new HashMap<>();
|
||||||
|
|
||||||
this.sessionTimeoutTimer = new Timer("SessionTimeoutTimer"); //$NON-NLS-1$
|
this.sessionTimeoutTimer = new Timer("SessionTimeoutTimer", true); //$NON-NLS-1$
|
||||||
long checkInterval = TimeUnit.MINUTES.toMillis(1);
|
long checkInterval = TimeUnit.MINUTES.toMillis(1);
|
||||||
this.sessionTimeoutTimer.schedule(new SessionTimeoutTask(), checkInterval, checkInterval);
|
this.sessionTimeoutTimer.schedule(new SessionTimeoutTask(), checkInterval, checkInterval);
|
||||||
|
|
||||||
|
|
|
@ -132,8 +132,8 @@ public class RestfulStrolchComponent extends StrolchComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// restful logging and tracing
|
// restful logging and tracing
|
||||||
this.restLogging = configuration.getBoolean(PARAM_REST_LOGGING, false);
|
this.restLogging = configuration.getBoolean(PARAM_REST_LOGGING, Boolean.FALSE);
|
||||||
this.restLoggingEntity = configuration.getBoolean(PARAM_REST_LOGGING_ENTITY, false);
|
this.restLoggingEntity = configuration.getBoolean(PARAM_REST_LOGGING_ENTITY, Boolean.FALSE);
|
||||||
this.restTracing = configuration.getString(PARAM_REST_TRACING, "OFF"); //$NON-NLS-1$
|
this.restTracing = configuration.getString(PARAM_REST_TRACING, "OFF"); //$NON-NLS-1$
|
||||||
this.restTracingThreshold = configuration.getString(PARAM_REST_TRACING_THRESHOLD, "TRACE"); //$NON-NLS-1$
|
this.restTracingThreshold = configuration.getString(PARAM_REST_TRACING_THRESHOLD, "TRACE"); //$NON-NLS-1$
|
||||||
|
|
||||||
|
|
|
@ -121,33 +121,32 @@ public class AuthenticationService {
|
||||||
@DELETE
|
@DELETE
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("{authToken}")
|
@Path("{sessionId}")
|
||||||
public Response logout(@PathParam("authToken") String authToken) {
|
public Response logout(@PathParam("sessionId") String sessionId) {
|
||||||
|
|
||||||
LogoutResult logoutResult = new LogoutResult();
|
LogoutResult logoutResult = new LogoutResult();
|
||||||
|
|
||||||
GenericEntity<LogoutResult> entity = new GenericEntity<LogoutResult>(logoutResult, LogoutResult.class) {
|
|
||||||
//
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
StrolchSessionHandler sessionHandlerHandler = RestfulStrolchComponent.getInstance().getComponent(
|
StrolchSessionHandler sessionHandlerHandler = RestfulStrolchComponent.getInstance().getComponent(
|
||||||
StrolchSessionHandler.class);
|
StrolchSessionHandler.class);
|
||||||
Certificate certificate = sessionHandlerHandler.validate(authToken);
|
Certificate certificate = sessionHandlerHandler.validate(sessionId);
|
||||||
sessionHandlerHandler.invalidateSession(certificate);
|
sessionHandlerHandler.invalidateSession(certificate);
|
||||||
|
|
||||||
|
logoutResult.setUsername(certificate.getUsername());
|
||||||
|
logoutResult.setSessionId(sessionId);
|
||||||
logoutResult.setMsg(MessageFormat.format("{0} has been logged out.", certificate.getUsername())); //$NON-NLS-1$
|
logoutResult.setMsg(MessageFormat.format("{0} has been logged out.", certificate.getUsername())); //$NON-NLS-1$
|
||||||
return Response.ok().entity(entity).build();
|
return Response.ok().entity(logoutResult).build();
|
||||||
|
|
||||||
} catch (StrolchException | PrivilegeException e) {
|
} catch (StrolchException | PrivilegeException e) {
|
||||||
logger.error(e.getMessage(), e);
|
logger.error(e.getMessage(), e);
|
||||||
logoutResult.setMsg(MessageFormat.format("Could not logout due to: {0}", e.getMessage())); //$NON-NLS-1$
|
logoutResult.setMsg(MessageFormat.format("Could not logout due to: {0}", e.getMessage())); //$NON-NLS-1$
|
||||||
return Response.status(Status.UNAUTHORIZED).entity(entity).build();
|
return Response.status(Status.UNAUTHORIZED).entity(logoutResult).build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error(e.getMessage(), e);
|
logger.error(e.getMessage(), e);
|
||||||
String msg = e.getMessage();
|
String msg = e.getMessage();
|
||||||
logoutResult.setMsg(MessageFormat.format("{0}: {1}", e.getClass().getName(), msg)); //$NON-NLS-1$
|
logoutResult.setMsg(MessageFormat.format("{0}: {1}", e.getClass().getName(), msg)); //$NON-NLS-1$
|
||||||
return Response.serverError().entity(entity).build();
|
return Response.serverError().entity(logoutResult).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,13 @@ public class LockingTest {
|
||||||
runners.add(new LockingRunner(svc, arg));
|
runners.add(new LockingRunner(svc, arg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runRunners(runners);
|
||||||
|
|
||||||
|
// now assert that we can perform another such service, thus validating that the resource is not locked any longer
|
||||||
|
doLockServiceTest(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runRunners(List<LockingRunner> runners) throws InterruptedException {
|
||||||
this.run = false;
|
this.run = false;
|
||||||
for (LockingRunner lockingRunner : runners) {
|
for (LockingRunner lockingRunner : runners) {
|
||||||
lockingRunner.start();
|
lockingRunner.start();
|
||||||
|
@ -112,11 +119,30 @@ public class LockingTest {
|
||||||
assertEquals(ServiceResultState.FAILED, result.getState());
|
assertEquals(ServiceResultState.FAILED, result.getState());
|
||||||
assertThat(result.getMessage(), containsString("Failed to acquire lock after"));
|
assertThat(result.getMessage(), containsString("Failed to acquire lock after"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUnlockCompletelyOnMultipleLock() throws InterruptedException {
|
||||||
|
|
||||||
|
List<LockingRunner> runners = new ArrayList<>();
|
||||||
|
|
||||||
// now assert that we can perform another such service, thus validating that the resource is not locked any longer
|
|
||||||
LockingServiceTest svc = new LockingServiceTest();
|
LockingServiceTest svc = new LockingServiceTest();
|
||||||
LockingArgumentTest arg = new LockingArgumentTest();
|
LockingArgumentTest arg = new LockingArgumentTest();
|
||||||
arg.longRunning = false;
|
arg.longRunning = false;
|
||||||
|
arg.nrOfLocks = 5;
|
||||||
|
arg.resourceLoc = Locator.valueOf(RESOURCE_LOCATOR);
|
||||||
|
runners.add(new LockingRunner(svc, arg));
|
||||||
|
|
||||||
|
runRunners(runners);
|
||||||
|
|
||||||
|
// now assert that we can perform another such service, thus validating that the resource is not locked any longer
|
||||||
|
doLockServiceTest(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doLockServiceTest(boolean longRunning) {
|
||||||
|
LockingServiceTest svc = new LockingServiceTest();
|
||||||
|
LockingArgumentTest arg = new LockingArgumentTest();
|
||||||
|
arg.longRunning = longRunning;
|
||||||
arg.resourceLoc = Locator.valueOf(RESOURCE_LOCATOR);
|
arg.resourceLoc = Locator.valueOf(RESOURCE_LOCATOR);
|
||||||
ServiceResult result = getServiceHandler().doService(login(), svc, arg);
|
ServiceResult result = getServiceHandler().doService(login(), svc, arg);
|
||||||
assertEquals(ServiceResultState.SUCCESS, result.getState());
|
assertEquals(ServiceResultState.SUCCESS, result.getState());
|
||||||
|
@ -157,6 +183,7 @@ public class LockingTest {
|
||||||
private static class LockingArgumentTest extends ServiceArgument {
|
private static class LockingArgumentTest extends ServiceArgument {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
public boolean longRunning;
|
public boolean longRunning;
|
||||||
|
public int nrOfLocks = 1;
|
||||||
public Locator resourceLoc;
|
public Locator resourceLoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +204,8 @@ public class LockingTest {
|
||||||
Thread.sleep(200l);
|
Thread.sleep(200l);
|
||||||
|
|
||||||
Resource res = tx.findElement(arg.resourceLoc);
|
Resource res = tx.findElement(arg.resourceLoc);
|
||||||
tx.lock(res);
|
for (int i = 0; i < arg.nrOfLocks; i++)
|
||||||
|
tx.lock(res);
|
||||||
|
|
||||||
if (arg.longRunning)
|
if (arg.longRunning)
|
||||||
Thread.sleep(5000l);
|
Thread.sleep(5000l);
|
||||||
|
|
Loading…
Reference in New Issue