Skip to content

Commit 27af838

Browse files
authored
Add resource type to the HFJ_RESOURCE_TYPE primary key to avoid collisions (#7171)
* 7152 - Add resource type to HFJ_RESOURCE_MODIFIED's PK to avoid conflicts * 7495 Fix integration tests affected by changes * 7495 Add unit tests plus some refinements * 7495: incorporate feedback from code review * 7495 - Add a test to verify updates work as expected when resources share external ID and version * 7152: Adjust version of DB we are migrating to * 7152: Add entry in upgrade.md regarding database changes
1 parent a0babeb commit 27af838

File tree

9 files changed

+124
-24
lines changed

9 files changed

+124
-24
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: fix
3+
issue: 7152
4+
jira: SMILE-10226
5+
title: "Modified resources could potentially be removed from the subscription list of they shared ID and Version with
6+
others. This has been fixed."

hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_4_0/upgrade.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ As of `8.3.12-SNAPSHOT`, HAPI-FHIR snapshots are now published on [Maven Central
2727

2828
* `$export` and `$everything` operations on Patient Compartment (instance or type) will no longer return `List` or `Group` resources, regardless of auth rules.
2929

30+
## Zero-Downtime Upgrades with Subscriptions
31+
32+
The database upgrade includes changes to the `HFJ_RESOURCE_MODIFIED` table. This table holds transitional data about resources that have been modified. Under normal conditions, this table will be empty or nearly so. It is recommended to verify that subscriptions are running smoothly to avoid prolonged table locks while the table structure is updated.

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ public HapiFhirJpaMigrationTasks(Set<String> theFlags) {
130130
init780();
131131
init820();
132132
init840();
133+
init860();
134+
}
135+
136+
protected void init860() {
137+
Builder version = forVersion(VersionEnum.V8_4_0);
138+
{
139+
version.onTable("HFJ_RESOURCE_MODIFIED").dropPrimaryKey("20250729.1");
140+
version.onTable("HFJ_RESOURCE_MODIFIED").addPrimaryKey("20250729.2", "RES_ID", "RES_VER", "RESOURCE_TYPE");
141+
}
133142
}
134143

135144
protected void init840() {

hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessagePersistenceSvcImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,11 @@ ResourceModifiedEntity createEntityFrom(ResourceModifiedMessage theMsg) {
171171
IIdType theMsgId = theMsg.getPayloadId(myFhirContext);
172172

173173
ResourceModifiedEntity resourceModifiedEntity = new ResourceModifiedEntity();
174-
resourceModifiedEntity.setResourceModifiedEntityPK(with(theMsgId.getIdPart(), theMsgId.getVersionIdPart()));
174+
resourceModifiedEntity.setResourceModifiedEntityPK(
175+
with(theMsgId.getIdPart(), theMsgId.getVersionIdPart(), theMsgId.getResourceType()));
175176

176177
String partialModifiedMessage = getPayloadLessMessageAsString(theMsg);
177178
resourceModifiedEntity.setSummaryResourceModifiedMessage(partialModifiedMessage);
178-
resourceModifiedEntity.setResourceType(theMsgId.getResourceType());
179179
resourceModifiedEntity.setCreatedTime(new Date());
180180

181181
return resourceModifiedEntity;

hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PersistedResourceModifiedMessageEntityPK.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public class PersistedResourceModifiedMessageEntityPK implements IPersistedResou
3535
@Column(name = "RES_VER", length = 8, nullable = false)
3636
private String myResourceVersion;
3737

38+
@Column(name = "RESOURCE_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false)
39+
private String myResourceType;
40+
3841
public String getResourcePid() {
3942
return myResourcePid;
4043
}
@@ -53,27 +56,40 @@ public PersistedResourceModifiedMessageEntityPK setResourceVersion(String theRes
5356
return this;
5457
}
5558

56-
public static PersistedResourceModifiedMessageEntityPK with(String theResourcePid, String theResourceVersion) {
59+
public String getResourceType() {
60+
return myResourceType;
61+
}
62+
63+
public PersistedResourceModifiedMessageEntityPK setResourceType(String theResourceType) {
64+
myResourceType = theResourceType;
65+
return this;
66+
}
67+
68+
public static PersistedResourceModifiedMessageEntityPK with(
69+
String theResourcePid, String theResourceVersion, String theResourceType) {
5770
return new PersistedResourceModifiedMessageEntityPK()
5871
.setResourcePid(theResourcePid)
59-
.setResourceVersion(theResourceVersion);
72+
.setResourceVersion(theResourceVersion)
73+
.setResourceType(theResourceType);
6074
}
6175

6276
@Override
6377
public boolean equals(Object theO) {
6478
if (this == theO) return true;
6579
if (theO == null || getClass() != theO.getClass()) return false;
6680
PersistedResourceModifiedMessageEntityPK that = (PersistedResourceModifiedMessageEntityPK) theO;
67-
return myResourcePid.equals(that.myResourcePid) && myResourceVersion.equals(that.myResourceVersion);
81+
return myResourcePid.equals(that.myResourcePid)
82+
&& myResourceVersion.equals(that.myResourceVersion)
83+
&& myResourceType.equals(that.myResourceType);
6884
}
6985

7086
@Override
7187
public int hashCode() {
72-
return Objects.hash(myResourcePid, myResourceVersion);
88+
return Objects.hash(myResourcePid, myResourceVersion, myResourceType);
7389
}
7490

7591
@Override
7692
public String toString() {
77-
return myResourcePid + "/" + myResourceVersion;
93+
return myResourceType + "/" + myResourcePid + "/" + myResourceVersion;
7894
}
7995
}

hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceModifiedEntity.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ public class ResourceModifiedEntity implements IPersistedResourceModifiedMessage
5353
@Temporal(TemporalType.TIMESTAMP)
5454
private Date myCreatedTime;
5555

56-
@Column(name = "RESOURCE_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false)
57-
private String myResourceType;
58-
5956
public PersistedResourceModifiedMessageEntityPK getResourceModifiedEntityPK() {
6057
return myResourceModifiedEntityPK;
6158
}
@@ -68,12 +65,7 @@ public ResourceModifiedEntity setResourceModifiedEntityPK(
6865

6966
@Override
7067
public String getResourceType() {
71-
return myResourceType;
72-
}
73-
74-
public ResourceModifiedEntity setResourceType(String theResourceType) {
75-
myResourceType = theResourceType;
76-
return this;
68+
return myResourceModifiedEntityPK == null ? null : myResourceModifiedEntityPK.getResourceType();
7769
}
7870

7971
@Override

hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import ca.uhn.fhir.util.BundleUtil;
2222
import ca.uhn.test.concurrency.PointcutLatch;
2323
import com.apicatalog.jsonld.StringUtils;
24+
import com.google.common.base.Strings;
2425
import jakarta.annotation.Nonnull;
2526
import jakarta.annotation.PostConstruct;
2627
import net.ttddyy.dsproxy.QueryCount;
@@ -224,21 +225,45 @@ protected Observation buildBaseObservation(String theCode, String theSystem) {
224225
}
225226

226227
protected Patient sendPatient() {
228+
return sendPatient(null);
229+
}
230+
231+
protected Patient sendPatient(String thePatientId) {
227232
Patient patient = new Patient();
228-
patient.setActive(true);
233+
if(!Strings.isNullOrEmpty(thePatientId)) {
234+
patient.setId(thePatientId);
235+
}
229236

230-
IIdType id = myPatientDao.create(patient).getId();
231-
patient.setId(id);
237+
DaoMethodOutcome outcome;
238+
if(Strings.isNullOrEmpty(thePatientId)) {
239+
outcome = myPatientDao.create(patient);
240+
patient.setId( outcome.getId());
241+
} else {
242+
myPatientDao.update(patient);
243+
}
232244

245+
patient.setActive(true);
233246
return patient;
234247
}
235248

236249
protected Organization sendOrganization() {
250+
return sendOrganization(null);
251+
}
252+
253+
protected Organization sendOrganization( String theOrgId) {
237254
Organization org = new Organization();
238-
org.setName("ORG");
255+
org.setName("ORG" + (theOrgId == null ? "" : theOrgId));
256+
if(!Strings.isNullOrEmpty(theOrgId)) {
257+
org.setId(theOrgId);
258+
}
239259

240-
IIdType id = myOrganizationDao.create(org).getId();
241-
org.setId(id);
260+
DaoMethodOutcome outcome;
261+
if(Strings.isNullOrEmpty(theOrgId)) {
262+
outcome = myOrganizationDao.create(org);
263+
org.setId(outcome.getId());
264+
} else {
265+
myOrganizationDao.update(org);
266+
}
242267

243268
return org;
244269
}

hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/async/AsyncSubscriptionMessageSubmissionIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ public void runDeliveryPass_withManyResources_isBatchedAndKeepsResourceUsageDown
100100
int numberOfResourcesToCreate = factor * AsyncResourceModifiedSubmitterSvc.MAX_LIMIT;
101101

102102
ResourceModifiedEntity entity = new ResourceModifiedEntity();
103-
entity.setResourceType(resourceType);
104103
PersistedResourceModifiedMessageEntityPK rpm = new PersistedResourceModifiedMessageEntityPK();
105104
rpm.setResourceVersion("1");
105+
rpm.setResourceType(resourceType);
106106
entity.setResourceModifiedEntityPK(rpm);
107107

108108
// we reuse the same exact msg content to avoid

hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/message/MessageSubscriptionR4Test.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.hl7.fhir.r4.model.Observation;
2727
import org.hl7.fhir.r4.model.Organization;
2828
import org.hl7.fhir.r4.model.Patient;
29+
import org.hl7.fhir.r4.model.ResourceType;
2930
import org.hl7.fhir.r4.model.Subscription;
3031
import org.junit.jupiter.api.AfterEach;
3132
import org.junit.jupiter.api.BeforeEach;
@@ -238,13 +239,61 @@ public void testMethodDeleteByPK_whenEntityExists_willDeleteTheEntityAndReturnTr
238239
.hasSize(0);
239240
}
240241

242+
@Test
243+
public void testMethodDeleteByPK_whenEntitiesExistWithRepeatedId_willDeleteTheCorrectEntityAndReturnTrue(){
244+
String commonId = "common-id";
245+
246+
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
247+
248+
// given
249+
TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager);
250+
251+
Patient patient = sendPatient(commonId);
252+
ResourceModifiedMessage patientResourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, patient, BaseResourceMessage.OperationTypeEnum.CREATE);
253+
IPersistedResourceModifiedMessage persistedPatientResourceModifiedMessage = myResourceModifiedMessagePersistenceSvc.persist(patientResourceModifiedMessage);
254+
255+
Organization org = sendOrganization(commonId);
256+
ResourceModifiedMessage orgResourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, org, BaseResourceMessage.OperationTypeEnum.CREATE);
257+
myResourceModifiedMessagePersistenceSvc.persist(orgResourceModifiedMessage);
258+
259+
// when
260+
boolean wasDeleted = transactionTemplate.execute(tx -> myResourceModifiedMessagePersistenceSvc.deleteByPK(persistedPatientResourceModifiedMessage.getPersistedResourceModifiedMessagePk()));
261+
262+
// then
263+
assertTrue(wasDeleted);
264+
Page<IPersistedResourceModifiedMessage> messages = myResourceModifiedMessagePersistenceSvc.findAllOrderedByCreatedTime(Pageable.unpaged());
265+
assertThat(messages).hasSize(1);
266+
assertEquals(ResourceType.Organization.name(), messages.stream().toList().get(0).getResourceType());
267+
268+
}
269+
270+
@Test
271+
public void testMethodPersist_AddEntriesWithSameExternalIdAndVersion_expectSuccess(){
272+
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
273+
274+
// given
275+
String commonId = "common-id";
276+
Patient patient = sendPatient(commonId);
277+
Organization organization = sendOrganization(commonId);
278+
279+
ResourceModifiedMessage patientResourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, patient, BaseResourceMessage.OperationTypeEnum.CREATE);
280+
ResourceModifiedMessage organizationResourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, organization, BaseResourceMessage.OperationTypeEnum.CREATE);
281+
282+
// when
283+
myResourceModifiedMessagePersistenceSvc.persist(patientResourceModifiedMessage);
284+
myResourceModifiedMessagePersistenceSvc.persist(organizationResourceModifiedMessage);
285+
286+
// then
287+
assertEquals(2, myResourceModifiedMessagePersistenceSvc.getMessagePersistedCount());
288+
}
289+
241290
@Test
242291
public void testMethodDeleteByPK_whenEntityDoesNotExist_willReturnFalse(){
243292
mySubscriptionTestUtil.unregisterSubscriptionInterceptor();
244293

245294
// given
246295
TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager);
247-
IPersistedResourceModifiedMessagePK nonExistentResourceWithPk = PersistedResourceModifiedMessageEntityPK.with("one", "one");
296+
IPersistedResourceModifiedMessagePK nonExistentResourceWithPk = PersistedResourceModifiedMessageEntityPK.with("one", "one", ResourceType.Patient.toString());
248297

249298
// when
250299
boolean wasDeleted = transactionTemplate.execute(tx -> myResourceModifiedMessagePersistenceSvc.deleteByPK(nonExistentResourceWithPk));

0 commit comments

Comments
 (0)