Merge remote-tracking branch 'remotes/origin/master' into im_20200728_term_multi_version_support

This commit is contained in:
ianmarshall 2020-09-03 22:37:39 -04:00
commit 019d08a064
14 changed files with 118 additions and 125 deletions

View File

@ -259,16 +259,7 @@
"added": "2019-11-25" "added": "2019-11-25"
}, },
{ {
"title": "Software Interoperability Lab for Asia", "title": "Standards and Interoperability Lab Asia",
"description": "",
"link": "",
"city": "Manila, Philippines",
"lat": 14.6914,
"lon": 120.9686,
"added": "2019-11-25"
},
{
"title": "Software Interoperability Lab for Asia",
"description": "SIL-A maintains a suite of tools used to promote interoperability and medical informatics across Asia.", "description": "SIL-A maintains a suite of tools used to promote interoperability and medical informatics across Asia.",
"link": "http://sil-asia.org/", "link": "http://sil-asia.org/",
"city": "Manila, Philippines", "city": "Manila, Philippines",

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 2062
title: "A deadlock was fixed where the Database-backed Binary Storage service could lock the system up when running
under very high contention."

View File

@ -222,11 +222,6 @@ public class DaoConfig {
*/ */
private boolean myLastNEnabled = false; private boolean myLastNEnabled = false;
/**
* @since 5.1.0
*/
private boolean myPreloadBlobFromInputStream = false;
/** /**
* Constructor * Constructor
*/ */
@ -2102,28 +2097,11 @@ public class DaoConfig {
* </p> * </p>
* *
* @since 5.1.0 * @since 5.1.0
* @deprecated In 5.2.0 this setting no longer does anything
*/ */
public boolean isPreloadBlobFromInputStream() { @Deprecated
return myPreloadBlobFromInputStream;
}
/**
* <p>
* This determines whether $binary-access-write operations should first load the InputStream into memory before persisting the
* contents to the database. This needs to be enabled for MS SQL Server as this DB requires that the blob size be known
* in advance.
* </p>
* <p>
* Note that this setting should be enabled with caution as it can lead to significant demands on memory.
* </p>
* <p>
* The default value for this setting is {@code false}.
* </p>
*
* @since 5.1.0
*/
public void setPreloadBlobFromInputStream(Boolean thePreloadBlobFromInputStream) { public void setPreloadBlobFromInputStream(Boolean thePreloadBlobFromInputStream) {
myPreloadBlobFromInputStream = thePreloadBlobFromInputStream; // ignore
} }
} }

View File

@ -60,8 +60,15 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
private DaoConfig myDaoConfig; private DaoConfig myDaoConfig;
@Override @Override
@Transactional(Transactional.TxType.SUPPORTS) @Transactional(Transactional.TxType.REQUIRED)
public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) throws IOException { public StoredDetails storeBlob(IIdType theResourceId, String theBlobIdOrNull, String theContentType, InputStream theInputStream) throws IOException {
/*
* Note on transactionality: This method used to have a propagation value of SUPPORTS and then do the actual
* write in a new transaction.. I don't actually get why that was the original design, but it causes
* connection pool deadlocks under load!
*/
Date publishedDate = new Date(); Date publishedDate = new Date();
HashingInputStream hashingInputStream = createHashingInputStream(theInputStream); HashingInputStream hashingInputStream = createHashingInputStream(theInputStream);
@ -77,32 +84,18 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
Session session = (Session) myEntityManager.getDelegate(); Session session = (Session) myEntityManager.getDelegate();
LobHelper lobHelper = session.getLobHelper(); LobHelper lobHelper = session.getLobHelper();
Blob dataBlob;
if (myDaoConfig.isPreloadBlobFromInputStream()) {
byte[] loadedStream = IOUtils.toByteArray(countingInputStream); byte[] loadedStream = IOUtils.toByteArray(countingInputStream);
dataBlob = lobHelper.createBlob(loadedStream); Blob dataBlob = lobHelper.createBlob(loadedStream);
} else {
dataBlob = lobHelper.createBlob(countingInputStream, 0);
}
entity.setBlob(dataBlob); entity.setBlob(dataBlob);
// Save the entity
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txTemplate.execute(t -> {
myEntityManager.persist(entity);
return null;
});
// Update the entity with the final byte count and hash // Update the entity with the final byte count and hash
long bytes = countingInputStream.getCount(); long bytes = countingInputStream.getCount();
String hash = hashingInputStream.hash().toString(); String hash = hashingInputStream.hash().toString();
txTemplate.execute(t -> { entity.setSize((int) bytes);
myBinaryStorageEntityDao.setSize(id, (int) bytes); entity.setHash(hash);
myBinaryStorageEntityDao.setHash(id, hash);
return null; // Save the entity
}); myEntityManager.persist(entity);
return new StoredDetails() return new StoredDetails()
.setBlobId(id) .setBlobId(id)

View File

@ -44,6 +44,10 @@ public class DaoSearchParamProvider implements ISearchParamProvider {
@Override @Override
public int refreshCache(SearchParamRegistryImpl theSearchParamRegistry, long theRefreshInterval) { public int refreshCache(SearchParamRegistryImpl theSearchParamRegistry, long theRefreshInterval) {
return theSearchParamRegistry.doRefresh(theRefreshInterval); int retVal = 0;
if (myDaoRegistry.getResourceDaoOrNull("SearchParameter") != null) {
retVal = theSearchParamRegistry.doRefresh(theRefreshInterval);
}
return retVal;
} }
} }

View File

@ -30,14 +30,6 @@ import java.util.Optional;
public interface IBinaryStorageEntityDao extends JpaRepository<BinaryStorageEntity, String> { public interface IBinaryStorageEntityDao extends JpaRepository<BinaryStorageEntity, String> {
@Modifying
@Query("UPDATE BinaryStorageEntity e SET e.mySize = :blob_size WHERE e.myBlobId = :blob_id")
void setSize(@Param("blob_id") String theId, @Param("blob_size") int theBytes);
@Modifying
@Query("UPDATE BinaryStorageEntity e SET e.myHash = :blob_hash WHERE e.myBlobId = :blob_id")
void setHash(@Param("blob_id") String theId, @Param("blob_hash") String theHash);
@Query("SELECT e FROM BinaryStorageEntity e WHERE e.myBlobId = :blob_id AND e.myResourceId = :resource_id") @Query("SELECT e FROM BinaryStorageEntity e WHERE e.myBlobId = :blob_id AND e.myResourceId = :resource_id")
Optional<BinaryStorageEntity> findByIdAndResourceId(@Param("blob_id") String theBlobId, @Param("resource_id") String theResourceId); Optional<BinaryStorageEntity> findByIdAndResourceId(@Param("blob_id") String theBlobId, @Param("resource_id") String theResourceId);

View File

@ -45,18 +45,6 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
@Autowired @Autowired
private DaoConfig myDaoConfig; private DaoConfig myDaoConfig;
@BeforeEach
public void backupDaoConfig() {
defaultPreloadBlobFromInputStream = myDaoConfig.isPreloadBlobFromInputStream();
}
@AfterEach
public void restoreDaoConfig() {
myDaoConfig.setPreloadBlobFromInputStream(defaultPreloadBlobFromInputStream);
}
boolean defaultPreloadBlobFromInputStream;
@Test @Test
public void testStoreAndRetrieve() throws IOException { public void testStoreAndRetrieve() throws IOException {
@ -74,7 +62,7 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
assertEquals(2, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
myCaptureQueriesListener.clear(); myCaptureQueriesListener.clear();
@ -128,7 +116,7 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size()); assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
assertEquals(2, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size()); assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
myCaptureQueriesListener.clear(); myCaptureQueriesListener.clear();

View File

@ -60,6 +60,14 @@ public abstract class BaseTask {
mySchemaVersion = theSchemaVersion; mySchemaVersion = theSchemaVersion;
} }
public String getProductVersion() {
return myProductVersion;
}
public String getSchemaVersion() {
return mySchemaVersion;
}
public boolean isNoColumnShrink() { public boolean isNoColumnShrink() {
return myNoColumnShrink; return myNoColumnShrink;
} }
@ -137,16 +145,18 @@ public abstract class BaseTask {
return myConnectionProperties; return myConnectionProperties;
} }
public void setConnectionProperties(DriverTypeEnum.ConnectionProperties theConnectionProperties) { public BaseTask setConnectionProperties(DriverTypeEnum.ConnectionProperties theConnectionProperties) {
myConnectionProperties = theConnectionProperties; myConnectionProperties = theConnectionProperties;
return this;
} }
public DriverTypeEnum getDriverType() { public DriverTypeEnum getDriverType() {
return myDriverType; return myDriverType;
} }
public void setDriverType(DriverTypeEnum theDriverType) { public BaseTask setDriverType(DriverTypeEnum theDriverType) {
myDriverType = theDriverType; myDriverType = theDriverType;
return this;
} }
public abstract void validate(); public abstract void validate();

View File

@ -86,9 +86,13 @@ public class DropIndexTask extends BaseTableTask {
@Language("SQL") String findConstraintSql = "SELECT DISTINCT constraint_name FROM user_cons_columns WHERE constraint_name = ? AND table_name = ?"; @Language("SQL") String findConstraintSql = "SELECT DISTINCT constraint_name FROM user_cons_columns WHERE constraint_name = ? AND table_name = ?";
@Language("SQL") String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?"; @Language("SQL") String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?";
findAndDropConstraint(findConstraintSql, dropConstraintSql); findAndDropConstraint(findConstraintSql, dropConstraintSql);
findConstraintSql = "SELECT DISTINCT constraint_name FROM all_constraints WHERE index_name = ? AND table_name = ?"; findConstraintSql = "SELECT DISTINCT constraint_name FROM all_constraints WHERE index_name = ? AND table_name = ?";
findAndDropConstraint(findConstraintSql, dropConstraintSql); findAndDropConstraint(findConstraintSql, dropConstraintSql);
} else if (getDriverType() == DriverTypeEnum.MSSQL_2012) {
// Legacy deletion for SQL Server unique indexes
@Language("SQL") String findConstraintSql = "SELECT tc.CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc WHERE tc.CONSTRAINT_NAME = ? AND tc.TABLE_NAME = ?";
@Language("SQL") String dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?";
findAndDropConstraint(findConstraintSql, dropConstraintSql);
} }
Set<String> indexNames = JdbcUtils.getIndexNames(getConnectionProperties(), getTableName()); Set<String> indexNames = JdbcUtils.getIndexNames(getConnectionProperties(), getTableName());

View File

@ -63,14 +63,15 @@ public class DropTableTask extends BaseTableTask {
} }
} }
DropIndexTask theIndexTask = new DropIndexTask(getProductVersion(), getSchemaVersion());
theIndexTask
.setTableName(getTableName())
.setConnectionProperties(getConnectionProperties())
.setDriverType(getDriverType());
for (String nextIndex : indexNames) { for (String nextIndex : indexNames) {
List<String> sqls = DropIndexTask.createDropIndexSql(getConnectionProperties(), getTableName(), nextIndex, getDriverType()); theIndexTask
if (!sqls.isEmpty()) { .setIndexName(nextIndex)
logInfo(ourLog, "Dropping index {} on table {} in preparation for table delete", nextIndex, getTableName()); .execute();
}
for (@Language("SQL") String sql : sqls) {
executeSql(getTableName(), sql);
}
} }
logInfo(ourLog, "Dropping table: {}", getTableName()); logInfo(ourLog, "Dropping table: {}", getTableName());

