diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_2_0/2062-avoid-db-bin-storage-deadlock.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_2_0/2062-avoid-db-bin-storage-deadlock.yaml new file mode 100644 index 00000000000..a767efaa4f9 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_2_0/2062-avoid-db-bin-storage-deadlock.yaml @@ -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." diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java index 98f49f6ff2d..03c8f3f5c5b 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/config/DaoConfig.java @@ -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 { *

* * @since 5.1.0 + * @deprecated In 5.2.0 this setting no longer does anything */ - public boolean isPreloadBlobFromInputStream() { - return myPreloadBlobFromInputStream; - } - - /** - *

- * 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. - *

- *

- * Note that this setting should be enabled with caution as it can lead to significant demands on memory. - *

- *

- * The default value for this setting is {@code false}. - *

- * - * @since 5.1.0 - */ + @Deprecated public void setPreloadBlobFromInputStream(Boolean thePreloadBlobFromInputStream) { - myPreloadBlobFromInputStream = thePreloadBlobFromInputStream; + // ignore } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/DatabaseBlobBinaryStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/DatabaseBlobBinaryStorageSvcImpl.java index 54a9e0e023c..1c5dd64ce9c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/DatabaseBlobBinaryStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/binstore/DatabaseBlobBinaryStorageSvcImpl.java @@ -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) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBinaryStorageEntityDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBinaryStorageEntityDao.java index e32a3604a66..dc17d8c1429 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBinaryStorageEntityDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBinaryStorageEntityDao.java @@ -30,14 +30,6 @@ import java.util.Optional; public interface IBinaryStorageEntityDao extends JpaRepository { - @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 findByIdAndResourceId(@Param("blob_id") String theBlobId, @Param("resource_id") String theResourceId); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/DatabaseBlobBinaryStorageSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/DatabaseBlobBinaryStorageSvcImplTest.java index c97eec91e5b..3c4bddd735b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/DatabaseBlobBinaryStorageSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/binstore/DatabaseBlobBinaryStorageSvcImplTest.java @@ -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(); diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java index 3fe3308b0b0..c496232c52e 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/BinaryStorageEntity.java @@ -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; + } }