Skip to content

Commit

Permalink
Merge pull request #1156 from b2ihealthcare/feature/generic-uniquenes…
Browse files Browse the repository at this point in the history
…s-presence-tx-checks

feat(core): add generic ID uniqueness and presence check step to each...
  • Loading branch information
cmark authored May 9, 2023
2 parents 15b9115 + 50b5ccd commit 11a0f9d
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 B2i Healthcare Pte Ltd, http://b2i.sg
* Copyright 2021-2023 B2i Healthcare Pte Ltd, http://b2i.sg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -132,4 +132,24 @@ public String parentLock() {
return getDelegate().parentLock();
}

@Override
public <T extends Revision> void ensurePresent(Class<T> documentType, String id) {
getDelegate().ensurePresent(documentType, id);
}

@Override
public <T extends Revision> void ensurePresent(Class<T> documentType, Iterable<String> ids) {
getDelegate().ensurePresent(documentType, ids);
}

@Override
public <T extends Revision> void ensureUnique(Class<T> documentType, String id) {
getDelegate().ensureUnique(documentType, id);
}

@Override
public <T extends Revision> void ensureUnique(Class<T> documentType, Iterable<String> ids) {
getDelegate().ensureUnique(documentType, ids);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public RepositoryBranchContext(RepositoryContext context, String path, RevisionS

this.path = path;
// configure query performance profiling
searcher.setMetrics(service(Metrics.class));
searcher.setMetrics(optionalService(Metrics.class).orElse(Metrics.NOOP));
bind(RevisionSearcher.class, searcher);
context.optionalService(ContextConfigurer.class).ifPresent(configurer -> configurer.configure(RepositoryBranchContext.this));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2011-2021 B2i Healthcare Pte Ltd, http://b2i.sg
* Copyright 2011-2023 B2i Healthcare Pte Ltd, http://b2i.sg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -191,6 +191,42 @@ public interface TransactionContext extends BranchContext, AutoCloseable {
*/
String parentLock();

/**
* Register an ID in the transaction and ensure its presence before committing the transaction changes upon the next {@link #commit()} call.
*
* @param <T>
* @param documentType - the document type to check
* @param id - the id that is required to be present in the system before committing
*/
<T extends Revision> void ensurePresent(Class<T> documentType, String id);

/**
* Register a collection of IDs in the transaction and ensure their presence before committing the transaction changes upon the next {@link #commit()} call.
*
* @param <T>
* @param documentType - the document type to check
* @param ids - the ids that are required to be present in the system before committing
*/
<T extends Revision> void ensurePresent(Class<T> documentType, Iterable<String> ids);

/**
* Register an ID in the transaction and ensure its uniqueness before committing the changes upon the next {@link #commit()} call.
*
* @param <T>
* @param documentType - the document type to check
* @param id - the id that is required to be unique before committing the transaction
*/
<T extends Revision> void ensureUnique(Class<T> documentType, String id);

/**
* Register a collection of IDs in the transaction and ensure their uniqueness before committing the changes upon the next {@link #commit()} call.
*
* @param <T>
* @param documentType - the document type to check
* @param ids - the ids that are required to be unique before committing the transaction
*/
<T extends Revision> void ensureUnique(Class<T> documentType, Iterable<String> ids);

@Override
default Builder<? extends TransactionContext> inject() {
return new DelegatingContext.Builder<TransactionContext>(TransactionContext.class, this);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2011-2022 B2i Healthcare Pte Ltd, http://b2i.sg
* Copyright 2011-2023 B2i Healthcare Pte Ltd, http://b2i.sg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -25,12 +25,14 @@
import org.eclipse.xtext.util.Pair;
import org.eclipse.xtext.util.Tuples;

import com.b2international.commons.exceptions.AlreadyExistsException;
import com.b2international.commons.exceptions.ConflictException;
import com.b2international.commons.exceptions.CycleDetectedException;
import com.b2international.index.Index;
import com.b2international.index.IndexException;
import com.b2international.index.Searcher;
import com.b2international.index.admin.IndexAdmin;
import com.b2international.index.mapping.DocumentMapping;
import com.b2international.index.mapping.Mappings;
import com.b2international.index.query.Expressions;
import com.b2international.index.query.Query;
Expand All @@ -50,6 +52,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;

/**
* @since 4.5
Expand All @@ -67,6 +72,9 @@ public final class RepositoryTransactionContext extends DelegatingBranchContext

@JsonIgnore
private transient final StagingArea staging;

private transient final Multimap<Class<? extends Revision>, String> ensureUnique = LinkedHashMultimap.create();
private transient final Multimap<Class<? extends Revision>, String> ensurePresent = LinkedHashMultimap.create();

public RepositoryTransactionContext(BranchContext context, String author, String commitComment, String parentLockContext) {
super(context);
Expand Down Expand Up @@ -250,6 +258,8 @@ public Optional<Commit> commit(String author, String commitComment, String paren
try {
locks.lock(lockContext, 1000L, lockTarget);
final long timestamp = service(TimestampProvider.class).getTimestamp();
log().info("Checking transaction content before commit to {}@{}", path(), timestamp);
checkTransaction();
log().info("Persisting changes to {}@{}", path(), timestamp);
commit = staging.commit(null, timestamp, author, commitComment);
log().info("Changes have been successfully persisted to {}@{}.", path(), timestamp);
Expand All @@ -269,6 +279,34 @@ public Optional<Commit> commit(String author, String commitComment, String paren
}
}

private void checkTransaction() {
if (!ensureUnique.isEmpty()) {
// check if any of the listed IDs are already present in the index and if so, report AlreadyExistsException for the first registered ID (same behavior as before with explicit checks)
for (Class<? extends Revision> type : ensureUnique.keySet()) {
final Collection<String> idsToCheck = ensureUnique.get(type);
final Map<String, ?> existingComponents = Maps.uniqueIndex(fetchComponents(idsToCheck, type), Revision::getId);
for (String idToCheck : idsToCheck) {
if (existingComponents.containsKey(idToCheck)) {
throw new AlreadyExistsException(DocumentMapping.getDocType(type), idToCheck);
}
}
}
}

if (!ensurePresent.isEmpty()) {
// check if any of the listed IDs are already present in the index and if NOT, report ComponentNotFoundException for the first registered ID (same behavior as before with explicit checks)
for (Class<? extends Revision> type : ensurePresent.keySet()) {
final Collection<String> idsToCheck = ensurePresent.get(type);
final Map<String, ?> existingComponents = Maps.uniqueIndex(fetchComponents(idsToCheck, type), Revision::getId);
for (String idToCheck : idsToCheck) {
if (!existingComponents.containsKey(idToCheck)) {
throw new ComponentNotFoundException(DocumentMapping.getDocType(type), idToCheck).toBadRequestException();
}
}
}
}
}

@Override
public void rollback() {
staging.rollback();
Expand All @@ -277,6 +315,8 @@ public void rollback() {

private void clear() {
resolvedObjectsById.clear();
ensureUnique.clear();
ensurePresent.clear();
}

@Override
Expand Down Expand Up @@ -317,6 +357,26 @@ public void clearContents() {
});
}

@Override
public <T extends Revision> void ensureUnique(Class<T> documentType, String id) {
ensureUnique.put(documentType, id);
}

@Override
public <T extends Revision> void ensureUnique(Class<T> documentType, Iterable<String> ids) {
ensureUnique.putAll(documentType, ids);
}

@Override
public <T extends Revision> void ensurePresent(Class<T> documentType, String id) {
ensurePresent.put(documentType, id);
}

@Override
public <T extends Revision> void ensurePresent(Class<T> documentType, Iterable<String> ids) {
ensurePresent.putAll(documentType, ids);
}

private static DatastoreLockContext createLockContext(String userId, String parentContextDescription) {
return new DatastoreLockContext(userId, DatastoreLockContextDescriptions.COMMIT, parentContextDescription);
}
Expand Down

0 comments on commit 11a0f9d

Please sign in to comment.