Skip to content

Commit

Permalink
Merge pull request #1353 from b2ihealthcare/issue/SO-6268-index-neste…
Browse files Browse the repository at this point in the history
…d-lock-contexts

Add nested context descriptions to lock documents
  • Loading branch information
cmark authored Dec 9, 2024
2 parents cf47866 + 13be6a6 commit a07e43d
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
*/
package com.b2international.snowowl.core.locks;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import java.util.List;

import org.junit.Before;
import org.junit.Test;
Expand All @@ -35,84 +38,103 @@
*/
public class DatastoreLockTests {

private static final long TIMEOUT = 10_000L;
private static final String USER1 = "[email protected]";
private static final String USER2 = "[email protected]";

private static final DatastoreLockContext CONTEXT_GRANTED = new DatastoreLockContext(
USER1,
DatastoreLockContextDescriptions.MAINTENANCE
);

private static final DatastoreLockContext CONTEXT_DIFFERENT_USER = new DatastoreLockContext(
USER2,
DatastoreLockContextDescriptions.MAINTENANCE
);

private static final DatastoreLockContext CONTEXT_NESTED = new DatastoreLockContext(
USER1,
DatastoreLockContextDescriptions.COMMIT,
DatastoreLockContextDescriptions.MAINTENANCE
);

private static final Lockable TARGET_ALL = Lockable.ALL;
private static final Lockable TARGET_REPOSITORY = new Lockable("snomed", null);
private static final Lockable TARGET_REPOSITORY_BRANCH = new Lockable("snomed", "MAIN/a/b");

private static final String USER = "snowowl";
private static final long TIMEOUT = 100L;

private DefaultOperationLockManager manager;

@Before
public void setup() {
final ObjectMapper mapper = JsonSupport.getDefaultObjectMapper();
final Index index = Indexes.createIndex("locks", mapper, new Mappings(DatastoreLockIndexEntry.class));

manager = new DefaultOperationLockManager(index);
manager.addLockTargetListener(new Slf4jOperationLockTargetListener());
manager.unlockAll();
}

@Test
public void testLockAll() {
final DatastoreLockContext context = createContext(USER, DatastoreLockContextDescriptions.MAINTENANCE);
final Lockable allLockTarget = Lockable.ALL;
private void testLock(Lockable target) {
// Take the lock
manager.lock(CONTEXT_GRANTED, TIMEOUT, target);
checkIfLockExists(CONTEXT_GRANTED, true, target);

manager.lock(context, TIMEOUT, allLockTarget);
checkIfLockExists(context, true, allLockTarget);
manager.unlockAll();
}

@Test
public void testUnlock() {
final DatastoreLockContext context = createContext(USER, DatastoreLockContextDescriptions.MAINTENANCE);
final Lockable target = new Lockable("snomedStore", "MAIN");
// Different user, same target is rejected
assertThrows(LockedException.class, () -> manager.lock(CONTEXT_DIFFERENT_USER, TIMEOUT, target));
checkIfLockExists(CONTEXT_DIFFERENT_USER, false, target);

manager.lock(context, TIMEOUT, target);
checkIfLockExists(context, true, target);

manager.unlock(context, target);
checkIfLockExists(context, false, target);
}

@Test
public void testUnlockAll() {
final DatastoreLockContext context = createContext(USER, DatastoreLockContextDescriptions.MAINTENANCE);
final Lockable target1 = new Lockable("snomedStore", "MAIN");
final Lockable target2 = new Lockable("loincStore", "MAIN");
// Same user, improper nesting (same parent description) is also rejected
assertThrows(LockedException.class, () -> manager.lock(CONTEXT_GRANTED, TIMEOUT, target));

manager.lock(context, TIMEOUT, target1, target2);
checkIfLockExists(context, true, target1, target2);
// Same user, proper nesting, same target is allowed
manager.lock(CONTEXT_NESTED, TIMEOUT, TARGET_ALL);
checkIfLockExists(CONTEXT_NESTED, true, TARGET_ALL);
manager.unlock(CONTEXT_NESTED, TARGET_ALL);
checkIfLockExists(CONTEXT_NESTED, false, TARGET_ALL);

manager.unlockAll();
checkIfLockExists(context, false, target1, target2);
// Same user, proper nesting, repository target is also allowed
manager.lock(CONTEXT_NESTED, TIMEOUT, TARGET_REPOSITORY);
checkIfLockExists(CONTEXT_NESTED, true, TARGET_REPOSITORY);
manager.unlock(CONTEXT_NESTED, TARGET_REPOSITORY);
checkIfLockExists(CONTEXT_NESTED, false, TARGET_REPOSITORY);

// Same user, proper nesting, repository + branch target is, yet again, allowed
manager.lock(CONTEXT_NESTED, TIMEOUT, TARGET_REPOSITORY_BRANCH);
checkIfLockExists(CONTEXT_NESTED, true, TARGET_REPOSITORY_BRANCH);
manager.unlock(CONTEXT_NESTED, TARGET_REPOSITORY_BRANCH);
checkIfLockExists(CONTEXT_NESTED, false, TARGET_REPOSITORY_BRANCH);

// Release the first lock
manager.unlock(CONTEXT_GRANTED, target);
checkIfLockExists(CONTEXT_GRANTED, false, target);
}

@Test(expected = LockedException.class)
public void testLockAllNotAbleToLockAnother() {
final DatastoreLockContext context = createContext(USER, DatastoreLockContextDescriptions.MAINTENANCE);
final Lockable allLockTarget = Lockable.ALL;

manager.lock(context, 1_000L, allLockTarget);
manager.lock(context, 1_000L, allLockTarget);
@Test
public void testLockAll() {
testLock(TARGET_ALL);
}

@Test
public void testLockBranchAndRepository() {
final DatastoreLockContext context = createContext(USER, DatastoreLockContextDescriptions.CREATE_VERSION);
final Lockable target = new Lockable("snomedStore", "MAIN");
manager.lock(context, 10_000L, target);
checkIfLockExists(context, true, target);
public void testLockRepository() {
testLock(TARGET_REPOSITORY);
}

private DatastoreLockContext createContext(final String user, final String description) {
return new DatastoreLockContext(user, description);
@Test
public void testLockRepositoryBranch() {
testLock(TARGET_REPOSITORY_BRANCH);
}

private void checkIfLockExists(DatastoreLockContext context, boolean expected, Lockable...targets) {
for (int i = 0; i < targets.length; i++) {
final Lockable target = targets[i];
final OperationLock operationLock = new OperationLock(i, target);
operationLock.acquire(context);
final OperationLockInfo info = new OperationLockInfo(i, operationLock.getLevel(), operationLock.getCreationDate(), target, context);
assertTrue(expected == manager.getLocks().contains(info));
private void checkIfLockExists(DatastoreLockContext context, boolean expected, Lockable...targets) {
final List<OperationLockInfo> locks = manager.getLocks();

for (final Lockable target : targets) {
final boolean lockExists = locks.stream()
.filter(info -> info.getTarget().equals(target) && info.getContext().equals(context))
.findFirst()
.isPresent();

assertEquals(expected, lockExists);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2023 B2i Healthcare, https://b2ihealthcare.com
* Copyright 2019-2024 B2i Healthcare, https://b2ihealthcare.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

import java.lang.reflect.Field;

Expand All @@ -39,6 +40,7 @@
public class LockIndexTests {

private static final String USER = "[email protected]";

private Index index;
private ObjectMapper mapper;

Expand All @@ -48,48 +50,101 @@ public void setup() {
index = Indexes.createIndex("locks", mapper, new Mappings(DatastoreLockIndexEntry.class));
index.admin().create();
}

@Test
public void indexLockEntry() {
final String lockId = "1";

final DatastoreLockIndexEntry lock = DatastoreLockIndexEntry.builder()
.id(lockId)
.userId(USER)
.description(DatastoreLockContextDescriptions.CLASSIFY)
.addContext(DatastoreLockContextDescriptions.CLASSIFY)
.repositoryId("repositoryUuid")
.branchPath("branchPath")
.build();

indexDocument(lock);
final DatastoreLockIndexEntry actual = get(lockId);

final DatastoreLockIndexEntry actual = getDocument(lockId);
assertDocEquals(lock, actual);
}

private void indexDocument(DatastoreLockIndexEntry doc) {

@Test
public void updateLockEntry() {
final String lockId = "2";

final DatastoreLockIndexEntry lock = DatastoreLockIndexEntry.builder()
.id(lockId)
.userId(USER)
.addContext(DatastoreLockContextDescriptions.CREATE_VERSION)
.repositoryId("repositoryUuid")
.branchPath("branchPath")
.build();

indexDocument(lock);

final DatastoreLockIndexEntry updatedLock = DatastoreLockIndexEntry.from(lock)
.addContext(DatastoreLockContextDescriptions.COMMIT)
.build();

indexDocument(updatedLock);

final DatastoreLockIndexEntry actual = getDocument(lockId);
assertDocEquals(updatedLock, actual);
}

@Test
public void deleteLockEntry() {
final String lockId = "3";

final DatastoreLockIndexEntry lock = DatastoreLockIndexEntry.builder()
.id(lockId)
.userId(USER)
.addContext(DatastoreLockContextDescriptions.CREATE_VERSION)
.repositoryId("repositoryUuid")
.branchPath("branchPath")
.build();

indexDocument(lock);
deleteDocument(lockId);

assertNull(getDocument(lockId));
}

private void indexDocument(final DatastoreLockIndexEntry doc) {
index.write(writer -> {
writer.put(doc);
writer.commit();

return null;
});
}
private DatastoreLockIndexEntry get(final String lockId) {

private DatastoreLockIndexEntry getDocument(final String lockId) {
return index.read(searcher -> searcher.get(DatastoreLockIndexEntry.class, lockId));
}

private void assertDocEquals(DatastoreLockIndexEntry expected, DatastoreLockIndexEntry actual) {

private void deleteDocument(final String id) {
index.write(writer -> {
writer.remove(DatastoreLockIndexEntry.class, id);
writer.commit();
return null;
});
}

private void assertDocEquals(final DatastoreLockIndexEntry expected, final DatastoreLockIndexEntry actual) {
assertNotNull("Actual document is missing from index", actual);
for (Field f : index.admin().getIndexMapping().getMapping(expected.getClass()).getFields()) {

for (final Field f : index.admin().getIndexMapping().getMapping(expected.getClass()).getFields()) {
if (Revision.Fields.CREATED.equals(f.getName())
|| Revision.Fields.REVISED.equals(f.getName())
|| WithScore.SCORE.equals(f.getName())
) {
|| Revision.Fields.REVISED.equals(f.getName())
|| WithScore.SCORE.equals(f.getName())
) {
// skip revision fields from equality check
continue;
}

assertEquals(String.format("Field '%s' should be equal", f.getName()), Reflections.getValue(expected, f), Reflections.getValue(actual, f));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
*/
package com.b2international.snowowl.core.locks;

import java.util.ArrayDeque;
import java.util.Collection;
import static com.google.common.collect.Lists.newArrayList;

import java.util.Date;
import java.util.Deque;
import java.util.List;

import com.b2international.snowowl.core.internal.locks.DatastoreLockContext;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

/**
* An abstract lock implementation which supports basic methods of {@link IOperationLock}.
Expand All @@ -34,29 +35,32 @@ public abstract class AbstractOperationLock implements IOperationLock {
private static final String CONTEXT_NOT_IN_STACK_MESSAGE = "Context is not registered as a lock owner.";

private final int id;
private final Date creationDate = new Date();
private final Deque<DatastoreLockContext> contextStack = new ArrayDeque<DatastoreLockContext>();
private final Date creationDate;
private final List<DatastoreLockContext> contexts = newArrayList();
private final Lockable target;

/**
* Creates a new abstract lock instance.
*
* @param id the lock identifier
* @param creationDate the lock creation date (may not be {@code null})
* @param target the lock target (may not be {@code null})
*/
protected AbstractOperationLock(final int id, final Lockable target) {
protected AbstractOperationLock(final int id, final Date creationDate, final Lockable target) {
Preconditions.checkNotNull(target, "Lock target may not be null.");
Preconditions.checkNotNull(creationDate, "Creation date may not be null.");

this.id = id;
this.creationDate = creationDate;
this.target = target;
}

private void pushContext(DatastoreLockContext context) {
contextStack.push(context);
contexts.add(context);
}

private void removeContext(DatastoreLockContext otherContext) {
if (!contextStack.remove(otherContext)) {
if (!contexts.remove(otherContext)) {
throw new IllegalArgumentException(CONTEXT_NOT_IN_STACK_MESSAGE);
}
}
Expand All @@ -73,12 +77,12 @@ public Date getCreationDate() {

@Override
public DatastoreLockContext getContext() {
return contextStack.peek();
return Iterables.getLast(contexts, null);
}

@Override
public Collection<DatastoreLockContext> getAllContexts() {
return ImmutableList.copyOf(contextStack);
public List<DatastoreLockContext> getAllContexts() {
return ImmutableList.copyOf(contexts);
}

@Override
Expand Down
Loading

0 comments on commit a07e43d

Please sign in to comment.