View File

@ -89,4 +89,12 @@ public class BinaryStorageEntity {
public String getBlobId() { public String getBlobId() {
return myBlobId; return myBlobId;
} }
public void setSize(int theSize) {
mySize = theSize;
}
public void setHash(String theHash) {
myHash = theHash;
}
} }

View File

@ -225,6 +225,11 @@ public class MethodUtil {
param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple()); param = new ConditionalParamBinder(theRestfulOperationTypeEnum, ((ConditionalUrlParam) nextAnnotation).supportsMultiple());
} else if (nextAnnotation instanceof OperationParam) { } else if (nextAnnotation instanceof OperationParam) {
Operation op = theMethod.getAnnotation(Operation.class); Operation op = theMethod.getAnnotation(Operation.class);
if (op == null) {
throw new ConfigurationException(
"@OperationParam detected on method that is not annotated with @Operation: " + theMethod.toGenericString());
}
OperationParam operationParam = (OperationParam) nextAnnotation; OperationParam operationParam = (OperationParam) nextAnnotation;
param = new OperationParameter(theContext, op.name(), operationParam); param = new OperationParameter(theContext, op.name(), operationParam);
if (isNotBlank(operationParam.typeName())) { if (isNotBlank(operationParam.typeName())) {

View File

@ -1,15 +1,20 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.util.TestUtil;
import com.google.common.collect.Lists;
import org.hamcrest.core.StringContains; import org.hamcrest.core.StringContains;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
@ -19,21 +24,19 @@ import org.junit.jupiter.api.Test;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
public class ServerInvalidDefinitionR4Test { public class ServerInvalidDefinitionR4Test extends BaseR4ServerTest {
private static FhirContext ourCtx = FhirContext.forR4();
@Test @Test
public void testWrongConditionalUrlType() { public void testWrongConditionalUrlType() throws Exception {
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new UpdateWithWrongConditionalUrlType());
try { try {
srv.init(); startServer(new UpdateWithWrongConditionalUrlType());
fail(); fail();
} catch (ServletException e) { } catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException")); assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
@ -43,13 +46,9 @@ public class ServerInvalidDefinitionR4Test {
} }
@Test @Test
public void testWrongResourceType() { public void testWrongResourceType() throws Exception {
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new UpdateWithWrongResourceType());
try { try {
srv.init(); startServer(new UpdateWithWrongResourceType());
fail(); fail();
} catch (ServletException e) { } catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException")); assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
@ -59,13 +58,9 @@ public class ServerInvalidDefinitionR4Test {
} }
@Test @Test
public void testWrongValidateModeType() { public void testWrongValidateModeType() throws Exception {
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new ValidateWithWrongModeType());
try { try {
srv.init(); startServer(new ValidateWithWrongModeType());
fail(); fail();
} catch (ServletException e) { } catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException")); assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
@ -74,13 +69,9 @@ public class ServerInvalidDefinitionR4Test {
} }
@Test @Test
public void testWrongValidateProfileType() { public void testWrongValidateProfileType() throws Exception {
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new ValidateWithWrongProfileType());
try { try {
srv.init(); startServer(new ValidateWithWrongProfileType());
fail(); fail();
} catch (ServletException e) { } catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException")); assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
@ -89,7 +80,7 @@ public class ServerInvalidDefinitionR4Test {
} }
@Test @Test
public void testWrongParameterAnnotationOnOperation() { public void testWrongParameterAnnotationOnOperation() throws Exception {
class MyProvider { class MyProvider {
@Operation(name = "foo") @Operation(name = "foo")
@ -99,18 +90,42 @@ public class ServerInvalidDefinitionR4Test {
} }
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.registerProvider(new MyProvider());
try { try {
srv.init(); startServer(new MyProvider());
fail(); fail();
} catch (ServletException e) { } catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("Failure scanning class MyProvider: Illegal method parameter annotation @OptionalParam on method: public ca.uhn.fhir.rest.api.MethodOutcome ca.uhn.fhir.rest.server.ServerInvalidDefinitionR4Test$1MyProvider.update(org.hl7.fhir.r4.model.StringType)")); assertThat(e.getCause().toString(), StringContains.containsString("Failure scanning class MyProvider: Illegal method parameter annotation @OptionalParam on method: public ca.uhn.fhir.rest.api.MethodOutcome ca.uhn.fhir.rest.server.ServerInvalidDefinitionR4Test$1MyProvider.update(org.hl7.fhir.r4.model.StringType)"));
} }
} }
/**
* @OperationParam on a search method
* <p>
* See #2063
*/
@Test
public void testOperationParamOnASearchMethod() throws Exception {
class MyProvider extends ServerMethodSelectionR4Test.MyBaseProvider {
@Search
public List<IBaseResource> search(
@OptionalParam(name = "name") StringType theName,
@OperationParam(name = "name2") StringType theName2
) {
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
}
}
MyProvider provider = new MyProvider();
try {
startServer(provider);
fail();
} catch (ServletException e) {
assertEquals("Failure scanning class MyProvider: @OperationParam detected on method that is not annotated with @Operation: public java.util.List<org.hl7.fhir.instance.model.api.IBaseResource> ca.uhn.fhir.rest.server.ServerInvalidDefinitionR4Test$2MyProvider.search(org.hl7.fhir.r4.model.StringType,org.hl7.fhir.r4.model.StringType)", e.getCause().getMessage());
}
}
public static class UpdateWithWrongConditionalUrlType implements IResourceProvider { public static class UpdateWithWrongConditionalUrlType implements IResourceProvider {
@Override @Override

View File

@ -51,7 +51,6 @@ public abstract class BaseJavaConfig${versionCapitalized} extends ca.uhn.fhir.jp
#foreach ( $res in $resources ) #foreach ( $res in $resources )
@Bean(name="my${res.name}Dao${versionCapitalized}", autowire=Autowire.BY_NAME) @Bean(name="my${res.name}Dao${versionCapitalized}", autowire=Autowire.BY_NAME)
@Lazy
public public
#if ( ${versionCapitalized} == 'Dstu2' && ${res.name} == 'ValueSet' ) #if ( ${versionCapitalized} == 'Dstu2' && ${res.name} == 'ValueSet' )
IFhirResourceDaoValueSet<ca.uhn.fhir.model.dstu2.resource.ValueSet, ca.uhn.fhir.model.dstu2.composite.CodingDt, ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt> IFhirResourceDaoValueSet<ca.uhn.fhir.model.dstu2.resource.ValueSet, ca.uhn.fhir.model.dstu2.composite.CodingDt, ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt>