From 6889e17672dbf78fa7628ea700f1c8356e1ceff1 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 26 Jun 2026 10:53:42 +0200 Subject: [PATCH 1/4] fix: Run sync request synchronously [DHIS2-21606][2.42] --- .../validation/PersistablesFilter.java | 17 +++- .../validator/event/ExistenceValidator.java | 8 +- ...SingleEventDataSynchronizationService.java | 80 ++++++++++------ ...leEventDataSynchronizationServiceTest.java | 94 +++++++++++++++++++ 4 files changed, 164 insertions(+), 35 deletions(-) create mode 100644 dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationServiceTest.java diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/PersistablesFilter.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/PersistablesFilter.java index 6db6e8e36c9d..42a567c21577 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/PersistablesFilter.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/PersistablesFilter.java @@ -174,12 +174,27 @@ private void filter() { private void collectDeletables(Class type, List entities) { for (T entity : entities) { - if (isValid(entity)) { + if (isValid(entity) && !isAlreadyDeleted(entity)) { collectPersistable(type, entity); } } } + private boolean isAlreadyDeleted(TrackerDto entity) { + if (entity instanceof Event ev) { + org.hisp.dhis.program.Event existing = preheat.getEvent(ev.getEvent()); + return existing != null && existing.isDeleted(); + } else if (entity instanceof Enrollment en) { + org.hisp.dhis.program.Enrollment existing = preheat.getEnrollment(en.getEnrollment()); + return existing != null && existing.isDeleted(); + } else if (entity instanceof TrackedEntity te) { + org.hisp.dhis.trackedentity.TrackedEntity existing = + preheat.getTrackedEntity(te.getTrackedEntity()); + return existing != null && existing.isDeleted(); + } + return false; + } + private void collectPersistables( Class type, List> parents, List entities) { for (T entity : entities) { diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/ExistenceValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/ExistenceValidator.java index 4e1a8ad44f1e..5e316e1db174 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/ExistenceValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/ExistenceValidator.java @@ -50,10 +50,12 @@ public void validate( Event existingEvent = bundle.getPreheat().getEvent(event.getEvent()); - // If the event is soft-deleted no operation is allowed if (existingEvent != null && existingEvent.isDeleted()) { - reporter.addError(event, E1082, event.getEvent()); - return; + if (importStrategy.isDelete()) { + reporter.addWarning(event, E1082, event.getEvent()); + } else { + reporter.addError(event, E1082, event.getEvent()); + } } if (existingEvent != null && importStrategy.isCreate()) { diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java index e446918524a9..d62df87d550f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java @@ -30,13 +30,12 @@ package org.hisp.dhis.webapi.controller.tracker.sync; import static java.lang.String.format; -import static org.hisp.dhis.dxf2.sync.SyncUtils.runSyncRequest; import static org.hisp.dhis.scheduling.JobProgress.FailurePolicy.SKIP_ITEM; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Date; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -44,6 +43,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.common.UID; +import org.hisp.dhis.commons.jackson.config.JacksonObjectMapperConfig; +import org.hisp.dhis.dxf2.importsummary.ImportCount; import org.hisp.dhis.dxf2.importsummary.ImportStatus; import org.hisp.dhis.dxf2.importsummary.ImportSummary; import org.hisp.dhis.dxf2.metadata.sync.exception.MetadataSyncServiceException; @@ -65,12 +66,15 @@ import org.hisp.dhis.tracker.export.event.EventOperationParams; import org.hisp.dhis.tracker.export.event.EventService; import org.hisp.dhis.tracker.imports.TrackerImportStrategy; +import org.hisp.dhis.tracker.imports.report.ImportReport; +import org.hisp.dhis.tracker.imports.report.Stats; import org.hisp.dhis.webapi.controller.tracker.export.event.EventMapper; -import org.hisp.dhis.webmessage.WebMessageResponse; import org.mapstruct.factory.Mappers; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RequestCallback; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; /** @@ -82,6 +86,7 @@ public class SingleEventDataSynchronizationService extends TrackerDataSynchronizationWithPaging { private static final String PROCESS_NAME = "Single event programs data synchronization"; private static final EventMapper EVENT_MAPPER = Mappers.getMapper(EventMapper.class); + private static final ObjectMapper JSON_MAPPER = JacksonObjectMapperConfig.staticJsonMapper(); private final EventService eventService; private final SystemSettingsService systemSettingsService; @@ -130,7 +135,7 @@ public SynchronizationResult synchronizeTrackerData(int pageSize, JobProgress pr return endProcess(progress, "No events to synchronize", PROCESS_NAME); } - boolean success = executeSynchronizationWithPaging(context, progress, settings); + boolean success = executeSynchronizationWithPaging(context, progress); return success ? endProcess(progress, "Completed successfully", PROCESS_NAME) @@ -180,7 +185,7 @@ private Map> getSkipSyncProgramStageDataElements() { } private boolean executeSynchronizationWithPaging( - EventSynchronizationContext context, JobProgress progress, SystemSettings settings) { + EventSynchronizationContext context, JobProgress progress) { String stageDescription = format( "Found %d single events. Remote: %s. Pages: %d (size %d)", @@ -194,15 +199,14 @@ private boolean executeSynchronizationWithPaging( progress.runStage( IntStream.range(1, context.getPages() + 1).boxed(), page -> format("Syncing page %d (size %d)", page, context.getPageSize()), - page -> synchronizePageSafely(page, context, settings)); + page -> synchronizePageSafely(page, context)); return !progress.isSkipCurrentStage(); } - private void synchronizePageSafely( - int page, EventSynchronizationContext context, SystemSettings settings) { + private void synchronizePageSafely(int page, EventSynchronizationContext context) { try { - synchronizePage(page, context, settings); + synchronizePage(page, context); } catch (Exception ex) { log.error("Failed to synchronize page {}", page, ex); throw new RuntimeException( @@ -210,8 +214,7 @@ private void synchronizePageSafely( } } - private void synchronizePage( - int page, EventSynchronizationContext context, SystemSettings settings) + private void synchronizePage(int page, EventSynchronizationContext context) throws ForbiddenException, BadRequestException { List events = fetchEventsForPage(page, context); @@ -219,7 +222,7 @@ private void synchronizePage( List deletedEvents = partitionedEvents.get(true); List activeEvents = partitionedEvents.get(false); - syncEventsByDeletionStatus(activeEvents, deletedEvents, context, settings); + syncEventsByDeletionStatus(activeEvents, deletedEvents, context); } private List fetchEventsForPage(int page, EventSynchronizationContext context) @@ -242,36 +245,31 @@ private Map> partitionEventsByDeletionStatus(List ev } private void syncEventsByDeletionStatus( - List activeEvents, - List deletedEvents, - EventSynchronizationContext context, - SystemSettings settings) { + List activeEvents, List deletedEvents, EventSynchronizationContext context) { Date syncTime = context.getStartTime(); SystemInstance instance = context.getInstance(); if (!activeEvents.isEmpty()) { List activeEventDtos = activeEvents.stream().map(EVENT_MAPPER::map).toList(); - syncEvents( - activeEventDtos, instance, settings, syncTime, TrackerImportStrategy.CREATE_AND_UPDATE); + syncEvents(activeEventDtos, instance, syncTime, TrackerImportStrategy.CREATE_AND_UPDATE); } if (!deletedEvents.isEmpty()) { List deletedEventDtos = deletedEvents.stream().map(this::toMinimalEvent).toList(); - syncEvents(deletedEventDtos, instance, settings, syncTime, TrackerImportStrategy.DELETE); + syncEvents(deletedEventDtos, instance, syncTime, TrackerImportStrategy.DELETE); } } private void syncEvents( List events, SystemInstance instance, - SystemSettings settings, Date syncTime, TrackerImportStrategy importStrategy) { String url = instance.getUrl() + "?importStrategy=" + importStrategy; - ImportSummary summary = sendTrackerRequest(events, instance, settings, url); + ImportSummary summary = sendTrackerRequest(events, instance, url); if (summary == null || summary.getStatus() != ImportStatus.SUCCESS) { throw new MetadataSyncServiceException( @@ -289,19 +287,39 @@ private void syncEvents( private ImportSummary sendTrackerRequest( List events, SystemInstance instance, - SystemSettings settings, String url) { RequestCallback requestCallback = createRequestCallback(events, instance); + String syncUrl = url + "&async=false"; + try { + return restTemplate.execute( + syncUrl, + HttpMethod.POST, + requestCallback, + response -> { + ImportReport report = JSON_MAPPER.readValue(response.getBody(), ImportReport.class); + return toImportSummary(report); + }); + } catch (RestClientException ex) { + log.error("Failed to POST single event sync payload: {}", ex.getMessage(), ex); + return null; + } + } - Optional response = - runSyncRequest( - restTemplate, - requestCallback, - SyncEndpoint.TRACKER_IMPORT.getKlass(), - url, - settings.getSyncMaxAttempts()); - - return response.map(ImportSummary.class::cast).orElse(null); + static ImportSummary toImportSummary(ImportReport report) { + ImportSummary summary = new ImportSummary(); + summary.setStatus( + switch (report.getStatus()) { + case OK -> ImportStatus.SUCCESS; + case WARNING -> ImportStatus.WARNING; + case ERROR -> ImportStatus.ERROR; + }); + Stats stats = report.getStats(); + if (stats != null) { + summary.setImportCount( + new ImportCount( + stats.getCreated(), stats.getUpdated(), stats.getIgnored(), stats.getDeleted())); + } + return summary; } private RequestCallback createRequestCallback( diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationServiceTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationServiceTest.java new file mode 100644 index 000000000000..5f9100f73af6 --- /dev/null +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationServiceTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.controller.tracker.sync; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.hisp.dhis.dxf2.importsummary.ImportStatus; +import org.hisp.dhis.dxf2.importsummary.ImportSummary; +import org.hisp.dhis.tracker.imports.report.ImportReport; +import org.hisp.dhis.tracker.imports.report.Stats; +import org.hisp.dhis.tracker.imports.report.Status; +import org.junit.jupiter.api.Test; + +class SingleEventDataSynchronizationServiceTest { + + @Test + void shouldMapOkStatusToSuccess() { + ImportReport report = ImportReport.builder().status(Status.OK).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(ImportStatus.SUCCESS, summary.getStatus()); + } + + @Test + void shouldMapWarningStatusToWarning() { + ImportReport report = ImportReport.builder().status(Status.WARNING).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(ImportStatus.WARNING, summary.getStatus()); + } + + @Test + void shouldMapErrorStatusToError() { + ImportReport report = ImportReport.builder().status(Status.ERROR).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(ImportStatus.ERROR, summary.getStatus()); + } + + @Test + void shouldMapStats() { + Stats stats = Stats.builder().created(1).updated(2).deleted(3).ignored(4).build(); + ImportReport report = ImportReport.builder().status(Status.OK).stats(stats).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(1, summary.getImportCount().getImported()); + assertEquals(2, summary.getImportCount().getUpdated()); + assertEquals(3, summary.getImportCount().getDeleted()); + assertEquals(4, summary.getImportCount().getIgnored()); + } + + @Test + void shouldLeaveImportCountZeroedWhenStatsAbsent() { + ImportReport report = ImportReport.builder().status(Status.OK).build(); + + ImportSummary summary = SingleEventDataSynchronizationService.toImportSummary(report); + + assertEquals(0, summary.getImportCount().getImported()); + assertEquals(0, summary.getImportCount().getUpdated()); + assertEquals(0, summary.getImportCount().getDeleted()); + assertEquals(0, summary.getImportCount().getIgnored()); + } +} From bbdfe38178dfa4b5875ec952bc6d12464c1ce3ff Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 26 Jun 2026 11:21:22 +0200 Subject: [PATCH 2/4] fix: Overload retry mechanism [DHIS2-21606][2.42] --- .../org/hisp/dhis/dxf2/sync/SyncUtils.java | 33 +++++++++++ ...SingleEventDataSynchronizationService.java | 56 +++++++++++-------- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java index 182ccf450b49..f80b7ec6e748 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java @@ -104,6 +104,39 @@ public static boolean sendSyncRequest( return false; } + public static Optional runSyncRequest( + RestTemplate restTemplate, + RequestCallback requestCallback, + ResponseExtractor responseExtractor, + String syncUrl, + int maxSyncAttempts) { + boolean networkErrorOccurred = true; + int syncAttemptsDone = 0; + T responseSummary = null; + + while (networkErrorOccurred) { + networkErrorOccurred = false; + syncAttemptsDone++; + try { + responseSummary = + restTemplate.execute(syncUrl, HttpMethod.POST, requestCallback, responseExtractor); + } catch (HttpServerErrorException ex) { + log.error( + "Internal error happened during sync request: {}", ex.getResponseBodyAsString(), ex); + if (syncAttemptsDone <= maxSyncAttempts) { + networkErrorOccurred = true; + } else { + throw ex; + } + } catch (ResourceAccessException ex) { + log.error("Exception during sync request: {}", ex.getMessage(), ex); + throw ex; + } + } + + return Optional.ofNullable(responseSummary); + } + public static Optional runSyncRequest( RestTemplate restTemplate, RequestCallback requestCallback, diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java index d62df87d550f..c4b9eaa0ae59 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java @@ -30,6 +30,7 @@ package org.hisp.dhis.webapi.controller.tracker.sync; import static java.lang.String.format; +import static org.hisp.dhis.dxf2.sync.SyncUtils.runSyncRequest; import static org.hisp.dhis.scheduling.JobProgress.FailurePolicy.SKIP_ITEM; import com.fasterxml.jackson.databind.ObjectMapper; @@ -70,11 +71,9 @@ import org.hisp.dhis.tracker.imports.report.Stats; import org.hisp.dhis.webapi.controller.tracker.export.event.EventMapper; import org.mapstruct.factory.Mappers; -import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RequestCallback; -import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; /** @@ -97,9 +96,10 @@ public class SingleEventDataSynchronizationService extends TrackerDataSynchroniz @Getter private static final class EventSynchronizationContext extends PagedDataSynchronisationContext { private final Map> skipSyncDataElementsByProgramStage; + private final int maxAttempts; public EventSynchronizationContext(Date skipChangedBefore, int pageSize) { - this(skipChangedBefore, 0, null, pageSize, Map.of()); + this(skipChangedBefore, 0, null, pageSize, Map.of(), 1); } public EventSynchronizationContext( @@ -107,9 +107,11 @@ public EventSynchronizationContext( long objectsToSynchronize, SystemInstance instance, int pageSize, - Map> skipSyncDataElementsByProgramStage) { + Map> skipSyncDataElementsByProgramStage, + int maxAttempts) { super(skipChangedBefore, objectsToSynchronize, instance, pageSize); this.skipSyncDataElementsByProgramStage = skipSyncDataElementsByProgramStage; + this.maxAttempts = maxAttempts; } public boolean hasNoObjectsToSynchronize() { @@ -165,7 +167,12 @@ private EventSynchronizationContext createContext(int pageSize, SystemSettings s getSkipSyncProgramStageDataElements(); return new EventSynchronizationContext( - skipChangedBefore, eventCount, instance, pageSize, skipSyncProgramStageDataElements); + skipChangedBefore, + eventCount, + instance, + pageSize, + skipSyncProgramStageDataElements, + settings.getSyncMaxAttempts()); } private long countEventsForSynchronization(Date skipChangedBefore) @@ -248,17 +255,23 @@ private void syncEventsByDeletionStatus( List activeEvents, List deletedEvents, EventSynchronizationContext context) { Date syncTime = context.getStartTime(); SystemInstance instance = context.getInstance(); + int maxAttempts = context.getMaxAttempts(); if (!activeEvents.isEmpty()) { List activeEventDtos = activeEvents.stream().map(EVENT_MAPPER::map).toList(); - syncEvents(activeEventDtos, instance, syncTime, TrackerImportStrategy.CREATE_AND_UPDATE); + syncEvents( + activeEventDtos, + instance, + syncTime, + TrackerImportStrategy.CREATE_AND_UPDATE, + maxAttempts); } if (!deletedEvents.isEmpty()) { List deletedEventDtos = deletedEvents.stream().map(this::toMinimalEvent).toList(); - syncEvents(deletedEventDtos, instance, syncTime, TrackerImportStrategy.DELETE); + syncEvents(deletedEventDtos, instance, syncTime, TrackerImportStrategy.DELETE, maxAttempts); } } @@ -266,10 +279,11 @@ private void syncEvents( List events, SystemInstance instance, Date syncTime, - TrackerImportStrategy importStrategy) { + TrackerImportStrategy importStrategy, + int maxAttempts) { String url = instance.getUrl() + "?importStrategy=" + importStrategy; - ImportSummary summary = sendTrackerRequest(events, instance, url); + ImportSummary summary = sendTrackerRequest(events, instance, url, maxAttempts); if (summary == null || summary.getStatus() != ImportStatus.SUCCESS) { throw new MetadataSyncServiceException( @@ -287,22 +301,18 @@ private void syncEvents( private ImportSummary sendTrackerRequest( List events, SystemInstance instance, - String url) { + String url, + int maxAttempts) { RequestCallback requestCallback = createRequestCallback(events, instance); String syncUrl = url + "&async=false"; - try { - return restTemplate.execute( - syncUrl, - HttpMethod.POST, - requestCallback, - response -> { - ImportReport report = JSON_MAPPER.readValue(response.getBody(), ImportReport.class); - return toImportSummary(report); - }); - } catch (RestClientException ex) { - log.error("Failed to POST single event sync payload: {}", ex.getMessage(), ex); - return null; - } + return runSyncRequest( + restTemplate, + requestCallback, + response -> + toImportSummary(JSON_MAPPER.readValue(response.getBody(), ImportReport.class)), + syncUrl, + maxAttempts) + .orElse(null); } static ImportSummary toImportSummary(ImportReport report) { From 2fa126c378aa82b21b8363c3dcc91bc83bfbb375 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 26 Jun 2026 11:30:25 +0200 Subject: [PATCH 3/4] fix: Get max attempts from settings [DHIS2-21606][2.42] --- ...SingleEventDataSynchronizationService.java | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java index c4b9eaa0ae59..6dcb4f583427 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/tracker/sync/SingleEventDataSynchronizationService.java @@ -96,10 +96,9 @@ public class SingleEventDataSynchronizationService extends TrackerDataSynchroniz @Getter private static final class EventSynchronizationContext extends PagedDataSynchronisationContext { private final Map> skipSyncDataElementsByProgramStage; - private final int maxAttempts; public EventSynchronizationContext(Date skipChangedBefore, int pageSize) { - this(skipChangedBefore, 0, null, pageSize, Map.of(), 1); + this(skipChangedBefore, 0, null, pageSize, Map.of()); } public EventSynchronizationContext( @@ -107,11 +106,9 @@ public EventSynchronizationContext( long objectsToSynchronize, SystemInstance instance, int pageSize, - Map> skipSyncDataElementsByProgramStage, - int maxAttempts) { + Map> skipSyncDataElementsByProgramStage) { super(skipChangedBefore, objectsToSynchronize, instance, pageSize); this.skipSyncDataElementsByProgramStage = skipSyncDataElementsByProgramStage; - this.maxAttempts = maxAttempts; } public boolean hasNoObjectsToSynchronize() { @@ -137,7 +134,7 @@ public SynchronizationResult synchronizeTrackerData(int pageSize, JobProgress pr return endProcess(progress, "No events to synchronize", PROCESS_NAME); } - boolean success = executeSynchronizationWithPaging(context, progress); + boolean success = executeSynchronizationWithPaging(context, progress, settings); return success ? endProcess(progress, "Completed successfully", PROCESS_NAME) @@ -167,12 +164,7 @@ private EventSynchronizationContext createContext(int pageSize, SystemSettings s getSkipSyncProgramStageDataElements(); return new EventSynchronizationContext( - skipChangedBefore, - eventCount, - instance, - pageSize, - skipSyncProgramStageDataElements, - settings.getSyncMaxAttempts()); + skipChangedBefore, eventCount, instance, pageSize, skipSyncProgramStageDataElements); } private long countEventsForSynchronization(Date skipChangedBefore) @@ -192,7 +184,7 @@ private Map> getSkipSyncProgramStageDataElements() { } private boolean executeSynchronizationWithPaging( - EventSynchronizationContext context, JobProgress progress) { + EventSynchronizationContext context, JobProgress progress, SystemSettings settings) { String stageDescription = format( "Found %d single events. Remote: %s. Pages: %d (size %d)", @@ -206,14 +198,15 @@ private boolean executeSynchronizationWithPaging( progress.runStage( IntStream.range(1, context.getPages() + 1).boxed(), page -> format("Syncing page %d (size %d)", page, context.getPageSize()), - page -> synchronizePageSafely(page, context)); + page -> synchronizePageSafely(page, context, settings)); return !progress.isSkipCurrentStage(); } - private void synchronizePageSafely(int page, EventSynchronizationContext context) { + private void synchronizePageSafely( + int page, EventSynchronizationContext context, SystemSettings settings) { try { - synchronizePage(page, context); + synchronizePage(page, context, settings); } catch (Exception ex) { log.error("Failed to synchronize page {}", page, ex); throw new RuntimeException( @@ -221,7 +214,8 @@ private void synchronizePageSafely(int page, EventSynchronizationContext context } } - private void synchronizePage(int page, EventSynchronizationContext context) + private void synchronizePage( + int page, EventSynchronizationContext context, SystemSettings settings) throws ForbiddenException, BadRequestException { List events = fetchEventsForPage(page, context); @@ -229,7 +223,7 @@ private void synchronizePage(int page, EventSynchronizationContext context) List deletedEvents = partitionedEvents.get(true); List activeEvents = partitionedEvents.get(false); - syncEventsByDeletionStatus(activeEvents, deletedEvents, context); + syncEventsByDeletionStatus(activeEvents, deletedEvents, context, settings); } private List fetchEventsForPage(int page, EventSynchronizationContext context) @@ -252,38 +246,36 @@ private Map> partitionEventsByDeletionStatus(List ev } private void syncEventsByDeletionStatus( - List activeEvents, List deletedEvents, EventSynchronizationContext context) { + List activeEvents, + List deletedEvents, + EventSynchronizationContext context, + SystemSettings settings) { Date syncTime = context.getStartTime(); SystemInstance instance = context.getInstance(); - int maxAttempts = context.getMaxAttempts(); if (!activeEvents.isEmpty()) { List activeEventDtos = activeEvents.stream().map(EVENT_MAPPER::map).toList(); syncEvents( - activeEventDtos, - instance, - syncTime, - TrackerImportStrategy.CREATE_AND_UPDATE, - maxAttempts); + activeEventDtos, instance, settings, syncTime, TrackerImportStrategy.CREATE_AND_UPDATE); } if (!deletedEvents.isEmpty()) { List deletedEventDtos = deletedEvents.stream().map(this::toMinimalEvent).toList(); - syncEvents(deletedEventDtos, instance, syncTime, TrackerImportStrategy.DELETE, maxAttempts); + syncEvents(deletedEventDtos, instance, settings, syncTime, TrackerImportStrategy.DELETE); } } private void syncEvents( List events, SystemInstance instance, + SystemSettings settings, Date syncTime, - TrackerImportStrategy importStrategy, - int maxAttempts) { + TrackerImportStrategy importStrategy) { String url = instance.getUrl() + "?importStrategy=" + importStrategy; - ImportSummary summary = sendTrackerRequest(events, instance, url, maxAttempts); + ImportSummary summary = sendTrackerRequest(events, instance, settings, url); if (summary == null || summary.getStatus() != ImportStatus.SUCCESS) { throw new MetadataSyncServiceException( @@ -301,8 +293,8 @@ private void syncEvents( private ImportSummary sendTrackerRequest( List events, SystemInstance instance, - String url, - int maxAttempts) { + SystemSettings settings, + String url) { RequestCallback requestCallback = createRequestCallback(events, instance); String syncUrl = url + "&async=false"; return runSyncRequest( @@ -311,7 +303,7 @@ private ImportSummary sendTrackerRequest( response -> toImportSummary(JSON_MAPPER.readValue(response.getBody(), ImportReport.class)), syncUrl, - maxAttempts) + settings.getSyncMaxAttempts()) .orElse(null); } From 70d9bb1a42f04588e426b0099c26b3ee73193fb2 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 26 Jun 2026 11:49:09 +0200 Subject: [PATCH 4/4] fix: Make runSyncRequest return ImportSummary --- .../src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java index f80b7ec6e748..698b4db4e654 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/SyncUtils.java @@ -104,15 +104,15 @@ public static boolean sendSyncRequest( return false; } - public static Optional runSyncRequest( + public static Optional runSyncRequest( RestTemplate restTemplate, RequestCallback requestCallback, - ResponseExtractor responseExtractor, + ResponseExtractor responseExtractor, String syncUrl, int maxSyncAttempts) { boolean networkErrorOccurred = true; int syncAttemptsDone = 0; - T responseSummary = null; + ImportSummary responseSummary = null; while (networkErrorOccurred) { networkErrorOccurred = false;