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"
},
{
"title": "Software Interoperability Lab for Asia",
"description": "",
"link": "",
"city": "Manila, Philippines",
"lat": 14.6914,
"lon": 120.9686,
"added": "2019-11-25"
},
{
"title": "Software Interoperability Lab for Asia",
"title": "Standards and Interoperability Lab Asia",
"description": "SIL-A maintains a suite of tools used to promote interoperability and medical informatics across Asia.",
"link": "http://sil-asia.org/",
"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;
/**
* @since 5.1.0
*/
private boolean myPreloadBlobFromInputStream = false;
/**
* Constructor
*/
@ -2102,28 +2097,11 @@ public class DaoConfig {
* </p>
*
* @since 5.1.0
* @deprecated In 5.2.0 this setting no longer does anything
*/
public boolean isPreloadBlobFromInputStream() {
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
*/
@Deprecated
public void setPreloadBlobFromInputStream(Boolean thePreloadBlobFromInputStream) {
myPreloadBlobFromInputStream = thePreloadBlobFromInputStream;
// ignore
}
}

View File

@ -60,8 +60,15 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
private DaoConfig myDaoConfig;
@Override
@Transactional(Transactional.TxType.SUPPORTS)
@Transactional(Transactional.TxType.REQUIRED)
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();
HashingInputStream hashingInputStream = createHashingInputStream(theInputStream);
@ -77,32 +84,18 @@ public class DatabaseBlobBinaryStorageSvcImpl extends BaseBinaryStorageSvcImpl {
Session session = (Session) myEntityManager.getDelegate();
LobHelper lobHelper = session.getLobHelper();
Blob dataBlob;
if (myDaoConfig.isPreloadBlobFromInputStream()) {
byte[] loadedStream = IOUtils.toByteArray(countingInputStream);
dataBlob = lobHelper.createBlob(loadedStream);
} else {
dataBlob = lobHelper.createBlob(countingInputStream, 0);
}
byte[] loadedStream = IOUtils.toByteArray(countingInputStream);
Blob dataBlob = lobHelper.createBlob(loadedStream);
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
long bytes = countingInputStream.getCount();
String hash = hashingInputStream.hash().toString();
txTemplate.execute(t -> {
myBinaryStorageEntityDao.setSize(id, (int) bytes);
myBinaryStorageEntityDao.setHash(id, hash);
return null;
});
entity.setSize((int) bytes);
entity.setHash(hash);
// Save the entity
myEntityManager.persist(entity);
return new StoredDetails()
.setBlobId(id)

View File

@ -44,6 +44,10 @@ public class DaoSearchParamProvider implements ISearchParamProvider {
@Override
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> {
@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")
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
private DaoConfig myDaoConfig;
@BeforeEach
public void backupDaoConfig() {
defaultPreloadBlobFromInputStream = myDaoConfig.isPreloadBlobFromInputStream();
}
@AfterEach
public void restoreDaoConfig() {
myDaoConfig.setPreloadBlobFromInputStream(defaultPreloadBlobFromInputStream);
}
boolean defaultPreloadBlobFromInputStream;
@Test
public void testStoreAndRetrieve() throws IOException {
@ -74,7 +62,7 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
assertEquals(2, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
myCaptureQueriesListener.clear();
@ -128,7 +116,7 @@ public class DatabaseBlobBinaryStorageSvcImplTest extends BaseJpaR4Test {
assertEquals(0, myCaptureQueriesListener.getSelectQueriesForCurrentThread().size());
assertEquals(1, myCaptureQueriesListener.getInsertQueriesForCurrentThread().size());
assertEquals(2, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
assertEquals(0, myCaptureQueriesListener.getUpdateQueriesForCurrentThread().size());
myCaptureQueriesListener.clear();

View File

@ -60,6 +60,14 @@ public abstract class BaseTask {
mySchemaVersion = theSchemaVersion;
}
public String getProductVersion() {
return myProductVersion;
}
public String getSchemaVersion() {
return mySchemaVersion;
}
public boolean isNoColumnShrink() {
return myNoColumnShrink;
}
@ -137,16 +145,18 @@ public abstract class BaseTask {
return myConnectionProperties;
}
public void setConnectionProperties(DriverTypeEnum.ConnectionProperties theConnectionProperties) {
public BaseTask setConnectionProperties(DriverTypeEnum.ConnectionProperties theConnectionProperties) {
myConnectionProperties = theConnectionProperties;
return this;
}
public DriverTypeEnum getDriverType() {
return myDriverType;
}
public void setDriverType(DriverTypeEnum theDriverType) {
public BaseTask setDriverType(DriverTypeEnum theDriverType) {
myDriverType = theDriverType;
return this;
}
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 dropConstraintSql = "ALTER TABLE " + getTableName() + " DROP CONSTRAINT ?";
findAndDropConstraint(findConstraintSql, dropConstraintSql);
findConstraintSql = "SELECT DISTINCT constraint_name FROM all_constraints WHERE index_name = ? AND table_name = ?";
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());

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) {
List<String> sqls = DropIndexTask.createDropIndexSql(getConnectionProperties(), getTableName(), nextIndex, getDriverType());
if (!sqls.isEmpty()) {
logInfo(ourLog, "Dropping index {} on table {} in preparation for table delete", nextIndex, getTableName());
}
for (@Language("SQL") String sql : sqls) {
executeSql(getTableName(), sql);
}
theIndexTask
.setIndexName(nextIndex)
.execute();
}
logInfo(ourLog, "Dropping table: {}", getTableName());

View File

@ -89,4 +89,12 @@ public class BinaryStorageEntity {
public String getBlobId() {
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());
} else if (nextAnnotation instanceof OperationParam) {
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;
param = new OperationParameter(theContext, op.name(), operationParam);
if (isNotBlank(operationParam.typeName())) {

View File

@ -1,15 +1,20 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
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.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.annotation.Validate;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.collect.Lists;
import org.hamcrest.core.StringContains;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Patient;
@ -19,21 +24,19 @@ import org.junit.jupiter.api.Test;
import javax.servlet.ServletException;
import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class ServerInvalidDefinitionR4Test {
private static FhirContext ourCtx = FhirContext.forR4();
public class ServerInvalidDefinitionR4Test extends BaseR4ServerTest {
@Test
public void testWrongConditionalUrlType() {
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new UpdateWithWrongConditionalUrlType());
public void testWrongConditionalUrlType() throws Exception {
try {
srv.init();
startServer(new UpdateWithWrongConditionalUrlType());
fail();
} catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
@ -43,13 +46,9 @@ public class ServerInvalidDefinitionR4Test {
}
@Test
public void testWrongResourceType() {
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new UpdateWithWrongResourceType());
public void testWrongResourceType() throws Exception {
try {
srv.init();
startServer(new UpdateWithWrongResourceType());
fail();
} catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
@ -59,13 +58,9 @@ public class ServerInvalidDefinitionR4Test {
}
@Test
public void testWrongValidateModeType() {
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new ValidateWithWrongModeType());
public void testWrongValidateModeType() throws Exception {
try {
srv.init();
startServer(new ValidateWithWrongModeType());
fail();
} catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
@ -74,13 +69,9 @@ public class ServerInvalidDefinitionR4Test {
}
@Test
public void testWrongValidateProfileType() {
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.setResourceProviders(new ValidateWithWrongProfileType());
public void testWrongValidateProfileType() throws Exception {
try {
srv.init();
startServer(new ValidateWithWrongProfileType());
fail();
} catch (ServletException e) {
assertThat(e.getCause().toString(), StringContains.containsString("ConfigurationException"));
@ -89,7 +80,7 @@ public class ServerInvalidDefinitionR4Test {
}
@Test
public void testWrongParameterAnnotationOnOperation() {
public void testWrongParameterAnnotationOnOperation() throws Exception {
class MyProvider {
@Operation(name = "foo")
@ -99,18 +90,42 @@ public class ServerInvalidDefinitionR4Test {
}
RestfulServer srv = new RestfulServer(ourCtx);
srv.setFhirContext(ourCtx);
srv.registerProvider(new MyProvider());
try {
srv.init();
startServer(new MyProvider());
fail();
} 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)"));
}
}
/**
* @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 {
@Override

View File

@ -51,8 +51,7 @@ public abstract class BaseJavaConfig${versionCapitalized} extends ca.uhn.fhir.jp
#foreach ( $res in $resources )
@Bean(name="my${res.name}Dao${versionCapitalized}", autowire=Autowire.BY_NAME)
@Lazy
public
public
#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>
#elseif ( ${versionCapitalized} == 'Dstu3' && ${res.name} == 'ValueSet' )